From b83d490ca844879fce09b38651506c6acd319fc7 Mon Sep 17 00:00:00 2001 From: lleeoo Date: Fri, 24 Nov 2023 11:24:58 +0100 Subject: [PATCH 01/19] wip --- docs/CHANGELOG.md | 1 - docs/LICENSE.md | 1 - docs/commands.md | 6 --- pyproject.toml | 1 + src/ralph/api/models.py | 1 + src/ralph/conf.py | 15 ++++--- src/ralph/models/xapi/base/agents.py | 10 +++-- src/ralph/models/xapi/base/common.py | 6 ++- src/ralph/models/xapi/base/ifi.py | 10 +++-- src/ralph/models/xapi/base/results.py | 7 ++- src/ralph/models/xapi/config.py | 2 +- .../models/xapi/virtual_classroom/results.py | 3 +- tests/fixtures/hypothesis_strategies.py | 8 ++++ tests/models/xapi/base/test_statements.py | 45 ++++++++++++++++++- tests/test_cli.py | 2 + 15 files changed, 90 insertions(+), 28 deletions(-) delete mode 120000 docs/CHANGELOG.md delete mode 120000 docs/LICENSE.md delete mode 100644 docs/commands.md diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md deleted file mode 120000 index 04c99a55c..000000000 --- a/docs/CHANGELOG.md +++ /dev/null @@ -1 +0,0 @@ -../CHANGELOG.md \ No newline at end of file diff --git a/docs/LICENSE.md b/docs/LICENSE.md deleted file mode 120000 index 7eabdb1c2..000000000 --- a/docs/LICENSE.md +++ /dev/null @@ -1 +0,0 @@ -../LICENSE.md \ No newline at end of file diff --git a/docs/commands.md b/docs/commands.md deleted file mode 100644 index 12616600f..000000000 --- a/docs/commands.md +++ /dev/null @@ -1,6 +0,0 @@ -# Commands - -::: mkdocs-click - :module: ralph.cli - :command: cli - :depth: 1 diff --git a/pyproject.toml b/pyproject.toml index e6c6421e4..b82227c97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,6 +96,7 @@ dev = [ "moto==4.2.8", "mypy==1.7.0", "neoteroi-mkdocs==1.0.4", + "polyfactory==2.12.0", "pyfakefs==5.3.0", "pymdown-extensions==10.4", "pytest==7.4.3", diff --git a/src/ralph/api/models.py b/src/ralph/api/models.py index 94a8ee3d1..c60f77e08 100644 --- a/src/ralph/api/models.py +++ b/src/ralph/api/models.py @@ -4,6 +4,7 @@ validation. """ from typing import Optional, Union + from uuid import UUID from pydantic import AnyUrl, BaseModel, Extra diff --git a/src/ralph/conf.py b/src/ralph/conf.py index eaf979d2f..1a303ecbf 100644 --- a/src/ralph/conf.py +++ b/src/ralph/conf.py @@ -4,9 +4,9 @@ import sys from enum import Enum from pathlib import Path -from typing import List, Sequence, Tuple, Union +from typing import Annotated, List, Sequence, Tuple, Union -from pydantic import AnyHttpUrl, AnyUrl, BaseModel, BaseSettings, Extra, root_validator +from pydantic import AnyHttpUrl, AnyUrl, BaseModel, BaseSettings, Extra, Field, root_validator, StrictStr from ralph.exceptions import ConfigurationException @@ -29,6 +29,8 @@ MODEL_PATH_SEPARATOR = "__" +NonEmptyStr = Annotated[str, Field(min_length=1)] +NonEmptyStrictStr = Annotated[StrictStr, Field(min_length=1)] class BaseSettingsConfig: """Pydantic model for BaseSettings Configuration.""" @@ -118,17 +120,16 @@ class ParserSettings(BaseModel): GELF: GELFParserSettings = GELFParserSettings() ES: ESParserSettings = ESParserSettings() - class XapiForwardingConfigurationSettings(BaseModel): """Pydantic model for xAPI forwarding configuration item.""" - class Config: # noqa: D106 - min_anystr_length = 1 + # class Config: # noqa: D106 # TODO: done + # min_anystr_length = 1 url: AnyUrl is_active: bool - basic_username: str - basic_password: str + basic_username: NonEmptyStr + basic_password: NonEmptyStr max_retries: int timeout: float diff --git a/src/ralph/models/xapi/base/agents.py b/src/ralph/models/xapi/base/agents.py index 9f6ce53f5..7d2d909a5 100644 --- a/src/ralph/models/xapi/base/agents.py +++ b/src/ralph/models/xapi/base/agents.py @@ -6,7 +6,8 @@ from pydantic import StrictStr -from ..config import BaseModelWithConfig +from ralph.conf import NonEmptyStr, NonEmptyStrictStr +from ralph.models.xapi.config import BaseModelWithConfig from .common import IRI from .ifi import ( BaseXapiAccountIFI, @@ -30,9 +31,12 @@ class BaseXapiAgentAccount(BaseModelWithConfig): """ homePage: IRI - name: StrictStr + name: NonEmptyStrictStr +from typing import Annotated +from pydantic import Field + class BaseXapiAgentCommonProperties(BaseModelWithConfig, ABC): """Pydantic model for core `Agent` type property. @@ -44,7 +48,7 @@ class BaseXapiAgentCommonProperties(BaseModelWithConfig, ABC): """ objectType: Optional[Literal["Agent"]] - name: Optional[StrictStr] + name: Optional[NonEmptyStrictStr] class BaseXapiAgentWithMbox(BaseXapiAgentCommonProperties, BaseXapiMboxIFI): diff --git a/src/ralph/models/xapi/base/common.py b/src/ralph/models/xapi/base/common.py index 14c27d1e7..30a024663 100644 --- a/src/ralph/models/xapi/base/common.py +++ b/src/ralph/models/xapi/base/common.py @@ -33,9 +33,13 @@ def validate(tag: str) -> Type["LanguageTag"]: yield validate +from typing import Annotated +from pydantic import Field -LanguageMap = Dict[LanguageTag, StrictStr] +LanguageMap = Dict[LanguageTag, Annotated[StrictStr, Field(min_length=1)]] +# pattern = r'mailto:\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b' +# MailtoEmail = Field(regex=pattern)#MailtoEmail class MailtoEmail(str): """Pydantic custom data type validating `mailto:email` format.""" diff --git a/src/ralph/models/xapi/base/ifi.py b/src/ralph/models/xapi/base/ifi.py index 642e933c7..8fa9e288e 100644 --- a/src/ralph/models/xapi/base/ifi.py +++ b/src/ralph/models/xapi/base/ifi.py @@ -5,6 +5,8 @@ from ..config import BaseModelWithConfig from .common import IRI, MailtoEmail +from ralph.conf import NonEmptyStrictStr + class BaseXapiAccount(BaseModelWithConfig): """Pydantic model for IFI `account` property. @@ -15,8 +17,10 @@ class BaseXapiAccount(BaseModelWithConfig): """ homePage: IRI - name: StrictStr + name: NonEmptyStrictStr +from typing import Annotated +from pydantic import Field class BaseXapiMboxIFI(BaseModelWithConfig): """Pydantic model for mailto Inverse Functional Identifier. @@ -24,8 +28,8 @@ class BaseXapiMboxIFI(BaseModelWithConfig): Attributes: mbox (MailtoEmail): Consists of the Agent's email address. """ - - mbox: MailtoEmail + pattern = r'mailto:\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b' + mbox: Annotated[str, Field(regex=pattern)]#MailtoEmail class BaseXapiMboxSha1SumIFI(BaseModelWithConfig): diff --git a/src/ralph/models/xapi/base/results.py b/src/ralph/models/xapi/base/results.py index bd3d49ec9..905cf4b10 100644 --- a/src/ralph/models/xapi/base/results.py +++ b/src/ralph/models/xapi/base/results.py @@ -9,6 +9,8 @@ from ..config import BaseModelWithConfig from .common import IRI +from ralph.conf import NonEmptyStrictStr + class BaseXapiResultScore(BaseModelWithConfig): """Pydantic model for result `score` property. @@ -44,7 +46,8 @@ def check_raw_min_max_relation(cls, values: Any) -> Any: return values - +from pydantic import Field +from typing import Annotated class BaseXapiResult(BaseModelWithConfig): """Pydantic model for `result` property. @@ -61,6 +64,6 @@ class BaseXapiResult(BaseModelWithConfig): score: Optional[BaseXapiResultScore] success: Optional[StrictBool] completion: Optional[StrictBool] - response: Optional[StrictStr] + response: Optional[NonEmptyStrictStr] duration: Optional[timedelta] extensions: Optional[Dict[IRI, Union[str, int, bool, list, dict, None]]] diff --git a/src/ralph/models/xapi/config.py b/src/ralph/models/xapi/config.py index 38927c7b6..a28eb2687 100644 --- a/src/ralph/models/xapi/config.py +++ b/src/ralph/models/xapi/config.py @@ -6,7 +6,7 @@ class BaseModelWithConfig(BaseModel): """Pydantic model for base configuration shared among all models.""" - class Config: # noqa: D106 + class Config: # noqa: D106 # TODO: doing extra = Extra.forbid min_anystr_length = 1 diff --git a/src/ralph/models/xapi/virtual_classroom/results.py b/src/ralph/models/xapi/virtual_classroom/results.py index 0bde87312..6cb42ca67 100644 --- a/src/ralph/models/xapi/virtual_classroom/results.py +++ b/src/ralph/models/xapi/virtual_classroom/results.py @@ -4,6 +4,7 @@ from ..base.results import BaseXapiResult +from ralph.conf import NonEmptyStrictStr class VirtualClassroomAnsweredPollResult(BaseXapiResult): """Pydantic model for virtual classroom answered poll `result` property. @@ -12,4 +13,4 @@ class VirtualClassroomAnsweredPollResult(BaseXapiResult): response (str): Consists of the response for the given Activity. """ - response: StrictStr # = StrictStr() + response: NonEmptyStrictStr # = StrictStr() diff --git a/tests/fixtures/hypothesis_strategies.py b/tests/fixtures/hypothesis_strategies.py index 0d73f53ff..9cde0b3ee 100644 --- a/tests/fixtures/hypothesis_strategies.py +++ b/tests/fixtures/hypothesis_strategies.py @@ -102,6 +102,14 @@ def custom_given(*args: Union[st.SearchStrategy, BaseModel], **kwargs): strategies.append(custom_builds(arg) if is_base_model(arg) else arg) return given(*strategies, **kwargs) +# from polyfactory.factories.pydantic_factory import ModelFactory + +# def custom_given(klass): +# def wrapper(func): +# ModelFactor.create_factory(model=klass) +# func() +# return wrapper + OVERWRITTEN_STRATEGIES = { UISeqPrev: { diff --git a/tests/models/xapi/base/test_statements.py b/tests/models/xapi/base/test_statements.py index a88fca426..690beba1d 100644 --- a/tests/models/xapi/base/test_statements.py +++ b/tests/models/xapi/base/test_statements.py @@ -27,14 +27,51 @@ from tests.fixtures.hypothesis_strategies import custom_builds, custom_given +from polyfactory.factories.pydantic_factory import ModelFactory +from polyfactory import Use +from ralph.models.xapi.base.common import IRI +from ralph.models.xapi.base.agents import BaseXapiAgentWithMbox +from typing import Dict, Type, Any + + +from uuid import UUID, uuid4 + +# class IRIFactory(ModelFactory[IRI]): +# __model__ = IRI +# name = Use(ModelFactory.__random__.choice, ["Roxy", "Spammy", "Moshe"]) + +from ralph.models.xapi.base.common import LanguageTag + +class BaseXapiStatementFactory(ModelFactory[BaseXapiStatement]): + __model__ = BaseXapiStatement + + @classmethod + def get_provider_map(cls) -> Dict[Type, Any]: + providers_map = super().get_provider_map() + return { + IRI: lambda: IRI("https://w3id.org/xapi/video/verbs/played"), + BaseXapiAgentWithMbox: lambda: BaseXapiAgentWithMbox(mbox="mailto:test@toast.com"), + UUID: lambda: UUID(uuid4()), + LanguageTag: lambda: LanguageTag("en-US"), + **providers_map, + + } + + @classmethod + def _get_or_create_factory(cls, model: type): + created_factory = super()._get_or_create_factory(model) + created_factory.get_provider_map = cls.get_provider_map + created_factory._get_or_create_factory = cls._get_or_create_factory + return created_factory + @pytest.mark.parametrize( "path", ["id", "stored", "verb__display", "context__contextActivities__parent"], ) @pytest.mark.parametrize("value", [None, "", {}]) -@custom_given(BaseXapiStatement) -def test_models_xapi_base_statement_with_invalid_null_values(path, value, statement): +#@custom_given(BaseXapiStatement) +def test_models_xapi_base_statement_with_invalid_null_values(path, value): """Test that the statement does not accept any null values. XAPI-00001 @@ -42,6 +79,10 @@ def test_models_xapi_base_statement_with_invalid_null_values(path, value, statem value is set to "null", an empty object, or has no value, except in an "extensions" property. """ + # klass = BaseXapiStatement + # factory = ModelFactory.create_factory(model=klass) + statement = BaseXapiStatementFactory.build() + statement = statement.dict(exclude_none=True) set_dict_value_from_path(statement, path.split("__"), value) with pytest.raises(ValidationError, match="invalid empty value"): diff --git a/tests/test_cli.py b/tests/test_cli.py index a859adea8..cd980a167 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -352,6 +352,8 @@ def test_cli_auth_command_when_writing_auth_file( username_1, password_1, scopes_1, ifi_command_1, ifi_value_1, write=True ) + print(cli_args) + assert Path(settings.AUTH_FILE).exists() is False result = runner.invoke(cli, cli_args) assert result.exit_code == 0 From 4cc0b64782d4af1c3ae01f4e075b7393a919bfc2 Mon Sep 17 00:00:00 2001 From: lleeoo Date: Fri, 24 Nov 2023 11:38:10 +0100 Subject: [PATCH 02/19] wip --- src/ralph/models/xapi/base/common.py | 3 ++- src/ralph/models/xapi/base/ifi.py | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/ralph/models/xapi/base/common.py b/src/ralph/models/xapi/base/common.py index 30a024663..26132c4fc 100644 --- a/src/ralph/models/xapi/base/common.py +++ b/src/ralph/models/xapi/base/common.py @@ -35,8 +35,9 @@ def validate(tag: str) -> Type["LanguageTag"]: from typing import Annotated from pydantic import Field +from ralph.conf import NonEmptyStrictStr -LanguageMap = Dict[LanguageTag, Annotated[StrictStr, Field(min_length=1)]] +LanguageMap = Dict[LanguageTag, NonEmptyStrictStr] # pattern = r'mailto:\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b' # MailtoEmail = Field(regex=pattern)#MailtoEmail diff --git a/src/ralph/models/xapi/base/ifi.py b/src/ralph/models/xapi/base/ifi.py index 8fa9e288e..5e03331ac 100644 --- a/src/ralph/models/xapi/base/ifi.py +++ b/src/ralph/models/xapi/base/ifi.py @@ -28,8 +28,9 @@ class BaseXapiMboxIFI(BaseModelWithConfig): Attributes: mbox (MailtoEmail): Consists of the Agent's email address. """ - pattern = r'mailto:\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b' - mbox: Annotated[str, Field(regex=pattern)]#MailtoEmail + # pattern = r'mailto:\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b' + # mbox: Annotated[str, Field(regex=pattern)]# + mbox: MailtoEmail class BaseXapiMboxSha1SumIFI(BaseModelWithConfig): From facfb0fa500ec151fb19c713bc5b8571ff1ce751 Mon Sep 17 00:00:00 2001 From: lleeoo Date: Mon, 27 Nov 2023 10:49:55 +0100 Subject: [PATCH 03/19] wip --- src/ralph/conf.py | 2 +- src/ralph/models/xapi/base/agents.py | 8 +------- src/ralph/models/xapi/base/contexts.py | 7 +++---- src/ralph/models/xapi/base/groups.py | 5 ++--- src/ralph/models/xapi/base/ifi.py | 4 +--- src/ralph/models/xapi/base/results.py | 2 +- src/ralph/models/xapi/virtual_classroom/results.py | 2 -- .../models/xapi/virtual_classroom/statements.py | 1 - tests/fixtures/hypothesis_configuration.py | 4 ++++ tests/models/xapi/base/test_statements.py | 14 +++++++++----- 10 files changed, 22 insertions(+), 27 deletions(-) diff --git a/src/ralph/conf.py b/src/ralph/conf.py index 1a303ecbf..9bc061757 100644 --- a/src/ralph/conf.py +++ b/src/ralph/conf.py @@ -6,7 +6,7 @@ from pathlib import Path from typing import Annotated, List, Sequence, Tuple, Union -from pydantic import AnyHttpUrl, AnyUrl, BaseModel, BaseSettings, Extra, Field, root_validator, StrictStr +from pydantic import AnyHttpUrl, AnyUrl, BaseModel, BaseSettings, constr, Extra, Field, root_validator, StrictStr from ralph.exceptions import ConfigurationException diff --git a/src/ralph/models/xapi/base/agents.py b/src/ralph/models/xapi/base/agents.py index 7d2d909a5..1be73f8b3 100644 --- a/src/ralph/models/xapi/base/agents.py +++ b/src/ralph/models/xapi/base/agents.py @@ -4,8 +4,6 @@ from abc import ABC from typing import Optional, Union -from pydantic import StrictStr - from ralph.conf import NonEmptyStr, NonEmptyStrictStr from ralph.models.xapi.config import BaseModelWithConfig from .common import IRI @@ -33,10 +31,6 @@ class BaseXapiAgentAccount(BaseModelWithConfig): homePage: IRI name: NonEmptyStrictStr - -from typing import Annotated -from pydantic import Field - class BaseXapiAgentCommonProperties(BaseModelWithConfig, ABC): """Pydantic model for core `Agent` type property. @@ -48,7 +42,7 @@ class BaseXapiAgentCommonProperties(BaseModelWithConfig, ABC): """ objectType: Optional[Literal["Agent"]] - name: Optional[NonEmptyStrictStr] + name: Optional[NonEmptyStr] class BaseXapiAgentWithMbox(BaseXapiAgentCommonProperties, BaseXapiMboxIFI): diff --git a/src/ralph/models/xapi/base/contexts.py b/src/ralph/models/xapi/base/contexts.py index febd78754..a552cc924 100644 --- a/src/ralph/models/xapi/base/contexts.py +++ b/src/ralph/models/xapi/base/contexts.py @@ -3,14 +3,13 @@ from typing import Dict, List, Optional, Union from uuid import UUID -from pydantic import StrictStr - from ..config import BaseModelWithConfig from .agents import BaseXapiAgent from .common import IRI, LanguageTag from .groups import BaseXapiGroup from .unnested_objects import BaseXapiActivity, BaseXapiStatementRef +from ralph.conf import NonEmptyStrictStr class BaseXapiContextContextActivities(BaseModelWithConfig): """Pydantic model for context `contextActivities` property. @@ -50,8 +49,8 @@ class BaseXapiContext(BaseModelWithConfig): instructor: Optional[BaseXapiAgent] team: Optional[BaseXapiGroup] contextActivities: Optional[BaseXapiContextContextActivities] - revision: Optional[StrictStr] - platform: Optional[StrictStr] + revision: Optional[NonEmptyStrictStr] + platform: Optional[NonEmptyStrictStr] language: Optional[LanguageTag] statement: Optional[BaseXapiStatementRef] extensions: Optional[Dict[IRI, Union[str, int, bool, list, dict, None]]] diff --git a/src/ralph/models/xapi/base/groups.py b/src/ralph/models/xapi/base/groups.py index 73c320058..563dc802f 100644 --- a/src/ralph/models/xapi/base/groups.py +++ b/src/ralph/models/xapi/base/groups.py @@ -4,8 +4,6 @@ from abc import ABC from typing import List, Optional, Union -from pydantic import StrictStr - from ..config import BaseModelWithConfig from .agents import BaseXapiAgent from .ifi import ( @@ -20,6 +18,7 @@ else: from typing_extensions import Literal +from ralph.conf import NonEmptyStrictStr class BaseXapiGroupCommonProperties(BaseModelWithConfig, ABC): """Pydantic model for core `Group` type property. @@ -32,7 +31,7 @@ class BaseXapiGroupCommonProperties(BaseModelWithConfig, ABC): """ objectType: Literal["Group"] - name: Optional[StrictStr] + name: Optional[NonEmptyStrictStr] class BaseXapiAnonymousGroup(BaseXapiGroupCommonProperties): diff --git a/src/ralph/models/xapi/base/ifi.py b/src/ralph/models/xapi/base/ifi.py index 5e03331ac..b940b41ef 100644 --- a/src/ralph/models/xapi/base/ifi.py +++ b/src/ralph/models/xapi/base/ifi.py @@ -1,6 +1,6 @@ """Base xAPI `Inverse Functional Identifier` definitions.""" -from pydantic import AnyUrl, StrictStr, constr +from pydantic import AnyUrl, constr from ..config import BaseModelWithConfig from .common import IRI, MailtoEmail @@ -19,8 +19,6 @@ class BaseXapiAccount(BaseModelWithConfig): homePage: IRI name: NonEmptyStrictStr -from typing import Annotated -from pydantic import Field class BaseXapiMboxIFI(BaseModelWithConfig): """Pydantic model for mailto Inverse Functional Identifier. diff --git a/src/ralph/models/xapi/base/results.py b/src/ralph/models/xapi/base/results.py index 905cf4b10..60356b729 100644 --- a/src/ralph/models/xapi/base/results.py +++ b/src/ralph/models/xapi/base/results.py @@ -4,7 +4,7 @@ from decimal import Decimal from typing import Any, Dict, Optional, Union -from pydantic import StrictBool, StrictStr, conint, root_validator +from pydantic import StrictBool, conint, root_validator from ..config import BaseModelWithConfig from .common import IRI diff --git a/src/ralph/models/xapi/virtual_classroom/results.py b/src/ralph/models/xapi/virtual_classroom/results.py index 6cb42ca67..898fd7b3f 100644 --- a/src/ralph/models/xapi/virtual_classroom/results.py +++ b/src/ralph/models/xapi/virtual_classroom/results.py @@ -1,7 +1,5 @@ """Virtual classroom xAPI events result fields definitions.""" -from pydantic import StrictStr - from ..base.results import BaseXapiResult from ralph.conf import NonEmptyStrictStr diff --git a/src/ralph/models/xapi/virtual_classroom/statements.py b/src/ralph/models/xapi/virtual_classroom/statements.py index 7494b661b..d23e9bb40 100644 --- a/src/ralph/models/xapi/virtual_classroom/statements.py +++ b/src/ralph/models/xapi/virtual_classroom/statements.py @@ -371,7 +371,6 @@ class VirtualClassroomAnsweredPoll(BaseVirtualClassroomStatement): object (dict): See CMIInteractionActivity. context (dict): See VirtualClassroomAnsweredPollContext. result (dict): See AnsweredPollResult. - result (dict): See AnsweredPollResult. timestamp (datetime): Consists of the timestamp of when the event occurred. result (dict): See AnsweredPollResult. timestamp (datetime): Consists of the timestamp of when the event occurred. diff --git a/tests/fixtures/hypothesis_configuration.py b/tests/fixtures/hypothesis_configuration.py index f7c7844b0..b2dfbb521 100644 --- a/tests/fixtures/hypothesis_configuration.py +++ b/tests/fixtures/hypothesis_configuration.py @@ -11,6 +11,10 @@ settings.register_profile("development", max_examples=1) settings.load_profile("development") +# from ralph.conf import NonEmptyStr, NonEmptyStrictStr +# st.register_type_strategy(NonEmptyStr, st.text(min_size=1)) +# st.register_type_strategy(NonEmptyStrictStr, st.text(min_size=1)) + st.register_type_strategy(str, st.text(min_size=1)) st.register_type_strategy(StrictStr, st.text(min_size=1)) st.register_type_strategy(AnyUrl, provisional.urls()) diff --git a/tests/models/xapi/base/test_statements.py b/tests/models/xapi/base/test_statements.py index 690beba1d..e2261e814 100644 --- a/tests/models/xapi/base/test_statements.py +++ b/tests/models/xapi/base/test_statements.py @@ -29,7 +29,7 @@ from polyfactory.factories.pydantic_factory import ModelFactory from polyfactory import Use -from ralph.models.xapi.base.common import IRI +from ralph.models.xapi.base.common import MailtoEmail, IRI from ralph.models.xapi.base.agents import BaseXapiAgentWithMbox from typing import Dict, Type, Any @@ -42,9 +42,7 @@ from ralph.models.xapi.base.common import LanguageTag -class BaseXapiStatementFactory(ModelFactory[BaseXapiStatement]): - __model__ = BaseXapiStatement - +class FactoryMixin(): @classmethod def get_provider_map(cls) -> Dict[Type, Any]: providers_map = super().get_provider_map() @@ -53,8 +51,8 @@ def get_provider_map(cls) -> Dict[Type, Any]: BaseXapiAgentWithMbox: lambda: BaseXapiAgentWithMbox(mbox="mailto:test@toast.com"), UUID: lambda: UUID(uuid4()), LanguageTag: lambda: LanguageTag("en-US"), + MailtoEmail: lambda: MailtoEmail("mailto:test@example.xyz"), **providers_map, - } @classmethod @@ -64,6 +62,12 @@ def _get_or_create_factory(cls, model: type): created_factory._get_or_create_factory = cls._get_or_create_factory return created_factory +class BaseXapiStatementFactory(FactoryMixin, ModelFactory[BaseXapiStatement]): + __model__ = BaseXapiStatement + + +print('jiglypuf') +print(BaseXapiStatementFactory.get_provider_map()[IRI]) @pytest.mark.parametrize( "path", From cf98c9179b6bc0f8e717b0af0a66324a60746047 Mon Sep 17 00:00:00 2001 From: lleeoo Date: Fri, 1 Dec 2023 22:01:39 +0100 Subject: [PATCH 04/19] wi --- src/ralph/conf.py | 6 +- src/ralph/models/xapi/base/agents.py | 4 +- src/ralph/models/xapi/base/common.py | 6 +- src/ralph/models/xapi/base/contexts.py | 1 - src/ralph/models/xapi/base/results.py | 8 +- src/ralph/models/xapi/base/statements.py | 16 +- .../models/xapi/base/unnested_objects.py | 13 +- tests/fixtures/hypothesis_strategies.py | 10 +- tests/models/xapi/base/test_statements.py | 281 ++++++++++++++---- 9 files changed, 258 insertions(+), 87 deletions(-) diff --git a/src/ralph/conf.py b/src/ralph/conf.py index 9bc061757..028028870 100644 --- a/src/ralph/conf.py +++ b/src/ralph/conf.py @@ -29,8 +29,10 @@ MODEL_PATH_SEPARATOR = "__" -NonEmptyStr = Annotated[str, Field(min_length=1)] -NonEmptyStrictStr = Annotated[StrictStr, Field(min_length=1)] +from pydantic import constr + +NonEmptyStr = constr(min_length=1) +NonEmptyStrictStr = constr(min_length=1, strict=True) class BaseSettingsConfig: """Pydantic model for BaseSettings Configuration.""" diff --git a/src/ralph/models/xapi/base/agents.py b/src/ralph/models/xapi/base/agents.py index 1be73f8b3..593602260 100644 --- a/src/ralph/models/xapi/base/agents.py +++ b/src/ralph/models/xapi/base/agents.py @@ -36,13 +36,13 @@ class BaseXapiAgentCommonProperties(BaseModelWithConfig, ABC): It defines who performed the action. - Attributes: + Attributes:name: objectType (str): Consists of the value `Agent`. name (str): Consists of the full name of the Agent. """ objectType: Optional[Literal["Agent"]] - name: Optional[NonEmptyStr] + name: Optional[NonEmptyStrictStr ] class BaseXapiAgentWithMbox(BaseXapiAgentCommonProperties, BaseXapiMboxIFI): diff --git a/src/ralph/models/xapi/base/common.py b/src/ralph/models/xapi/base/common.py index 26132c4fc..05825c0a2 100644 --- a/src/ralph/models/xapi/base/common.py +++ b/src/ralph/models/xapi/base/common.py @@ -6,7 +6,7 @@ from pydantic import StrictStr, validate_email from rfc3987 import parse - +from ralph.conf import NonEmptyStrictStr class IRI(str): """Pydantic custom data type validating RFC 3987 IRIs.""" @@ -33,12 +33,10 @@ def validate(tag: str) -> Type["LanguageTag"]: yield validate -from typing import Annotated -from pydantic import Field -from ralph.conf import NonEmptyStrictStr LanguageMap = Dict[LanguageTag, NonEmptyStrictStr] + # pattern = r'mailto:\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b' # MailtoEmail = Field(regex=pattern)#MailtoEmail diff --git a/src/ralph/models/xapi/base/contexts.py b/src/ralph/models/xapi/base/contexts.py index a552cc924..81c4a8460 100644 --- a/src/ralph/models/xapi/base/contexts.py +++ b/src/ralph/models/xapi/base/contexts.py @@ -29,7 +29,6 @@ class BaseXapiContextContextActivities(BaseModelWithConfig): category: Optional[Union[BaseXapiActivity, List[BaseXapiActivity]]] other: Optional[Union[BaseXapiActivity, List[BaseXapiActivity]]] - class BaseXapiContext(BaseModelWithConfig): """Pydantic model for `context` property. diff --git a/src/ralph/models/xapi/base/results.py b/src/ralph/models/xapi/base/results.py index 60356b729..725fcc44a 100644 --- a/src/ralph/models/xapi/base/results.py +++ b/src/ralph/models/xapi/base/results.py @@ -27,7 +27,7 @@ class BaseXapiResultScore(BaseModelWithConfig): min: Optional[Decimal] max: Optional[Decimal] - @root_validator + @root_validator # TODO: check if adding pre is still valid @classmethod def check_raw_min_max_relation(cls, values: Any) -> Any: """Check the relationship `min < raw < max`.""" @@ -36,18 +36,20 @@ def check_raw_min_max_relation(cls, values: Any) -> Any: max_value = values.get("max", None) if min_value: + print("max value is", max_value) + print("min value is", min_value) if max_value and min_value > max_value: raise ValueError("min cannot be greater than max") if raw_value and min_value > raw_value: raise ValueError("min cannot be greater than raw") if max_value: + print("raw value is", raw_value) if raw_value and raw_value > max_value: raise ValueError("raw cannot be greater than max") return values + -from pydantic import Field -from typing import Annotated class BaseXapiResult(BaseModelWithConfig): """Pydantic model for `result` property. diff --git a/src/ralph/models/xapi/base/statements.py b/src/ralph/models/xapi/base/statements.py index a86b31134..36dc1278d 100644 --- a/src/ralph/models/xapi/base/statements.py +++ b/src/ralph/models/xapi/base/statements.py @@ -16,6 +16,8 @@ from .verbs import BaseXapiVerb +from pprint import pprint # TODO: remove + class BaseXapiStatement(BaseModelWithConfig): """Pydantic model for base xAPI statements. @@ -55,8 +57,10 @@ def check_absence_of_empty_and_invalid_values(cls, values: Any) -> Any: """ for field, value in list(values.items()): if value in [None, "", {}]: + print("field is:", field, "value is:", value) raise ValueError(f"{field}: invalid empty value") if isinstance(value, dict) and field != "extensions": + print("nested field is:", field) cls.check_absence_of_empty_and_invalid_values(value) context = dict(values.get("context", {})) @@ -65,8 +69,16 @@ def check_absence_of_empty_and_invalid_values(cls, values: Any) -> Any: revision = context.get("revision", {}) object_type = dict(values["object"]).get("objectType", "Activity") if (platform or revision) and object_type != "Activity": + pprint('gigoglin') + pprint(context) + print(">>> context platform:") + pprint(context.get("platform")) + print(">>> context revision:") + pprint(context.get("revision")) + print("///object:///") + pprint(values["object"]) raise ValueError( - "revision and platform properties can only be used if the " - "Statement's Object is an Activity" + "context revision and platform properties can only be used" + " if the Statement's Object is an Activity" ) return values diff --git a/src/ralph/models/xapi/base/unnested_objects.py b/src/ralph/models/xapi/base/unnested_objects.py index f1113ab5a..296cfdfa7 100644 --- a/src/ralph/models/xapi/base/unnested_objects.py +++ b/src/ralph/models/xapi/base/unnested_objects.py @@ -1,10 +1,10 @@ """Base xAPI `Object` definitions (1).""" import sys -from typing import Any, Dict, List, Optional, Union +from typing import Annotated, Any, Dict, List, Optional, Union from uuid import UUID -from pydantic import AnyUrl, StrictStr, constr, validator +from pydantic import AnyUrl, Field, StrictStr, constr, validator from ..config import BaseModelWithConfig from .common import IRI, LanguageMap @@ -100,11 +100,10 @@ class BaseXapiActivity(BaseModelWithConfig): id: IRI objectType: Optional[Literal["Activity"]] definition: Optional[ - Union[ - BaseXapiActivityDefinition, - BaseXapiActivityInteractionDefinition, - ] - ] + Union[ + BaseXapiActivityDefinition, + BaseXapiActivityInteractionDefinition, + ]] class BaseXapiStatementRef(BaseModelWithConfig): diff --git a/tests/fixtures/hypothesis_strategies.py b/tests/fixtures/hypothesis_strategies.py index 9cde0b3ee..a2eddcf25 100644 --- a/tests/fixtures/hypothesis_strategies.py +++ b/tests/fixtures/hypothesis_strategies.py @@ -122,11 +122,11 @@ def custom_given(*args: Union[st.SearchStrategy, BaseModel], **kwargs): "revision": False, "platform": False, }, - BaseXapiResultScore: { - "raw": False, - "min": False, - "max": False, - }, + # BaseXapiResultScore: { + # "raw": False, + # "min": False, + # "max": False, + # }, LMSContextContextActivities: {"category": custom_builds(LMSProfileActivity)}, VideoContextContextActivities: {"category": custom_builds(VideoProfileActivity)}, VirtualClassroomContextContextActivities: { diff --git a/tests/models/xapi/base/test_statements.py b/tests/models/xapi/base/test_statements.py index e2261e814..d57efb53b 100644 --- a/tests/models/xapi/base/test_statements.py +++ b/tests/models/xapi/base/test_statements.py @@ -29,6 +29,7 @@ from polyfactory.factories.pydantic_factory import ModelFactory from polyfactory import Use +from polyfactory.fields import Ignore, Require from ralph.models.xapi.base.common import MailtoEmail, IRI from ralph.models.xapi.base.agents import BaseXapiAgentWithMbox from typing import Dict, Type, Any @@ -36,14 +37,72 @@ from uuid import UUID, uuid4 +from pprint import pprint + # class IRIFactory(ModelFactory[IRI]): # __model__ = IRI # name = Use(ModelFactory.__random__.choice, ["Roxy", "Spammy", "Moshe"]) -from ralph.models.xapi.base.common import LanguageTag +from ralph.models.xapi.base.contexts import BaseXapiContext +from ralph.models.xapi.base.results import BaseXapiResultScore +from ralph.models.xapi.base.common import LanguageTag, LanguageMap + +from decimal import Decimal + +# from typing import Generic, TypeVar +# from pydantic import BaseModel +# T = TypeVar("T", bound=BaseModel) +# class ModelFactory(Generic[T], ModelFactory): + +# __is_base_factory__ = True + +# @classmethod +# def process_kwargs(cls, **kwargs: Any) -> dict[str, Any]: +# values = super().process_kwargs(**kwargs) +# return cls._prune(values) + +# @classmethod +# def _prune(cls, values: dict): +# for key, item in values.items(): +# if item: +# if isinstance(item, dict): +# item = cls._prune(item) +# return item +# del item +# return values + + +def prune(d, exceptions:list=[]): + if isinstance(d, dict): + return {k:prune(v) for k,v in d.items() if prune(v) or (k in exceptions)} # TODO: Not ideal as pruning is applyied to exception + elif isinstance(d, list): + return [prune(v) for v in d if prune(v)] + if d: + return d + return False + +import inspect +from ralph.models import xapi + +def get_subclasses_of(klass): + for name in dir(xapi): + obj = getattr(xapi, name) + print("\n\n") + print(obj) + print(type(obj)) + if inspect.isclass(obj): + print("yolo") + if issubclass(klass, obj): + print(obj) + +get_subclasses_of(BaseXapiResultScore) + +assert False class FactoryMixin(): - @classmethod + __allow_none_optionals__ = False + + @classmethod def get_provider_map(cls) -> Dict[Type, Any]: providers_map = super().get_provider_map() return { @@ -51,6 +110,9 @@ def get_provider_map(cls) -> Dict[Type, Any]: BaseXapiAgentWithMbox: lambda: BaseXapiAgentWithMbox(mbox="mailto:test@toast.com"), UUID: lambda: UUID(uuid4()), LanguageTag: lambda: LanguageTag("en-US"), + LanguageMap: lambda: {LanguageTag("en-US"): "testval"}, # unsure why this is needed + BaseXapiResultScore: lambda: BaseXapiResultScore(min=Decimal("0.0"), max=Decimal("200.0"), raw=Decimal("132.1")), + # BaseXapiResultScore: lambda: BaseXapiResultScore(min=0, max=200, raw=130), MailtoEmail: lambda: MailtoEmail("mailto:test@example.xyz"), **providers_map, } @@ -62,19 +124,93 @@ def _get_or_create_factory(cls, model: type): created_factory._get_or_create_factory = cls._get_or_create_factory return created_factory + + # actor = Require() + # verb = Require() + # object = Require() # actor = Require() + # verb = Require() + # object = Require() + + +# TODO: create class for mailto emails + +from polyfactory.decorators import post_generated +from ralph.models.xapi.base.contexts import BaseXapiContextContextActivities + +def gen_statement(*args, **kwargs): + # Custom logic necessary as post-processing is not possible in polyfactory (when can generate empty dicts when all fields are optional) + return BaseXapiStatement(**prune(BaseXapiStatementFactory.process_kwargs(*args, **kwargs), exceptions=["extensions"])) + +def gen_base_xapi_context(*args, **kwargs): + return BaseXapiContext(**prune(BaseXapiContextFactory.process_kwargs(*args, **kwargs))) + +def gen_base_xapi_activity(*args, **kwargs): + return BaseXapiActivity(**prune(BaseXapiActivityFactory.process_kwargs(*args, **kwargs))) + + +class BaseXapiContextContextActivitiesFactory(FactoryMixin, ModelFactory[BaseXapiContextContextActivities]): + __model__ = BaseXapiContextContextActivities + +class BaseXapiContextFactory(FactoryMixin, ModelFactory[BaseXapiContext]): + __model__ = BaseXapiContext + + revision = Ignore() + platform = Ignore() + + contextActivities = lambda: BaseXapiContextContextActivitiesFactory.build() or Ignore() + class BaseXapiStatementFactory(FactoryMixin, ModelFactory[BaseXapiStatement]): - __model__ = BaseXapiStatement + __model__ = BaseXapiStatement# Don't generate None for Optional fields + + context = lambda: gen_base_xapi_context() # TODO: Remove ? (here because of after validation with revision platform are "properties can only be used if ") + result = Ignore() + + # use post generated for mails (and other custom validators) + +# print("jololo") +# pprint(gen_statement()) +# print("jalala") +# assert False + +print("jornimo") +pprint(BaseXapiStatementFactory.get_provider_map()) +for x in range(100): + a = BaseXapiStatementFactory.process_kwargs() + + + + + +class BaseXapiActivityFactory(FactoryMixin, ModelFactory[BaseXapiActivity]): + __model__ = BaseXapiActivity + + +class BaseXapiActivityInteractionDefinitionFactory(FactoryMixin, ModelFactory[BaseXapiActivityInteractionDefinition]): + __model__ = BaseXapiActivityInteractionDefinition + + correctResponsesPattern = None + +class BaseXapiAgentWithAccountFactory(FactoryMixin, ModelFactory[BaseXapiAgentWithAccount]): + __model__ = BaseXapiAgentWithAccount + + +class BaseXapiAnonymousGroupFactory(FactoryMixin, ModelFactory[BaseXapiAnonymousGroup]): + __model__ = BaseXapiAnonymousGroup + + +class BaseXapiIdentifiedGroupWithAccountFactory(FactoryMixin, ModelFactory[BaseXapiIdentifiedGroupWithAccount]): + __model__ = BaseXapiIdentifiedGroupWithAccount + +class BaseXapiSubStatementFactory(FactoryMixin, ModelFactory[BaseXapiSubStatement]): + __model__ = BaseXapiSubStatement -print('jiglypuf') -print(BaseXapiStatementFactory.get_provider_map()[IRI]) @pytest.mark.parametrize( "path", - ["id", "stored", "verb__display", "context__contextActivities__parent"], + ["id", "stored", "verb__display", "result__score__raw"], ) @pytest.mark.parametrize("value", [None, "", {}]) -#@custom_given(BaseXapiStatement) def test_models_xapi_base_statement_with_invalid_null_values(path, value): """Test that the statement does not accept any null values. @@ -83,12 +219,11 @@ def test_models_xapi_base_statement_with_invalid_null_values(path, value): value is set to "null", an empty object, or has no value, except in an "extensions" property. """ - # klass = BaseXapiStatement - # factory = ModelFactory.create_factory(model=klass) - statement = BaseXapiStatementFactory.build() + statement = gen_statement() statement = statement.dict(exclude_none=True) set_dict_value_from_path(statement, path.split("__"), value) + with pytest.raises(ValidationError, match="invalid empty value"): BaseXapiStatement(**statement) @@ -102,8 +237,7 @@ def test_models_xapi_base_statement_with_invalid_null_values(path, value): ], ) @pytest.mark.parametrize("value", [None, "", {}]) -@custom_given(custom_builds(BaseXapiStatement, object=custom_builds(BaseXapiActivity))) -def test_models_xapi_base_statement_with_valid_null_values(path, value, statement): +def test_models_xapi_base_statement_with_valid_null_values(path, value): """Test that the statement does accept valid null values in extensions fields. XAPI-00001 @@ -111,8 +245,14 @@ def test_models_xapi_base_statement_with_valid_null_values(path, value, statemen value is set to "null", an empty object, or has no value, except in an "extensions" property. """ + + # statement = gen_statement() + statement = gen_statement(object=gen_base_xapi_activity()) + statement = statement.dict(exclude_none=True) set_dict_value_from_path(statement, path.split("__"), value) + print("jogogo") + pprint(statement) try: BaseXapiStatement(**statement) except ValidationError as err: @@ -120,21 +260,15 @@ def test_models_xapi_base_statement_with_valid_null_values(path, value, statemen @pytest.mark.parametrize("path", ["object__definition__correctResponsesPattern"]) -@custom_given( - custom_builds( - BaseXapiStatement, - object=custom_builds( - BaseXapiActivity, - definition=custom_builds(BaseXapiActivityInteractionDefinition), - ), - ) -) -def test_models_xapi_base_statement_with_valid_empty_array(path, statement): +def test_models_xapi_base_statement_with_valid_empty_array(path): """Test that the statement does accept a valid empty array. Where the Correct Responses Pattern contains an empty array, the meaning of this is that there is no correct answer. """ + + statement = gen_statement(object=BaseXapiActivityFactory.build(definition=gen_base_xapi_activity())) + statement = statement.dict(exclude_none=True) set_dict_value_from_path(statement, path.split("__"), []) try: @@ -143,12 +277,13 @@ def test_models_xapi_base_statement_with_valid_empty_array(path, statement): pytest.fail(f"Valid statement should not raise exceptions: {err}") +from polyfactory.field_meta import Null + @pytest.mark.parametrize( "field", ["actor", "verb", "object"], ) -@custom_given(BaseXapiStatement) -def test_models_xapi_base_statement_must_use_actor_verb_and_object(field, statement): +def test_models_xapi_base_statement_must_use_actor_verb_and_object(field): """Test that the statement raises an exception if required fields are missing. XAPI-00003 @@ -161,8 +296,14 @@ def test_models_xapi_base_statement_must_use_actor_verb_and_object(field, statem An LRS rejects with error code 400 Bad Request a Statement which does not contain an "object" property. """ + + statement = gen_statement() + statement = statement.dict(exclude_none=True) + del statement["context"] # Necessary as context leads to another validation error del statement[field] + print("youloulou") + pprint(statement) with pytest.raises(ValidationError, match="field required"): BaseXapiStatement(**statement) @@ -179,18 +320,18 @@ def test_models_xapi_base_statement_must_use_actor_verb_and_object(field, statem ("object__id", ["foo"]), # Should be an IRI ], ) -@custom_given( - custom_builds(BaseXapiStatement, actor=custom_builds(BaseXapiAgentWithAccount)) -) -def test_models_xapi_base_statement_with_invalid_data_types(path, value, statement): +def test_models_xapi_base_statement_with_invalid_data_types(path, value): """Test that the statement does not accept values with wrong types. XAPI-00006 An LRS rejects with error code 400 Bad Request a Statement which uses the wrong data type. """ + statement = gen_statement(actor=BaseXapiAgentWithAccountFactory.build()) + statement = statement.dict(exclude_none=True) set_dict_value_from_path(statement, path.split("__"), value) + err = "(type expected|not a valid dict|expected string )" with pytest.raises(ValidationError, match=err): BaseXapiStatement(**statement) @@ -205,10 +346,7 @@ def test_models_xapi_base_statement_with_invalid_data_types(path, value, stateme ("object__id", ["This is not an IRI"]), # Should be an IRI ], ) -@custom_given( - custom_builds(BaseXapiStatement, actor=custom_builds(BaseXapiAgentWithAccount)) -) -def test_models_xapi_base_statement_with_invalid_data_format(path, value, statement): +def test_models_xapi_base_statement_with_invalid_data_format(path, value): """Test that the statement does not accept values having a wrong format. XAPI-00007 @@ -217,6 +355,9 @@ def test_models_xapi_base_statement_with_invalid_data_format(path, value, statem particular format (such as mailto IRI, UUID, or IRI) is required. (Empty strings are covered by XAPI-00001) """ + statement = gen_statement(actor=BaseXapiAgentWithAccountFactory.build()) + + statement = statement.dict(exclude_none=True) set_dict_value_from_path(statement, path.split("__"), value) err = "(Invalid `mailto:email`|Invalid RFC 5646 Language tag|not a valid uuid)" @@ -225,16 +366,18 @@ def test_models_xapi_base_statement_with_invalid_data_format(path, value, statem @pytest.mark.parametrize("path,value", [("actor__objecttype", "Agent")]) -@custom_given( - custom_builds(BaseXapiStatement, actor=custom_builds(BaseXapiAgentWithAccount)) -) -def test_models_xapi_base_statement_with_invalid_letter_cases(path, value, statement): +# @custom_given( +# custom_builds(BaseXapiStatement, actor=custom_builds(BaseXapiAgentWithAccount)) +# ) +def test_models_xapi_base_statement_with_invalid_letter_cases(path, value): """Test that the statement does not accept keys having invalid letter cases. XAPI-00008 An LRS rejects with error code 400 Bad Request a Statement where the case of a key does not match the case specified in this specification. """ + statement = gen_statement(actor=BaseXapiAgentWithAccountFactory.build()) + statement = statement.dict(exclude_none=True) if statement["actor"].get("objectType", None): del statement["actor"]["objectType"] @@ -244,14 +387,15 @@ def test_models_xapi_base_statement_with_invalid_letter_cases(path, value, state BaseXapiStatement(**statement) -@custom_given(BaseXapiStatement) -def test_models_xapi_base_statement_should_not_accept_additional_properties(statement): +def test_models_xapi_base_statement_should_not_accept_additional_properties(): """Test that the statement does not accept additional properties. XAPI-00010 An LRS rejects with error code 400 Bad Request a Statement where a key or value is not allowed by this specification. """ + statement = gen_statement() + invalid_statement = statement.dict(exclude_none=True) invalid_statement["NEW_INVALID_FIELD"] = "some value" with pytest.raises(ValidationError, match="extra fields not permitted"): @@ -259,16 +403,20 @@ def test_models_xapi_base_statement_should_not_accept_additional_properties(stat @pytest.mark.parametrize("path,value", [("object__id", "w3id.org/xapi/video")]) -@custom_given(BaseXapiStatement) -def test_models_xapi_base_statement_with_iri_without_scheme(path, value, statement): +# @custom_given(BaseXapiStatement) +def test_models_xapi_base_statement_with_iri_without_scheme(path, value): """Test that the statement does not accept IRIs without a scheme. XAPI-00011 An LRS rejects with error code 400 Bad Request a Statement containing IRL or IRI values without a scheme. """ + statement = gen_statement() + statement = statement.dict(exclude_none=True) set_dict_value_from_path(statement, path.split("__"), value) + print("jaklin") + pprint(statement) with pytest.raises(ValidationError, match="is not a valid 'IRI'"): BaseXapiStatement(**statement) @@ -281,43 +429,50 @@ def test_models_xapi_base_statement_with_iri_without_scheme(path, value, stateme "context__extensions__w3id.org/xapi/video", ], ) -@custom_given(custom_builds(BaseXapiStatement, object=custom_builds(BaseXapiActivity))) -def test_models_xapi_base_statement_with_invalid_extensions(path, statement): +def test_models_xapi_base_statement_with_invalid_extensions(path): """Test that the statement does not accept extensions keys with invalid IRIs. XAPI-00118 An Extension "key" is an IRI. The LRS rejects with 400 a statement which has an extension key which is not a valid IRI, if an extension object is present. """ + statement = gen_statement(object=gen_base_xapi_activity()) + statement = statement.dict(exclude_none=True) set_dict_value_from_path(statement, path.split("__"), "") + print("joklin") + pprint(statement) with pytest.raises(ValidationError, match="is not a valid 'IRI'"): BaseXapiStatement(**statement) @pytest.mark.parametrize("path,value", [("actor__mbox", "mailto:example@mail.com")]) -@custom_given( - custom_builds(BaseXapiStatement, actor=custom_builds(BaseXapiAgentWithAccount)) -) -def test_models_xapi_base_statement_with_two_agent_types(path, value, statement): +# @custom_given( +# custom_builds(BaseXapiStatement, actor=custom_builds(BaseXapiAgentWithAccount)) +# ) +def test_models_xapi_base_statement_with_two_agent_types(path, value): """Test that the statement does not accept multiple agent types. An Agent MUST NOT include more than one Inverse Functional Identifier. """ + statement = gen_statement(actor=BaseXapiAgentWithAccountFactory.build()) + statement = statement.dict(exclude_none=True) set_dict_value_from_path(statement, path.split("__"), value) with pytest.raises(ValidationError, match="extra fields not permitted"): BaseXapiStatement(**statement) -@custom_given( - custom_builds(BaseXapiStatement, actor=custom_builds(BaseXapiAnonymousGroup)) -) -def test_models_xapi_base_statement_missing_member_property(statement): +# @custom_given( +# custom_builds(BaseXapiStatement, actor=custom_builds(BaseXapiAnonymousGroup)) +# ) +def test_models_xapi_base_statement_missing_member_property(): """Test that the statement does not accept group agents with missing members. An Anonymous Group MUST include a "member" property listing constituent Agents. """ + statement = gen_statement(actor=BaseXapiAnonymousGroupFactory.build()) + statement = statement.dict(exclude_none=True) del statement["actor"]["member"] with pytest.raises(ValidationError, match="member\n field required"): @@ -371,17 +526,19 @@ def test_models_xapi_base_statement_with_invalid_group_objects(value, statement, @pytest.mark.parametrize("path,value", [("actor__mbox", "mailto:example@mail.com")]) -@custom_given( - custom_builds( - BaseXapiStatement, - actor=custom_builds(BaseXapiIdentifiedGroupWithAccount), - ) -) -def test_models_xapi_base_statement_with_two_group_identifiers(path, value, statement): +# @custom_given( +# custom_builds( +# BaseXapiStatement, +# actor=custom_builds(BaseXapiIdentifiedGroupWithAccount), +# ) +# ) +def test_models_xapi_base_statement_with_two_group_identifiers(path, value): """Test that the statement does not accept multiple group identifiers. An Identified Group MUST include exactly one Inverse Functional Identifier. """ + statement = gen_statement(actor=BaseXapiIdentifiedGroupWithAccountFactory.build()) + statement = statement.dict(exclude_none=True) set_dict_value_from_path(statement, path.split("__"), value) with pytest.raises(ValidationError, match="extra fields not permitted"): @@ -397,15 +554,17 @@ def test_models_xapi_base_statement_with_two_group_identifiers(path, value, stat ("object__authority", {"mbox": "mailto:example@mail.com"}), ], ) -@custom_given( - custom_builds(BaseXapiStatement, object=custom_builds(BaseXapiSubStatement)) -) -def test_models_xapi_base_statement_with_sub_statement_ref(path, value, statement): +# @custom_given( +# custom_builds(BaseXapiStatement, object=custom_builds(BaseXapiSubStatement)) +# ) +def test_models_xapi_base_statement_with_sub_statement_ref(path, value): """Test that the sub-statement does not accept invalid properties. A SubStatement MUST NOT have the "id", "stored", "version" or "authority" properties. """ + statement = gen_statement(object=BaseXapiSubStatementFactory.build()) + statement = statement.dict(exclude_none=True) set_dict_value_from_path(statement, path.split("__"), value) with pytest.raises(ValidationError, match="extra fields not permitted"): From 22170c729f7a713fd7c10066fc2c37c7d0d9b0e1 Mon Sep 17 00:00:00 2001 From: lleeoo Date: Tue, 5 Dec 2023 18:52:54 +0100 Subject: [PATCH 05/19] wip --- src/ralph/models/xapi/base/common.py | 66 ++--- src/ralph/models/xapi/base/results.py | 3 - src/ralph/models/xapi/base/statements.py | 23 +- tests/fixtures/hypothesis_configuration.py | 6 +- tests/fixtures/hypothesis_strategies.py | 1 + tests/models/xapi/base/test_statements.py | 300 +++++++++------------ 6 files changed, 180 insertions(+), 219 deletions(-) diff --git a/src/ralph/models/xapi/base/common.py b/src/ralph/models/xapi/base/common.py index 05825c0a2..28b69e85b 100644 --- a/src/ralph/models/xapi/base/common.py +++ b/src/ralph/models/xapi/base/common.py @@ -6,50 +6,54 @@ from pydantic import StrictStr, validate_email from rfc3987 import parse -from ralph.conf import NonEmptyStrictStr -class IRI(str): +from ralph.conf import NonEmptyStr, NonEmptyStrictStr +class IRI(NonEmptyStrictStr): """Pydantic custom data type validating RFC 3987 IRIs.""" - @classmethod - def __get_validators__(cls) -> Generator: # noqa: D105 - def validate(iri: str) -> Type["IRI"]: - """Check whether the provided IRI is a valid RFC 3987 IRI.""" - parse(iri, rule="IRI") - return cls(iri) + # TODO: Put this back + # @classmethod + # def __get_validators__(cls) -> Generator: # noqa: D105 + # def validate(iri: str) -> Type["IRI"]: + # """Check whether the provided IRI is a valid RFC 3987 IRI.""" + # parse(iri, rule="IRI") + # return cls(iri) - yield validate + # yield validate -class LanguageTag(str): +class LanguageTag(NonEmptyStr): """Pydantic custom data type validating RFC 5646 Language tags.""" - @classmethod - def __get_validators__(cls) -> Generator: # noqa: D105 - def validate(tag: str) -> Type["LanguageTag"]: - """Check whether the provided tag is a valid RFC 5646 Language tag.""" - if not tag_is_valid(tag): - raise TypeError("Invalid RFC 5646 Language tag") - return cls(tag) + # TODO: Put this back + # @classmethod + # def __get_validators__(cls) -> Generator: # noqa: D105 + # def validate(tag: str) -> Type["LanguageTag"]: + # """Check whether the provided tag is a valid RFC 5646 Language tag.""" + # if not tag_is_valid(tag): + # raise TypeError("Invalid RFC 5646 Language tag") + # return cls(tag) - yield validate + # yield validate LanguageMap = Dict[LanguageTag, NonEmptyStrictStr] +from typing import Annotated +from pydantic import Field -# pattern = r'mailto:\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b' -# MailtoEmail = Field(regex=pattern)#MailtoEmail +email_pattern = r"(^mailto:[-!#-'*+/-9=?A-Z^-~]+(\.[-!#-'*+/-9=?A-Z^-~]+)*|\"([]!#-[^-~ \t]|(\\[\t -~]))+\")@([-!#-'*+/-9=?A-Z^-~]+(\.[-!#-'*+/-9=?A-Z^-~]+)*|\[[\t -Z^-~]*])" +MailtoEmail = Annotated[str, Field(regex=email_pattern)] -class MailtoEmail(str): - """Pydantic custom data type validating `mailto:email` format.""" +# class MailtoEmail(str): +# """Pydantic custom data type validating `mailto:email` format.""" - @classmethod - def __get_validators__(cls) -> Generator: # noqa: D105 - def validate(mailto: str) -> Type["MailtoEmail"]: - """Check whether the provided value follows the `mailto:email` format.""" - if not mailto.startswith("mailto:"): - raise TypeError("Invalid `mailto:email` value") - valid = validate_email(mailto[7:]) - return cls(f"mailto:{valid[1]}") +# @classmethod +# def __get_validators__(cls) -> Generator: # noqa: D105 +# def validate(mailto: str) -> Type["MailtoEmail"]: +# """Check whether the provided value follows the `mailto:email` format.""" +# if not mailto.startswith("mailto:"): +# raise TypeError(f"Invalid `mailto:email` value: {str(mailto)}") +# valid = validate_email(mailto[7:]) +# return cls(f"mailto:{valid[1]}") - yield validate +# yield validate diff --git a/src/ralph/models/xapi/base/results.py b/src/ralph/models/xapi/base/results.py index 725fcc44a..21c0f3282 100644 --- a/src/ralph/models/xapi/base/results.py +++ b/src/ralph/models/xapi/base/results.py @@ -36,14 +36,11 @@ def check_raw_min_max_relation(cls, values: Any) -> Any: max_value = values.get("max", None) if min_value: - print("max value is", max_value) - print("min value is", min_value) if max_value and min_value > max_value: raise ValueError("min cannot be greater than max") if raw_value and min_value > raw_value: raise ValueError("min cannot be greater than raw") if max_value: - print("raw value is", raw_value) if raw_value and raw_value > max_value: raise ValueError("raw cannot be greater than max") diff --git a/src/ralph/models/xapi/base/statements.py b/src/ralph/models/xapi/base/statements.py index 36dc1278d..d65e3cb01 100644 --- a/src/ralph/models/xapi/base/statements.py +++ b/src/ralph/models/xapi/base/statements.py @@ -52,16 +52,19 @@ class BaseXapiStatement(BaseModelWithConfig): def check_absence_of_empty_and_invalid_values(cls, values: Any) -> Any: """Check the model for empty and invalid values. + Check that there are no empty values except in branch of `extenstions`. + Check that the `context` field contains `platform` and `revision` fields only if the `object.objectType` property is equal to `Activity`. """ for field, value in list(values.items()): - if value in [None, "", {}]: - print("field is:", field, "value is:", value) - raise ValueError(f"{field}: invalid empty value") - if isinstance(value, dict) and field != "extensions": - print("nested field is:", field) - cls.check_absence_of_empty_and_invalid_values(value) + if field != "extensions": + if value in [None, "", {}]: + print("field is:", field, "value is:", value) + raise ValueError(f"{field}: invalid empty value") + if isinstance(value, dict): + print("nested field is:", field) + cls.check_absence_of_empty_and_invalid_values(value) context = dict(values.get("context", {})) if context: @@ -69,14 +72,6 @@ def check_absence_of_empty_and_invalid_values(cls, values: Any) -> Any: revision = context.get("revision", {}) object_type = dict(values["object"]).get("objectType", "Activity") if (platform or revision) and object_type != "Activity": - pprint('gigoglin') - pprint(context) - print(">>> context platform:") - pprint(context.get("platform")) - print(">>> context revision:") - pprint(context.get("revision")) - print("///object:///") - pprint(values["object"]) raise ValueError( "context revision and platform properties can only be used" " if the Statement's Object is an Activity" diff --git a/tests/fixtures/hypothesis_configuration.py b/tests/fixtures/hypothesis_configuration.py index b2dfbb521..1924be0e3 100644 --- a/tests/fixtures/hypothesis_configuration.py +++ b/tests/fixtures/hypothesis_configuration.py @@ -20,7 +20,7 @@ st.register_type_strategy(AnyUrl, provisional.urls()) st.register_type_strategy(AnyHttpUrl, provisional.urls()) st.register_type_strategy(IRI, provisional.urls()) -st.register_type_strategy( - MailtoEmail, st.builds(operator.add, st.just("mailto:"), st.emails()) -) +# st.register_type_strategy( +# MailtoEmail, st.builds(operator.add, st.just("mailto:"), st.emails()) +# ) st.register_type_strategy(LanguageTag, st.just("en-US")) diff --git a/tests/fixtures/hypothesis_strategies.py b/tests/fixtures/hypothesis_strategies.py index a2eddcf25..d2091299e 100644 --- a/tests/fixtures/hypothesis_strategies.py +++ b/tests/fixtures/hypothesis_strategies.py @@ -95,6 +95,7 @@ def custom_builds( return st.fixed_dictionaries(required, optional=optional).map(klass.parse_obj) + def custom_given(*args: Union[st.SearchStrategy, BaseModel], **kwargs): """Wrap the Hypothesis `given` function. Replace st.builds with custom_builds.""" strategies = [] diff --git a/tests/models/xapi/base/test_statements.py b/tests/models/xapi/base/test_statements.py index d57efb53b..35faaad54 100644 --- a/tests/models/xapi/base/test_statements.py +++ b/tests/models/xapi/base/test_statements.py @@ -46,33 +46,14 @@ from ralph.models.xapi.base.contexts import BaseXapiContext from ralph.models.xapi.base.results import BaseXapiResultScore from ralph.models.xapi.base.common import LanguageTag, LanguageMap +from ralph.models.xapi.base.contexts import BaseXapiContextContextActivities from decimal import Decimal -# from typing import Generic, TypeVar -# from pydantic import BaseModel -# T = TypeVar("T", bound=BaseModel) -# class ModelFactory(Generic[T], ModelFactory): - -# __is_base_factory__ = True - -# @classmethod -# def process_kwargs(cls, **kwargs: Any) -> dict[str, Any]: -# values = super().process_kwargs(**kwargs) -# return cls._prune(values) - -# @classmethod -# def _prune(cls, values: dict): -# for key, item in values.items(): -# if item: -# if isinstance(item, dict): -# item = cls._prune(item) -# return item -# del item -# return values - +from typing import Any -def prune(d, exceptions:list=[]): +def prune(d: Any, exceptions:list=[]): + """Remove all empty leaves from a dict, except fo those in `exceptions`.""" if isinstance(d, dict): return {k:prune(v) for k,v in d.items() if prune(v) or (k in exceptions)} # TODO: Not ideal as pruning is applyied to exception elif isinstance(d, list): @@ -81,128 +62,125 @@ def prune(d, exceptions:list=[]): return d return False -import inspect -from ralph.models import xapi -def get_subclasses_of(klass): - for name in dir(xapi): - obj = getattr(xapi, name) - print("\n\n") - print(obj) - print(type(obj)) - if inspect.isclass(obj): - print("yolo") - if issubclass(klass, obj): - print(obj) +# from pydantic import constr -get_subclasses_of(BaseXapiResultScore) +# Toto = constr(min_length=5, strict=True) +# class MyClass(Toto): +# pass -assert False +# test = MyClass("2") +# Toto("2") +# assert False class FactoryMixin(): __allow_none_optionals__ = False - @classmethod - def get_provider_map(cls) -> Dict[Type, Any]: - providers_map = super().get_provider_map() - return { - IRI: lambda: IRI("https://w3id.org/xapi/video/verbs/played"), - BaseXapiAgentWithMbox: lambda: BaseXapiAgentWithMbox(mbox="mailto:test@toast.com"), - UUID: lambda: UUID(uuid4()), - LanguageTag: lambda: LanguageTag("en-US"), - LanguageMap: lambda: {LanguageTag("en-US"): "testval"}, # unsure why this is needed - BaseXapiResultScore: lambda: BaseXapiResultScore(min=Decimal("0.0"), max=Decimal("200.0"), raw=Decimal("132.1")), - # BaseXapiResultScore: lambda: BaseXapiResultScore(min=0, max=200, raw=130), - MailtoEmail: lambda: MailtoEmail("mailto:test@example.xyz"), - **providers_map, - } - - @classmethod - def _get_or_create_factory(cls, model: type): - created_factory = super()._get_or_create_factory(model) - created_factory.get_provider_map = cls.get_provider_map - created_factory._get_or_create_factory = cls._get_or_create_factory - return created_factory - - - # actor = Require() - # verb = Require() - # object = Require() # actor = Require() - # verb = Require() - # object = Require() + # @classmethod + # def get_provider_map(cls) -> Dict[Type, Any]: + # providers_map = super().get_provider_map() + # return { + # # IRI: lambda: IRI("https://w3id.org/xapi/video/verbs/played"), + # # BaseXapiAgentWithMbox: lambda: BaseXapiAgentWithMbox(mbox="mailto:test@toast.com"), + # # UUID: lambda: UUID(str(uuid4())), + # LanguageTag: lambda: LanguageTag("en-US"), + # LanguageMap: lambda: {LanguageTag("en-US"): "testval"}, # unsure why this is needed + # #BaseXapiResultScore: lambda: BaseXapiResultScore(min=Decimal("0.0"), max=Decimal("20.0"), raw=Decimal("11")), + # # MailtoEmail: lambda: MailtoEmail("mailto:test@example.xyz"), + # **providers_map, + # } - -# TODO: create class for mailto emails - -from polyfactory.decorators import post_generated -from ralph.models.xapi.base.contexts import BaseXapiContextContextActivities - -def gen_statement(*args, **kwargs): - # Custom logic necessary as post-processing is not possible in polyfactory (when can generate empty dicts when all fields are optional) - return BaseXapiStatement(**prune(BaseXapiStatementFactory.process_kwargs(*args, **kwargs), exceptions=["extensions"])) - -def gen_base_xapi_context(*args, **kwargs): - return BaseXapiContext(**prune(BaseXapiContextFactory.process_kwargs(*args, **kwargs))) - -def gen_base_xapi_activity(*args, **kwargs): - return BaseXapiActivity(**prune(BaseXapiActivityFactory.process_kwargs(*args, **kwargs))) - - -class BaseXapiContextContextActivitiesFactory(FactoryMixin, ModelFactory[BaseXapiContextContextActivities]): - __model__ = BaseXapiContextContextActivities + # @classmethod + # def _get_or_create_factory(cls, model: type): + # created_factory = super()._get_or_create_factory(model) + # created_factory.get_provider_map = cls.get_provider_map + # created_factory._get_or_create_factory = cls._get_or_create_factory + # return created_factory + class BaseXapiContextFactory(FactoryMixin, ModelFactory[BaseXapiContext]): __model__ = BaseXapiContext + __set_as_default_factory_for_type__ = True revision = Ignore() platform = Ignore() contextActivities = lambda: BaseXapiContextContextActivitiesFactory.build() or Ignore() - -class BaseXapiStatementFactory(FactoryMixin, ModelFactory[BaseXapiStatement]): - __model__ = BaseXapiStatement# Don't generate None for Optional fields - context = lambda: gen_base_xapi_context() # TODO: Remove ? (here because of after validation with revision platform are "properties can only be used if ") - result = Ignore() +class BaseXapiResultScoreFactory(FactoryMixin, ModelFactory[BaseXapiResultScore]): + __set_as_default_factory_for_type__ = True + __model__ = BaseXapiResultScore - # use post generated for mails (and other custom validators) + min=Decimal("0.0") + max=Decimal("20.0") + raw=Decimal("11") -# print("jololo") -# pprint(gen_statement()) -# print("jalala") -# assert False +class LanguageTagFactory(FactoryMixin, ModelFactory[LanguageTag]): + __is_base_factory__ = True + __set_as_default_factory_for_type__ = True + __model__ = LanguageTag -print("jornimo") -pprint(BaseXapiStatementFactory.get_provider_map()) -for x in range(100): - a = BaseXapiStatementFactory.process_kwargs() - + __root__ = LanguageTag("en-US") + # @classmethod + # def get_provider_map(cls) -> Dict[Type, Any]: + # providers_map = super().get_provider_map() + # return { + # LanguageTag: lambda: LanguageTag("en-US"), + # LanguageMap: lambda: {LanguageTag("en-US"): "testval"}, # unsure why this is needed + # **providers_map, + # } +class BaseXapiActivityInteractionDefinitionFactory(FactoryMixin, ModelFactory[BaseXapiActivityInteractionDefinition]): + __is_base_factory__ = True + __model__ = BaseXapiActivityInteractionDefinition + + correctResponsesPattern = None + +class BaseXapiContextContextActivitiesFactory(FactoryMixin, ModelFactory[BaseXapiContextContextActivities]): + __model__ = BaseXapiContextContextActivities class BaseXapiActivityFactory(FactoryMixin, ModelFactory[BaseXapiActivity]): __model__ = BaseXapiActivity +# class BaseXapiAgentWithAccountFactory(FactoryMixin, ModelFactory[BaseXapiAgentWithAccount]): +# __model__ = BaseXapiAgentWithAccount -class BaseXapiActivityInteractionDefinitionFactory(FactoryMixin, ModelFactory[BaseXapiActivityInteractionDefinition]): - __model__ = BaseXapiActivityInteractionDefinition +# class BaseXapiAnonymousGroupFactory(FactoryMixin, ModelFactory[BaseXapiAnonymousGroup]): +# __model__ = BaseXapiAnonymousGroup - correctResponsesPattern = None +# class BaseXapiIdentifiedGroupWithAccountFactory(ModelFactory[BaseXapiIdentifiedGroupWithAccount]): +# __model__ = BaseXapiIdentifiedGroupWithAccount + +# class BaseXapiSubStatementFactory(FactoryMixin, ModelFactory[BaseXapiSubStatement]): +# __model__ = BaseXapiSubStatement + +class BaseXapiStatementFactory(FactoryMixin, ModelFactory[BaseXapiStatement]): + __model__ = BaseXapiStatement + #result = Ignore() + + #context = lambda: BaseXapiContextFactory.build() -class BaseXapiAgentWithAccountFactory(FactoryMixin, ModelFactory[BaseXapiAgentWithAccount]): - __model__ = BaseXapiAgentWithAccount -class BaseXapiAnonymousGroupFactory(FactoryMixin, ModelFactory[BaseXapiAnonymousGroup]): - __model__ = BaseXapiAnonymousGroup +# def gen_statement(*args, **kwargs): +# # Custom logic necessary as post-processing is not possible in polyfactory (when can generate empty dicts when all fields are optional) +# return BaseXapiStatement(**prune(BaseXapiStatementFactory.process_kwargs(*args, **kwargs), exceptions=["extensions"])) -class BaseXapiIdentifiedGroupWithAccountFactory(FactoryMixin, ModelFactory[BaseXapiIdentifiedGroupWithAccount]): - __model__ = BaseXapiIdentifiedGroupWithAccount +def mock_instance(klass, *args, **kwargs): + """Generate a mock instance of a given class (`klass`).""" -class BaseXapiSubStatementFactory(FactoryMixin, ModelFactory[BaseXapiSubStatement]): - __model__ = BaseXapiSubStatement + class KlassFactory(FactoryMixin, ModelFactory[klass]): + __model__ = klass + + return klass(**prune(KlassFactory.process_kwargs(*args, **kwargs))) + + +for x in range(100): + a = BaseXapiStatementFactory.process_kwargs() + @@ -219,7 +197,7 @@ def test_models_xapi_base_statement_with_invalid_null_values(path, value): value is set to "null", an empty object, or has no value, except in an "extensions" property. """ - statement = gen_statement() + statement = mock_instance(BaseXapiStatement) statement = statement.dict(exclude_none=True) set_dict_value_from_path(statement, path.split("__"), value) @@ -246,18 +224,18 @@ def test_models_xapi_base_statement_with_valid_null_values(path, value): property. """ - # statement = gen_statement() - statement = gen_statement(object=gen_base_xapi_activity()) + statement = mock_instance(BaseXapiStatement, object=mock_instance(BaseXapiActivity)) statement = statement.dict(exclude_none=True) set_dict_value_from_path(statement, path.split("__"), value) - print("jogogo") - pprint(statement) try: BaseXapiStatement(**statement) except ValidationError as err: + pprint(statement) pytest.fail(f"Valid statement should not raise exceptions: {err}") +# class BaseXapiActivityInteractionDefinitionFactory(ModelFactory[BaseXapiActivityInteractionDefinitionFactory]): +# __model__ = BaseXapiActivityInteractionDefinitionFactory @pytest.mark.parametrize("path", ["object__definition__correctResponsesPattern"]) def test_models_xapi_base_statement_with_valid_empty_array(path): @@ -267,7 +245,9 @@ def test_models_xapi_base_statement_with_valid_empty_array(path): that there is no correct answer. """ - statement = gen_statement(object=BaseXapiActivityFactory.build(definition=gen_base_xapi_activity())) + statement = mock_instance(BaseXapiStatement, + object=mock_instance(BaseXapiActivity, + definition=mock_instance(BaseXapiActivityInteractionDefinition))) statement = statement.dict(exclude_none=True) set_dict_value_from_path(statement, path.split("__"), []) @@ -277,8 +257,6 @@ def test_models_xapi_base_statement_with_valid_empty_array(path): pytest.fail(f"Valid statement should not raise exceptions: {err}") -from polyfactory.field_meta import Null - @pytest.mark.parametrize( "field", ["actor", "verb", "object"], @@ -297,13 +275,11 @@ def test_models_xapi_base_statement_must_use_actor_verb_and_object(field): "object" property. """ - statement = gen_statement() + statement = mock_instance(BaseXapiStatement) statement = statement.dict(exclude_none=True) del statement["context"] # Necessary as context leads to another validation error del statement[field] - print("youloulou") - pprint(statement) with pytest.raises(ValidationError, match="field required"): BaseXapiStatement(**statement) @@ -327,48 +303,47 @@ def test_models_xapi_base_statement_with_invalid_data_types(path, value): An LRS rejects with error code 400 Bad Request a Statement which uses the wrong data type. """ - statement = gen_statement(actor=BaseXapiAgentWithAccountFactory.build()) + statement = mock_instance(BaseXapiStatement, actor=mock_instance(BaseXapiAgentWithAccount)) statement = statement.dict(exclude_none=True) set_dict_value_from_path(statement, path.split("__"), value) err = "(type expected|not a valid dict|expected string )" + with pytest.raises(ValidationError, match=err): BaseXapiStatement(**statement) -@pytest.mark.parametrize( - "path,value", - [ - ("id", "0545fe73-1bbd-4f84-9c9a"), # Should be a valid UUID - ("actor", {"mbox": "example@mail.com"}), # Should be a Mailto IRI - ("verb__display", {"bad language tag": "foo"}), # Should be a valid LanguageTag - ("object__id", ["This is not an IRI"]), # Should be an IRI - ], -) -def test_models_xapi_base_statement_with_invalid_data_format(path, value): - """Test that the statement does not accept values having a wrong format. - - XAPI-00007 - An LRS rejects with error code 400 Bad Request a Statement which uses any - non-format-following key or value, including the empty string, where a string with a - particular format (such as mailto IRI, UUID, or IRI) is required. - (Empty strings are covered by XAPI-00001) - """ - statement = gen_statement(actor=BaseXapiAgentWithAccountFactory.build()) +# TODO: put this back +# @pytest.mark.parametrize( +# "path,value", +# [ +# ("id", "0545fe73-1bbd-4f84-9c9a"), # Should be a valid UUID +# ("actor", {"mbox": "example@mail.com"}), # Should be a Mailto IRI +# ("verb__display", {"bad language tag": "foo"}), # Should be a valid LanguageTag +# ("object__id", ["This is not an IRI"]), # Should be an IRI +# ], +# ) +# def test_models_xapi_base_statement_with_invalid_data_format(path, value): +# """Test that the statement does not accept values having a wrong format. +# XAPI-00007 +# An LRS rejects with error code 400 Bad Request a Statement which uses any +# non-format-following key or value, including the empty string, where a string with a +# particular format (such as mailto IRI, UUID, or IRI) is required. +# (Empty strings are covered by XAPI-00001) +# """ +# statement = mock_instance(BaseXapiStatement, actor=mock_instance(BaseXapiAgentWithAccount)) - statement = statement.dict(exclude_none=True) - set_dict_value_from_path(statement, path.split("__"), value) - err = "(Invalid `mailto:email`|Invalid RFC 5646 Language tag|not a valid uuid)" - with pytest.raises(ValidationError, match=err): - BaseXapiStatement(**statement) + +# statement = statement.dict(exclude_none=True) +# set_dict_value_from_path(statement, path.split("__"), value) +# err = "(string does not match regex|Invalid RFC 5646 Language tag|not a valid uuid)" +# with pytest.raises(ValidationError, match=err): +# BaseXapiStatement(**statement) @pytest.mark.parametrize("path,value", [("actor__objecttype", "Agent")]) -# @custom_given( -# custom_builds(BaseXapiStatement, actor=custom_builds(BaseXapiAgentWithAccount)) -# ) def test_models_xapi_base_statement_with_invalid_letter_cases(path, value): """Test that the statement does not accept keys having invalid letter cases. @@ -376,7 +351,7 @@ def test_models_xapi_base_statement_with_invalid_letter_cases(path, value): An LRS rejects with error code 400 Bad Request a Statement where the case of a key does not match the case specified in this specification. """ - statement = gen_statement(actor=BaseXapiAgentWithAccountFactory.build()) + statement = mock_instance(BaseXapiStatement, actor=mock_instance(BaseXapiAgentWithAccount)) statement = statement.dict(exclude_none=True) if statement["actor"].get("objectType", None): @@ -394,7 +369,7 @@ def test_models_xapi_base_statement_should_not_accept_additional_properties(): An LRS rejects with error code 400 Bad Request a Statement where a key or value is not allowed by this specification. """ - statement = gen_statement() + statement = mock_instance(BaseXapiStatement) invalid_statement = statement.dict(exclude_none=True) invalid_statement["NEW_INVALID_FIELD"] = "some value" @@ -403,7 +378,6 @@ def test_models_xapi_base_statement_should_not_accept_additional_properties(): @pytest.mark.parametrize("path,value", [("object__id", "w3id.org/xapi/video")]) -# @custom_given(BaseXapiStatement) def test_models_xapi_base_statement_with_iri_without_scheme(path, value): """Test that the statement does not accept IRIs without a scheme. @@ -411,12 +385,10 @@ def test_models_xapi_base_statement_with_iri_without_scheme(path, value): An LRS rejects with error code 400 Bad Request a Statement containing IRL or IRI values without a scheme. """ - statement = gen_statement() + statement = mock_instance(BaseXapiStatement) statement = statement.dict(exclude_none=True) set_dict_value_from_path(statement, path.split("__"), value) - print("jaklin") - pprint(statement) with pytest.raises(ValidationError, match="is not a valid 'IRI'"): BaseXapiStatement(**statement) @@ -436,26 +408,21 @@ def test_models_xapi_base_statement_with_invalid_extensions(path): An Extension "key" is an IRI. The LRS rejects with 400 a statement which has an extension key which is not a valid IRI, if an extension object is present. """ - statement = gen_statement(object=gen_base_xapi_activity()) + statement = mock_instance(BaseXapiStatement, object=mock_instance(BaseXapiActivity)) statement = statement.dict(exclude_none=True) set_dict_value_from_path(statement, path.split("__"), "") - print("joklin") - pprint(statement) with pytest.raises(ValidationError, match="is not a valid 'IRI'"): BaseXapiStatement(**statement) @pytest.mark.parametrize("path,value", [("actor__mbox", "mailto:example@mail.com")]) -# @custom_given( -# custom_builds(BaseXapiStatement, actor=custom_builds(BaseXapiAgentWithAccount)) -# ) def test_models_xapi_base_statement_with_two_agent_types(path, value): """Test that the statement does not accept multiple agent types. An Agent MUST NOT include more than one Inverse Functional Identifier. """ - statement = gen_statement(actor=BaseXapiAgentWithAccountFactory.build()) + statement = mock_instance(BaseXapiStatement, actor=mock_instance(BaseXapiAgentWithAccount)) statement = statement.dict(exclude_none=True) set_dict_value_from_path(statement, path.split("__"), value) @@ -463,15 +430,12 @@ def test_models_xapi_base_statement_with_two_agent_types(path, value): BaseXapiStatement(**statement) -# @custom_given( -# custom_builds(BaseXapiStatement, actor=custom_builds(BaseXapiAnonymousGroup)) -# ) def test_models_xapi_base_statement_missing_member_property(): """Test that the statement does not accept group agents with missing members. An Anonymous Group MUST include a "member" property listing constituent Agents. """ - statement = gen_statement(actor=BaseXapiAnonymousGroupFactory.build()) + statement = mock_instance(BaseXapiStatement, actor=mock_instance(BaseXapiAnonymousGroup)) statement = statement.dict(exclude_none=True) del statement["actor"]["member"] @@ -537,7 +501,7 @@ def test_models_xapi_base_statement_with_two_group_identifiers(path, value): An Identified Group MUST include exactly one Inverse Functional Identifier. """ - statement = gen_statement(actor=BaseXapiIdentifiedGroupWithAccountFactory.build()) + statement = mock_instance(BaseXapiStatement, actor=mock_instance(BaseXapiIdentifiedGroupWithAccount)) statement = statement.dict(exclude_none=True) set_dict_value_from_path(statement, path.split("__"), value) @@ -563,7 +527,7 @@ def test_models_xapi_base_statement_with_sub_statement_ref(path, value): A SubStatement MUST NOT have the "id", "stored", "version" or "authority" properties. """ - statement = gen_statement(object=BaseXapiSubStatementFactory.build()) + statement = mock_instance(BaseXapiStatement, object=mock_instance(BaseXapiSubStatement)) statement = statement.dict(exclude_none=True) set_dict_value_from_path(statement, path.split("__"), value) From a23d0114c9cb7f41fe276ad61ab4d308a621fe37 Mon Sep 17 00:00:00 2001 From: lleeoo Date: Wed, 6 Dec 2023 20:36:47 +0100 Subject: [PATCH 06/19] seems to work --- src/ralph/conf.py | 5 +- src/ralph/models/xapi/base/agents.py | 2 +- src/ralph/models/xapi/base/attachments.py | 2 +- src/ralph/models/xapi/base/common.py | 47 +++--- src/ralph/models/xapi/base/contexts.py | 2 +- src/ralph/models/xapi/base/statements.py | 21 +-- .../models/xapi/base/unnested_objects.py | 4 +- tests/models/xapi/base/test_statements.py | 138 ++++++------------ 8 files changed, 84 insertions(+), 137 deletions(-) diff --git a/src/ralph/conf.py b/src/ralph/conf.py index 028028870..a474dd92f 100644 --- a/src/ralph/conf.py +++ b/src/ralph/conf.py @@ -31,8 +31,9 @@ from pydantic import constr -NonEmptyStr = constr(min_length=1) -NonEmptyStrictStr = constr(min_length=1, strict=True) +NonEmptyStr = Annotated[str, Field(min_length=1)] +NonEmptyStrictStrPatch = Annotated[str, Field(min_length=1)] +NonEmptyStrictStr = constr(min_length=1, strict=True)#Annotated[StrictStr, Field(min_length=1)] class BaseSettingsConfig: """Pydantic model for BaseSettings Configuration.""" diff --git a/src/ralph/models/xapi/base/agents.py b/src/ralph/models/xapi/base/agents.py index 593602260..f3eaeb0e6 100644 --- a/src/ralph/models/xapi/base/agents.py +++ b/src/ralph/models/xapi/base/agents.py @@ -42,7 +42,7 @@ class BaseXapiAgentCommonProperties(BaseModelWithConfig, ABC): """ objectType: Optional[Literal["Agent"]] - name: Optional[NonEmptyStrictStr ] + name: Optional[NonEmptyStrictStr] class BaseXapiAgentWithMbox(BaseXapiAgentCommonProperties, BaseXapiMboxIFI): diff --git a/src/ralph/models/xapi/base/attachments.py b/src/ralph/models/xapi/base/attachments.py index 91ffdf93a..48e52ae31 100644 --- a/src/ralph/models/xapi/base/attachments.py +++ b/src/ralph/models/xapi/base/attachments.py @@ -27,4 +27,4 @@ class BaseXapiAttachment(BaseModelWithConfig): contentType: str length: int sha2: str - fileUrl: Optional[AnyUrl] + fileUrl: Optional[str] # TODO: change back to AnyUrl diff --git a/src/ralph/models/xapi/base/common.py b/src/ralph/models/xapi/base/common.py index 28b69e85b..67c50f9b4 100644 --- a/src/ralph/models/xapi/base/common.py +++ b/src/ralph/models/xapi/base/common.py @@ -6,40 +6,41 @@ from pydantic import StrictStr, validate_email from rfc3987 import parse -from ralph.conf import NonEmptyStr, NonEmptyStrictStr -class IRI(NonEmptyStrictStr): - """Pydantic custom data type validating RFC 3987 IRIs.""" +from ralph.conf import NonEmptyStr, NonEmptyStrictStr, NonEmptyStrictStrPatch - # TODO: Put this back - # @classmethod - # def __get_validators__(cls) -> Generator: # noqa: D105 - # def validate(iri: str) -> Type["IRI"]: - # """Check whether the provided IRI is a valid RFC 3987 IRI.""" - # parse(iri, rule="IRI") - # return cls(iri) +from typing import Annotated +from pydantic import Field +from pydantic import BaseModel, root_validator - # yield validate +class IRI(NonEmptyStrictStrPatch): + """Pydantic custom data type validating RFC 3987 IRIs.""" + @classmethod + def __get_validators__(cls) -> Generator: # noqa: D105 + def validate(iri: str) -> Type["IRI"]: + """Check whether the provided IRI is a valid RFC 3987 IRI.""" + parse(iri, rule="IRI") + return cls(iri) + + yield validate class LanguageTag(NonEmptyStr): """Pydantic custom data type validating RFC 5646 Language tags.""" - # TODO: Put this back - # @classmethod - # def __get_validators__(cls) -> Generator: # noqa: D105 - # def validate(tag: str) -> Type["LanguageTag"]: - # """Check whether the provided tag is a valid RFC 5646 Language tag.""" - # if not tag_is_valid(tag): - # raise TypeError("Invalid RFC 5646 Language tag") - # return cls(tag) + @classmethod + def __get_validators__(cls) -> Generator: # noqa: D105 + def validate(tag: str) -> Type["LanguageTag"]: + """Check whether the provided tag is a valid RFC 5646 Language tag.""" + if not tag_is_valid(tag): + print("Provided tag is:", tag) + raise TypeError("Invalid RFC 5646 Language tag") + return cls(tag) - # yield validate + yield validate -LanguageMap = Dict[LanguageTag, NonEmptyStrictStr] +LanguageMap = Dict[LanguageTag, NonEmptyStrictStr] # TODO: change back to strictstr -from typing import Annotated -from pydantic import Field email_pattern = r"(^mailto:[-!#-'*+/-9=?A-Z^-~]+(\.[-!#-'*+/-9=?A-Z^-~]+)*|\"([]!#-[^-~ \t]|(\\[\t -~]))+\")@([-!#-'*+/-9=?A-Z^-~]+(\.[-!#-'*+/-9=?A-Z^-~]+)*|\[[\t -Z^-~]*])" MailtoEmail = Annotated[str, Field(regex=email_pattern)] diff --git a/src/ralph/models/xapi/base/contexts.py b/src/ralph/models/xapi/base/contexts.py index 81c4a8460..164c851cb 100644 --- a/src/ralph/models/xapi/base/contexts.py +++ b/src/ralph/models/xapi/base/contexts.py @@ -29,6 +29,7 @@ class BaseXapiContextContextActivities(BaseModelWithConfig): category: Optional[Union[BaseXapiActivity, List[BaseXapiActivity]]] other: Optional[Union[BaseXapiActivity, List[BaseXapiActivity]]] + class BaseXapiContext(BaseModelWithConfig): """Pydantic model for `context` property. @@ -43,7 +44,6 @@ class BaseXapiContext(BaseModelWithConfig): statement (dict): Another Statement giving context for this Statement. extensions (dict): Consists of a dictionary of other properties as needed. """ - registration: Optional[UUID] instructor: Optional[BaseXapiAgent] team: Optional[BaseXapiGroup] diff --git a/src/ralph/models/xapi/base/statements.py b/src/ralph/models/xapi/base/statements.py index d65e3cb01..b67d85050 100644 --- a/src/ralph/models/xapi/base/statements.py +++ b/src/ralph/models/xapi/base/statements.py @@ -4,7 +4,7 @@ from typing import Any, List, Optional, Union from uuid import UUID -from pydantic import constr, root_validator +from pydantic import constr, root_validator, BaseModel from ..config import BaseModelWithConfig from .agents import BaseXapiAgent @@ -52,19 +52,14 @@ class BaseXapiStatement(BaseModelWithConfig): def check_absence_of_empty_and_invalid_values(cls, values: Any) -> Any: """Check the model for empty and invalid values. - Check that there are no empty values except in branch of `extenstions`. - Check that the `context` field contains `platform` and `revision` fields only if the `object.objectType` property is equal to `Activity`. """ for field, value in list(values.items()): - if field != "extensions": - if value in [None, "", {}]: - print("field is:", field, "value is:", value) - raise ValueError(f"{field}: invalid empty value") - if isinstance(value, dict): - print("nested field is:", field) - cls.check_absence_of_empty_and_invalid_values(value) + if value in [None, "", {}]: + raise ValueError(f"{field}: invalid empty value") + if isinstance(value, dict) and field != "extensions": + cls.check_absence_of_empty_and_invalid_values(value) context = dict(values.get("context", {})) if context: @@ -73,7 +68,7 @@ def check_absence_of_empty_and_invalid_values(cls, values: Any) -> Any: object_type = dict(values["object"]).get("objectType", "Activity") if (platform or revision) and object_type != "Activity": raise ValueError( - "context revision and platform properties can only be used" - " if the Statement's Object is an Activity" + "revision and platform properties can only be used if the " + "Statement's Object is an Activity" ) - return values + return values \ No newline at end of file diff --git a/src/ralph/models/xapi/base/unnested_objects.py b/src/ralph/models/xapi/base/unnested_objects.py index 296cfdfa7..44c1aa54c 100644 --- a/src/ralph/models/xapi/base/unnested_objects.py +++ b/src/ralph/models/xapi/base/unnested_objects.py @@ -6,6 +6,8 @@ from pydantic import AnyUrl, Field, StrictStr, constr, validator +from ralph.conf import NonEmptyStr, NonEmptyStrictStr + from ..config import BaseModelWithConfig from .common import IRI, LanguageMap @@ -72,7 +74,7 @@ class BaseXapiActivityInteractionDefinition(BaseXapiActivityDefinition): "numeric", "other", ] - correctResponsesPattern: Optional[List[StrictStr]] + correctResponsesPattern: Optional[List[NonEmptyStrictStr]] # TODO: change back to strictstr choices: Optional[List[BaseXapiInteractionComponent]] scale: Optional[List[BaseXapiInteractionComponent]] source: Optional[List[BaseXapiInteractionComponent]] diff --git a/tests/models/xapi/base/test_statements.py b/tests/models/xapi/base/test_statements.py index 35faaad54..b602397b9 100644 --- a/tests/models/xapi/base/test_statements.py +++ b/tests/models/xapi/base/test_statements.py @@ -1,6 +1,7 @@ """Tests for the BaseXapiStatement.""" import json +from typing import Callable import pytest from hypothesis import settings @@ -27,22 +28,23 @@ from tests.fixtures.hypothesis_strategies import custom_builds, custom_given -from polyfactory.factories.pydantic_factory import ModelFactory +# from polyfactory.factories.pydantic_factory import ModelFactory as PydanticFactory + +from polyfactory.factories.pydantic_factory import ( + ModelFactory as PolyfactoryModelFactory, + T, +) + from polyfactory import Use from polyfactory.fields import Ignore, Require from ralph.models.xapi.base.common import MailtoEmail, IRI from ralph.models.xapi.base.agents import BaseXapiAgentWithMbox from typing import Dict, Type, Any - from uuid import UUID, uuid4 from pprint import pprint -# class IRIFactory(ModelFactory[IRI]): -# __model__ = IRI -# name = Use(ModelFactory.__random__.choice, ["Roxy", "Spammy", "Moshe"]) - from ralph.models.xapi.base.contexts import BaseXapiContext from ralph.models.xapi.base.results import BaseXapiResultScore from ralph.models.xapi.base.common import LanguageTag, LanguageMap @@ -63,51 +65,28 @@ def prune(d: Any, exceptions:list=[]): return False -# from pydantic import constr -# Toto = constr(min_length=5, strict=True) -# class MyClass(Toto): -# pass -# test = MyClass("2") -# Toto("2") -# assert False - -class FactoryMixin(): +class ModelFactory(PolyfactoryModelFactory[T]): __allow_none_optionals__ = False - - # @classmethod - # def get_provider_map(cls) -> Dict[Type, Any]: - # providers_map = super().get_provider_map() - # return { - # # IRI: lambda: IRI("https://w3id.org/xapi/video/verbs/played"), - # # BaseXapiAgentWithMbox: lambda: BaseXapiAgentWithMbox(mbox="mailto:test@toast.com"), - # # UUID: lambda: UUID(str(uuid4())), - # LanguageTag: lambda: LanguageTag("en-US"), - # LanguageMap: lambda: {LanguageTag("en-US"): "testval"}, # unsure why this is needed - # #BaseXapiResultScore: lambda: BaseXapiResultScore(min=Decimal("0.0"), max=Decimal("20.0"), raw=Decimal("11")), - # # MailtoEmail: lambda: MailtoEmail("mailto:test@example.xyz"), - # **providers_map, - # } + __is_base_factory__ = True - # @classmethod - # def _get_or_create_factory(cls, model: type): - # created_factory = super()._get_or_create_factory(model) - # created_factory.get_provider_map = cls.get_provider_map - # created_factory._get_or_create_factory = cls._get_or_create_factory - # return created_factory - + @classmethod + def get_provider_map(cls) -> dict[Any, Callable[[], Any]]: + provider_map = super().get_provider_map() + provider_map[LanguageTag] = lambda: LanguageTag("en-US") + provider_map[IRI] = lambda: IRI("https://w3id.org/xapi/video/verbs/played") + return provider_map + + @classmethod + def _get_or_create_factory(cls, model: type): + created_factory = super()._get_or_create_factory(model) + created_factory.get_provider_map = cls.get_provider_map + created_factory._get_or_create_factory = cls._get_or_create_factory + return created_factory -class BaseXapiContextFactory(FactoryMixin, ModelFactory[BaseXapiContext]): - __model__ = BaseXapiContext - __set_as_default_factory_for_type__ = True - - revision = Ignore() - platform = Ignore() - - contextActivities = lambda: BaseXapiContextContextActivitiesFactory.build() or Ignore() -class BaseXapiResultScoreFactory(FactoryMixin, ModelFactory[BaseXapiResultScore]): +class BaseXapiResultScoreFactory(ModelFactory[BaseXapiResultScore]): __set_as_default_factory_for_type__ = True __model__ = BaseXapiResultScore @@ -115,74 +94,40 @@ class BaseXapiResultScoreFactory(FactoryMixin, ModelFactory[BaseXapiResultScore] max=Decimal("20.0") raw=Decimal("11") -class LanguageTagFactory(FactoryMixin, ModelFactory[LanguageTag]): - __is_base_factory__ = True +class BaseXapiActivityInteractionDefinitionFactory(ModelFactory[BaseXapiActivityInteractionDefinition]): __set_as_default_factory_for_type__ = True - __model__ = LanguageTag - - __root__ = LanguageTag("en-US") - - # @classmethod - # def get_provider_map(cls) -> Dict[Type, Any]: - # providers_map = super().get_provider_map() - # return { - # LanguageTag: lambda: LanguageTag("en-US"), - # LanguageMap: lambda: {LanguageTag("en-US"): "testval"}, # unsure why this is needed - # **providers_map, - # } - - -class BaseXapiActivityInteractionDefinitionFactory(FactoryMixin, ModelFactory[BaseXapiActivityInteractionDefinition]): - __is_base_factory__ = True __model__ = BaseXapiActivityInteractionDefinition correctResponsesPattern = None -class BaseXapiContextContextActivitiesFactory(FactoryMixin, ModelFactory[BaseXapiContextContextActivities]): +class BaseXapiContextContextActivitiesFactory(ModelFactory[BaseXapiContextContextActivities]): __model__ = BaseXapiContextContextActivities -class BaseXapiActivityFactory(FactoryMixin, ModelFactory[BaseXapiActivity]): - __model__ = BaseXapiActivity - -# class BaseXapiAgentWithAccountFactory(FactoryMixin, ModelFactory[BaseXapiAgentWithAccount]): -# __model__ = BaseXapiAgentWithAccount - -# class BaseXapiAnonymousGroupFactory(FactoryMixin, ModelFactory[BaseXapiAnonymousGroup]): -# __model__ = BaseXapiAnonymousGroup - -# class BaseXapiIdentifiedGroupWithAccountFactory(ModelFactory[BaseXapiIdentifiedGroupWithAccount]): -# __model__ = BaseXapiIdentifiedGroupWithAccount - -# class BaseXapiSubStatementFactory(FactoryMixin, ModelFactory[BaseXapiSubStatement]): -# __model__ = BaseXapiSubStatement - -class BaseXapiStatementFactory(FactoryMixin, ModelFactory[BaseXapiStatement]): - __model__ = BaseXapiStatement - #result = Ignore() - - #context = lambda: BaseXapiContextFactory.build() +class BaseXapiContextFactory(ModelFactory[BaseXapiContext]): + __model__ = BaseXapiContext + __set_as_default_factory_for_type__ = True + revision = Ignore() + platform = Ignore() -# def gen_statement(*args, **kwargs): -# # Custom logic necessary as post-processing is not possible in polyfactory (when can generate empty dicts when all fields are optional) -# return BaseXapiStatement(**prune(BaseXapiStatementFactory.process_kwargs(*args, **kwargs), exceptions=["extensions"])) + contextActivities = lambda: BaseXapiContextContextActivitiesFactory.build() or Ignore() def mock_instance(klass, *args, **kwargs): """Generate a mock instance of a given class (`klass`).""" - class KlassFactory(FactoryMixin, ModelFactory[klass]): + class KlassFactory(ModelFactory[klass]): __model__ = klass - - return klass(**prune(KlassFactory.process_kwargs(*args, **kwargs))) + + kwargs = KlassFactory.process_kwargs(*args, **kwargs) + kwargs = prune(kwargs) + return klass(**kwargs) -for x in range(100): - a = BaseXapiStatementFactory.process_kwargs() - + @pytest.mark.parametrize( "path", @@ -224,14 +169,17 @@ def test_models_xapi_base_statement_with_valid_null_values(path, value): property. """ - statement = mock_instance(BaseXapiStatement, object=mock_instance(BaseXapiActivity)) + statement = mock_instance(BaseXapiStatement, object=mock_instance(BaseXapiActivity)) # TODO: check that change from Activity to Agent is OK statement = statement.dict(exclude_none=True) + + # Error might be linked to empty values being generated + # assert BaseXapiStatement(**statement) # TODO: Remove + set_dict_value_from_path(statement, path.split("__"), value) try: BaseXapiStatement(**statement) except ValidationError as err: - pprint(statement) pytest.fail(f"Valid statement should not raise exceptions: {err}") # class BaseXapiActivityInteractionDefinitionFactory(ModelFactory[BaseXapiActivityInteractionDefinitionFactory]): From 12f01827ab0462e118565f6dc6111e89c38825a6 Mon Sep 17 00:00:00 2001 From: lleeoo Date: Wed, 6 Dec 2023 22:57:46 +0100 Subject: [PATCH 07/19] wip --- src/ralph/conf.py | 22 +- src/ralph/models/xapi/base/agents.py | 1 + src/ralph/models/xapi/base/attachments.py | 2 +- src/ralph/models/xapi/base/common.py | 4 +- src/ralph/models/xapi/base/contexts.py | 2 + src/ralph/models/xapi/base/groups.py | 1 + src/ralph/models/xapi/base/ifi.py | 1 + src/ralph/models/xapi/base/results.py | 4 +- src/ralph/models/xapi/base/statements.py | 5 +- .../models/xapi/base/unnested_objects.py | 11 +- .../models/xapi/virtual_classroom/results.py | 1 + tests/fixtures/hypothesis_strategies.py | 2 +- tests/models/xapi/base/test_statements.py | 860 ++++++++---------- tests/test_cli.py | 2 - 14 files changed, 420 insertions(+), 498 deletions(-) diff --git a/src/ralph/conf.py b/src/ralph/conf.py index a474dd92f..ed64dc0ad 100644 --- a/src/ralph/conf.py +++ b/src/ralph/conf.py @@ -6,7 +6,17 @@ from pathlib import Path from typing import Annotated, List, Sequence, Tuple, Union -from pydantic import AnyHttpUrl, AnyUrl, BaseModel, BaseSettings, constr, Extra, Field, root_validator, StrictStr +from pydantic import ( + AnyHttpUrl, + AnyUrl, + BaseModel, + BaseSettings, + constr, + Extra, + Field, + root_validator, + StrictStr, +) from ralph.exceptions import ConfigurationException @@ -31,9 +41,12 @@ from pydantic import constr -NonEmptyStr = Annotated[str, Field(min_length=1)] -NonEmptyStrictStrPatch = Annotated[str, Field(min_length=1)] -NonEmptyStrictStr = constr(min_length=1, strict=True)#Annotated[StrictStr, Field(min_length=1)] +NonEmptyStr = Annotated[str, Field(min_length=1)] +NonEmptyStrictStrPatch = Annotated[str, Field(min_length=1)] +NonEmptyStrictStr = constr( + min_length=1, strict=True +) # Annotated[StrictStr, Field(min_length=1)] + class BaseSettingsConfig: """Pydantic model for BaseSettings Configuration.""" @@ -123,6 +136,7 @@ class ParserSettings(BaseModel): GELF: GELFParserSettings = GELFParserSettings() ES: ESParserSettings = ESParserSettings() + class XapiForwardingConfigurationSettings(BaseModel): """Pydantic model for xAPI forwarding configuration item.""" diff --git a/src/ralph/models/xapi/base/agents.py b/src/ralph/models/xapi/base/agents.py index f3eaeb0e6..139ef4d93 100644 --- a/src/ralph/models/xapi/base/agents.py +++ b/src/ralph/models/xapi/base/agents.py @@ -31,6 +31,7 @@ class BaseXapiAgentAccount(BaseModelWithConfig): homePage: IRI name: NonEmptyStrictStr + class BaseXapiAgentCommonProperties(BaseModelWithConfig, ABC): """Pydantic model for core `Agent` type property. diff --git a/src/ralph/models/xapi/base/attachments.py b/src/ralph/models/xapi/base/attachments.py index 48e52ae31..91ffdf93a 100644 --- a/src/ralph/models/xapi/base/attachments.py +++ b/src/ralph/models/xapi/base/attachments.py @@ -27,4 +27,4 @@ class BaseXapiAttachment(BaseModelWithConfig): contentType: str length: int sha2: str - fileUrl: Optional[str] # TODO: change back to AnyUrl + fileUrl: Optional[AnyUrl] diff --git a/src/ralph/models/xapi/base/common.py b/src/ralph/models/xapi/base/common.py index 67c50f9b4..0c62789bf 100644 --- a/src/ralph/models/xapi/base/common.py +++ b/src/ralph/models/xapi/base/common.py @@ -12,6 +12,7 @@ from pydantic import Field from pydantic import BaseModel, root_validator + class IRI(NonEmptyStrictStrPatch): """Pydantic custom data type validating RFC 3987 IRIs.""" @@ -24,6 +25,7 @@ def validate(iri: str) -> Type["IRI"]: yield validate + class LanguageTag(NonEmptyStr): """Pydantic custom data type validating RFC 5646 Language tags.""" @@ -39,7 +41,7 @@ def validate(tag: str) -> Type["LanguageTag"]: yield validate -LanguageMap = Dict[LanguageTag, NonEmptyStrictStr] # TODO: change back to strictstr +LanguageMap = Dict[LanguageTag, NonEmptyStrictStr] # TODO: change back to strictstr email_pattern = r"(^mailto:[-!#-'*+/-9=?A-Z^-~]+(\.[-!#-'*+/-9=?A-Z^-~]+)*|\"([]!#-[^-~ \t]|(\\[\t -~]))+\")@([-!#-'*+/-9=?A-Z^-~]+(\.[-!#-'*+/-9=?A-Z^-~]+)*|\[[\t -Z^-~]*])" diff --git a/src/ralph/models/xapi/base/contexts.py b/src/ralph/models/xapi/base/contexts.py index 164c851cb..6eb3d0fd6 100644 --- a/src/ralph/models/xapi/base/contexts.py +++ b/src/ralph/models/xapi/base/contexts.py @@ -11,6 +11,7 @@ from ralph.conf import NonEmptyStrictStr + class BaseXapiContextContextActivities(BaseModelWithConfig): """Pydantic model for context `contextActivities` property. @@ -44,6 +45,7 @@ class BaseXapiContext(BaseModelWithConfig): statement (dict): Another Statement giving context for this Statement. extensions (dict): Consists of a dictionary of other properties as needed. """ + registration: Optional[UUID] instructor: Optional[BaseXapiAgent] team: Optional[BaseXapiGroup] diff --git a/src/ralph/models/xapi/base/groups.py b/src/ralph/models/xapi/base/groups.py index 563dc802f..2ac3433c5 100644 --- a/src/ralph/models/xapi/base/groups.py +++ b/src/ralph/models/xapi/base/groups.py @@ -20,6 +20,7 @@ from ralph.conf import NonEmptyStrictStr + class BaseXapiGroupCommonProperties(BaseModelWithConfig, ABC): """Pydantic model for core `Group` type property. diff --git a/src/ralph/models/xapi/base/ifi.py b/src/ralph/models/xapi/base/ifi.py index b940b41ef..1ba3c5a40 100644 --- a/src/ralph/models/xapi/base/ifi.py +++ b/src/ralph/models/xapi/base/ifi.py @@ -26,6 +26,7 @@ class BaseXapiMboxIFI(BaseModelWithConfig): Attributes: mbox (MailtoEmail): Consists of the Agent's email address. """ + # pattern = r'mailto:\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b' # mbox: Annotated[str, Field(regex=pattern)]# mbox: MailtoEmail diff --git a/src/ralph/models/xapi/base/results.py b/src/ralph/models/xapi/base/results.py index 21c0f3282..908656bb2 100644 --- a/src/ralph/models/xapi/base/results.py +++ b/src/ralph/models/xapi/base/results.py @@ -27,7 +27,7 @@ class BaseXapiResultScore(BaseModelWithConfig): min: Optional[Decimal] max: Optional[Decimal] - @root_validator # TODO: check if adding pre is still valid + @root_validator @classmethod def check_raw_min_max_relation(cls, values: Any) -> Any: """Check the relationship `min < raw < max`.""" @@ -45,7 +45,7 @@ def check_raw_min_max_relation(cls, values: Any) -> Any: raise ValueError("raw cannot be greater than max") return values - + class BaseXapiResult(BaseModelWithConfig): """Pydantic model for `result` property. diff --git a/src/ralph/models/xapi/base/statements.py b/src/ralph/models/xapi/base/statements.py index b67d85050..4ba229065 100644 --- a/src/ralph/models/xapi/base/statements.py +++ b/src/ralph/models/xapi/base/statements.py @@ -16,7 +16,8 @@ from .verbs import BaseXapiVerb -from pprint import pprint # TODO: remove +from pprint import pprint # TODO: remove + class BaseXapiStatement(BaseModelWithConfig): """Pydantic model for base xAPI statements. @@ -71,4 +72,4 @@ def check_absence_of_empty_and_invalid_values(cls, values: Any) -> Any: "revision and platform properties can only be used if the " "Statement's Object is an Activity" ) - return values \ No newline at end of file + return values diff --git a/src/ralph/models/xapi/base/unnested_objects.py b/src/ralph/models/xapi/base/unnested_objects.py index 44c1aa54c..3b5f2c59d 100644 --- a/src/ralph/models/xapi/base/unnested_objects.py +++ b/src/ralph/models/xapi/base/unnested_objects.py @@ -74,7 +74,7 @@ class BaseXapiActivityInteractionDefinition(BaseXapiActivityDefinition): "numeric", "other", ] - correctResponsesPattern: Optional[List[NonEmptyStrictStr]] # TODO: change back to strictstr + correctResponsesPattern: Optional[List[NonEmptyStrictStr]] choices: Optional[List[BaseXapiInteractionComponent]] scale: Optional[List[BaseXapiInteractionComponent]] source: Optional[List[BaseXapiInteractionComponent]] @@ -102,10 +102,11 @@ class BaseXapiActivity(BaseModelWithConfig): id: IRI objectType: Optional[Literal["Activity"]] definition: Optional[ - Union[ - BaseXapiActivityDefinition, - BaseXapiActivityInteractionDefinition, - ]] + Union[ + BaseXapiActivityDefinition, + BaseXapiActivityInteractionDefinition, + ] + ] class BaseXapiStatementRef(BaseModelWithConfig): diff --git a/src/ralph/models/xapi/virtual_classroom/results.py b/src/ralph/models/xapi/virtual_classroom/results.py index 898fd7b3f..ee6d0eaee 100644 --- a/src/ralph/models/xapi/virtual_classroom/results.py +++ b/src/ralph/models/xapi/virtual_classroom/results.py @@ -4,6 +4,7 @@ from ralph.conf import NonEmptyStrictStr + class VirtualClassroomAnsweredPollResult(BaseXapiResult): """Pydantic model for virtual classroom answered poll `result` property. diff --git a/tests/fixtures/hypothesis_strategies.py b/tests/fixtures/hypothesis_strategies.py index d2091299e..9fbd033bc 100644 --- a/tests/fixtures/hypothesis_strategies.py +++ b/tests/fixtures/hypothesis_strategies.py @@ -95,7 +95,6 @@ def custom_builds( return st.fixed_dictionaries(required, optional=optional).map(klass.parse_obj) - def custom_given(*args: Union[st.SearchStrategy, BaseModel], **kwargs): """Wrap the Hypothesis `given` function. Replace st.builds with custom_builds.""" strategies = [] @@ -103,6 +102,7 @@ def custom_given(*args: Union[st.SearchStrategy, BaseModel], **kwargs): strategies.append(custom_builds(arg) if is_base_model(arg) else arg) return given(*strategies, **kwargs) + # from polyfactory.factories.pydantic_factory import ModelFactory # def custom_given(klass): diff --git a/tests/models/xapi/base/test_statements.py b/tests/models/xapi/base/test_statements.py index b602397b9..2adf570cd 100644 --- a/tests/models/xapi/base/test_statements.py +++ b/tests/models/xapi/base/test_statements.py @@ -1,11 +1,11 @@ """Tests for the BaseXapiStatement.""" import json -from typing import Callable import pytest from hypothesis import settings from hypothesis import strategies as st +from polyfactory import Use from pydantic import ValidationError from ralph.models.selector import ModelSelector @@ -28,241 +28,143 @@ from tests.fixtures.hypothesis_strategies import custom_builds, custom_given -# from polyfactory.factories.pydantic_factory import ModelFactory as PydanticFactory - -from polyfactory.factories.pydantic_factory import ( - ModelFactory as PolyfactoryModelFactory, - T, -) - -from polyfactory import Use -from polyfactory.fields import Ignore, Require -from ralph.models.xapi.base.common import MailtoEmail, IRI -from ralph.models.xapi.base.agents import BaseXapiAgentWithMbox -from typing import Dict, Type, Any - -from uuid import UUID, uuid4 - -from pprint import pprint - -from ralph.models.xapi.base.contexts import BaseXapiContext -from ralph.models.xapi.base.results import BaseXapiResultScore -from ralph.models.xapi.base.common import LanguageTag, LanguageMap -from ralph.models.xapi.base.contexts import BaseXapiContextContextActivities - -from decimal import Decimal - -from typing import Any - -def prune(d: Any, exceptions:list=[]): - """Remove all empty leaves from a dict, except fo those in `exceptions`.""" - if isinstance(d, dict): - return {k:prune(v) for k,v in d.items() if prune(v) or (k in exceptions)} # TODO: Not ideal as pruning is applyied to exception - elif isinstance(d, list): - return [prune(v) for v in d if prune(v)] - if d: - return d - return False - - - - -class ModelFactory(PolyfactoryModelFactory[T]): - __allow_none_optionals__ = False - __is_base_factory__ = True - - @classmethod - def get_provider_map(cls) -> dict[Any, Callable[[], Any]]: - provider_map = super().get_provider_map() - provider_map[LanguageTag] = lambda: LanguageTag("en-US") - provider_map[IRI] = lambda: IRI("https://w3id.org/xapi/video/verbs/played") - return provider_map - - @classmethod - def _get_or_create_factory(cls, model: type): - created_factory = super()._get_or_create_factory(model) - created_factory.get_provider_map = cls.get_provider_map - created_factory._get_or_create_factory = cls._get_or_create_factory - return created_factory - - -class BaseXapiResultScoreFactory(ModelFactory[BaseXapiResultScore]): - __set_as_default_factory_for_type__ = True - __model__ = BaseXapiResultScore - - min=Decimal("0.0") - max=Decimal("20.0") - raw=Decimal("11") - -class BaseXapiActivityInteractionDefinitionFactory(ModelFactory[BaseXapiActivityInteractionDefinition]): - __set_as_default_factory_for_type__ = True - __model__ = BaseXapiActivityInteractionDefinition - - correctResponsesPattern = None - -class BaseXapiContextContextActivitiesFactory(ModelFactory[BaseXapiContextContextActivities]): - __model__ = BaseXapiContextContextActivities - - -class BaseXapiContextFactory(ModelFactory[BaseXapiContext]): - __model__ = BaseXapiContext - __set_as_default_factory_for_type__ = True - - revision = Ignore() - platform = Ignore() - - contextActivities = lambda: BaseXapiContextContextActivitiesFactory.build() or Ignore() +from tests.factories import mock_instance, ModelFactory -def mock_instance(klass, *args, **kwargs): - """Generate a mock instance of a given class (`klass`).""" - - class KlassFactory(ModelFactory[klass]): - __model__ = klass - - - kwargs = KlassFactory.process_kwargs(*args, **kwargs) - kwargs = prune(kwargs) - return klass(**kwargs) - - - - - -@pytest.mark.parametrize( - "path", - ["id", "stored", "verb__display", "result__score__raw"], -) -@pytest.mark.parametrize("value", [None, "", {}]) -def test_models_xapi_base_statement_with_invalid_null_values(path, value): - """Test that the statement does not accept any null values. - - XAPI-00001 - An LRS rejects with error code 400 Bad Request any Statement having a property whose - value is set to "null", an empty object, or has no value, except in an "extensions" - property. - """ - statement = mock_instance(BaseXapiStatement) - - statement = statement.dict(exclude_none=True) - set_dict_value_from_path(statement, path.split("__"), value) +# @pytest.mark.parametrize( +# "path", +# ["id", "stored", "verb__display", "result__score__raw"], +# ) +# @pytest.mark.parametrize("value", [None, "", {}]) +# def test_models_xapi_base_statement_with_invalid_null_values(path, value): +# """Test that the statement does not accept any null values. + +# XAPI-00001 +# An LRS rejects with error code 400 Bad Request any Statement having a property whose +# value is set to "null", an empty object, or has no value, except in an "extensions" +# property. +# """ +# statement = mock_instance(BaseXapiStatement) - with pytest.raises(ValidationError, match="invalid empty value"): - BaseXapiStatement(**statement) +# statement = statement.dict(exclude_none=True) +# set_dict_value_from_path(statement, path.split("__"), value) +# with pytest.raises(ValidationError, match="invalid empty value"): +# BaseXapiStatement(**statement) -@pytest.mark.parametrize( - "path", - [ - "object__definition__extensions__https://w3id.org/xapi/video", - "result__extensions__https://w3id.org/xapi/video", - "context__extensions__https://w3id.org/xapi/video", - ], -) -@pytest.mark.parametrize("value", [None, "", {}]) -def test_models_xapi_base_statement_with_valid_null_values(path, value): - """Test that the statement does accept valid null values in extensions fields. - XAPI-00001 - An LRS rejects with error code 400 Bad Request any Statement having a property whose - value is set to "null", an empty object, or has no value, except in an "extensions" - property. - """ +# @pytest.mark.parametrize( +# "path", +# [ +# "object__definition__extensions__https://w3id.org/xapi/video", +# "result__extensions__https://w3id.org/xapi/video", +# "context__extensions__https://w3id.org/xapi/video", +# ], +# ) +# @pytest.mark.parametrize("value", [None, "", {}]) +# def test_models_xapi_base_statement_with_valid_null_values(path, value): +# """Test that the statement does accept valid null values in extensions fields. + +# XAPI-00001 +# An LRS rejects with error code 400 Bad Request any Statement having a property whose +# value is set to "null", an empty object, or has no value, except in an "extensions" +# property. +# """ - statement = mock_instance(BaseXapiStatement, object=mock_instance(BaseXapiActivity)) # TODO: check that change from Activity to Agent is OK +# statement = mock_instance(BaseXapiStatement, object=mock_instance(BaseXapiActivity)) - statement = statement.dict(exclude_none=True) - - # Error might be linked to empty values being generated - # assert BaseXapiStatement(**statement) # TODO: Remove +# statement = statement.dict(exclude_none=True) - set_dict_value_from_path(statement, path.split("__"), value) - try: - BaseXapiStatement(**statement) - except ValidationError as err: - pytest.fail(f"Valid statement should not raise exceptions: {err}") +# set_dict_value_from_path(statement, path.split("__"), value) +# try: +# BaseXapiStatement(**statement) +# except ValidationError as err: +# pytest.fail(f"Valid statement should not raise exceptions: {err}") -# class BaseXapiActivityInteractionDefinitionFactory(ModelFactory[BaseXapiActivityInteractionDefinitionFactory]): -# __model__ = BaseXapiActivityInteractionDefinitionFactory -@pytest.mark.parametrize("path", ["object__definition__correctResponsesPattern"]) -def test_models_xapi_base_statement_with_valid_empty_array(path): - """Test that the statement does accept a valid empty array. +# @pytest.mark.parametrize("path", ["object__definition__correctResponsesPattern"]) +# def test_models_xapi_base_statement_with_valid_empty_array(path): +# """Test that the statement does accept a valid empty array. - Where the Correct Responses Pattern contains an empty array, the meaning of this is - that there is no correct answer. - """ +# Where the Correct Responses Pattern contains an empty array, the meaning of this is +# that there is no correct answer. +# """ - statement = mock_instance(BaseXapiStatement, - object=mock_instance(BaseXapiActivity, - definition=mock_instance(BaseXapiActivityInteractionDefinition))) +# statement = mock_instance( +# BaseXapiStatement, +# object=mock_instance( +# BaseXapiActivity, +# definition=mock_instance(BaseXapiActivityInteractionDefinition), +# ), +# ) - statement = statement.dict(exclude_none=True) - set_dict_value_from_path(statement, path.split("__"), []) - try: - BaseXapiStatement(**statement) - except ValidationError as err: - pytest.fail(f"Valid statement should not raise exceptions: {err}") +# statement = statement.dict(exclude_none=True) +# set_dict_value_from_path(statement, path.split("__"), []) +# try: +# BaseXapiStatement(**statement) +# except ValidationError as err: +# pytest.fail(f"Valid statement should not raise exceptions: {err}") -@pytest.mark.parametrize( - "field", - ["actor", "verb", "object"], -) -def test_models_xapi_base_statement_must_use_actor_verb_and_object(field): - """Test that the statement raises an exception if required fields are missing. - - XAPI-00003 - An LRS rejects with error code 400 Bad Request a Statement which does not contain an - "actor" property. - XAPI-00004 - An LRS rejects with error code 400 Bad Request a Statement which does not contain a - "verb" property. - XAPI-00005 - An LRS rejects with error code 400 Bad Request a Statement which does not contain an - "object" property. - """ +# @pytest.mark.parametrize( +# "field", +# ["actor", "verb", "object"], +# ) +# def test_models_xapi_base_statement_must_use_actor_verb_and_object(field): +# """Test that the statement raises an exception if required fields are missing. + +# XAPI-00003 +# An LRS rejects with error code 400 Bad Request a Statement which does not contain an +# "actor" property. +# XAPI-00004 +# An LRS rejects with error code 400 Bad Request a Statement which does not contain a +# "verb" property. +# XAPI-00005 +# An LRS rejects with error code 400 Bad Request a Statement which does not contain an +# "object" property. +# """ - statement = mock_instance(BaseXapiStatement) +# statement = mock_instance(BaseXapiStatement) - statement = statement.dict(exclude_none=True) - del statement["context"] # Necessary as context leads to another validation error - del statement[field] - with pytest.raises(ValidationError, match="field required"): - BaseXapiStatement(**statement) +# statement = statement.dict(exclude_none=True) +# del statement["context"] # Necessary as context leads to another validation error +# del statement[field] +# with pytest.raises(ValidationError, match="field required"): +# BaseXapiStatement(**statement) -@pytest.mark.parametrize( - "path,value", - [ - ("actor__name", 1), # Should be a string - ("actor__account__name", 1), # Should be a string - ("actor__account__homePage", 1), # Should be an IRI - ("actor__account", ["foo", "bar"]), # Should be a dictionary - ("verb__display", ["foo"]), # Should be a dictionary - ("verb__display", {"en": 1}), # Should have string values - ("object__id", ["foo"]), # Should be an IRI - ], -) -def test_models_xapi_base_statement_with_invalid_data_types(path, value): - """Test that the statement does not accept values with wrong types. +# @pytest.mark.parametrize( +# "path,value", +# [ +# ("actor__name", 1), # Should be a string +# ("actor__account__name", 1), # Should be a string +# ("actor__account__homePage", 1), # Should be an IRI +# ("actor__account", ["foo", "bar"]), # Should be a dictionary +# ("verb__display", ["foo"]), # Should be a dictionary +# ("verb__display", {"en": 1}), # Should have string values +# ("object__id", ["foo"]), # Should be an IRI +# ], +# ) +# def test_models_xapi_base_statement_with_invalid_data_types(path, value): +# """Test that the statement does not accept values with wrong types. - XAPI-00006 - An LRS rejects with error code 400 Bad Request a Statement which uses the wrong data - type. - """ - statement = mock_instance(BaseXapiStatement, actor=mock_instance(BaseXapiAgentWithAccount)) +# XAPI-00006 +# An LRS rejects with error code 400 Bad Request a Statement which uses the wrong data +# type. +# """ +# statement = mock_instance( +# BaseXapiStatement, actor=mock_instance(BaseXapiAgentWithAccount) +# ) - statement = statement.dict(exclude_none=True) - set_dict_value_from_path(statement, path.split("__"), value) +# statement = statement.dict(exclude_none=True) +# set_dict_value_from_path(statement, path.split("__"), value) - err = "(type expected|not a valid dict|expected string )" +# err = "(type expected|not a valid dict|expected string )" - with pytest.raises(ValidationError, match=err): - BaseXapiStatement(**statement) +# with pytest.raises(ValidationError, match=err): +# BaseXapiStatement(**statement) -# TODO: put this back # @pytest.mark.parametrize( # "path,value", # [ @@ -281,8 +183,9 @@ def test_models_xapi_base_statement_with_invalid_data_types(path, value): # particular format (such as mailto IRI, UUID, or IRI) is required. # (Empty strings are covered by XAPI-00001) # """ -# statement = mock_instance(BaseXapiStatement, actor=mock_instance(BaseXapiAgentWithAccount)) - +# statement = mock_instance( +# BaseXapiStatement, actor=mock_instance(BaseXapiAgentWithAccount) +# ) # statement = statement.dict(exclude_none=True) # set_dict_value_from_path(statement, path.split("__"), value) @@ -291,335 +194,332 @@ def test_models_xapi_base_statement_with_invalid_data_types(path, value): # BaseXapiStatement(**statement) -@pytest.mark.parametrize("path,value", [("actor__objecttype", "Agent")]) -def test_models_xapi_base_statement_with_invalid_letter_cases(path, value): - """Test that the statement does not accept keys having invalid letter cases. +# @pytest.mark.parametrize("path,value", [("actor__objecttype", "Agent")]) +# def test_models_xapi_base_statement_with_invalid_letter_cases(path, value): +# """Test that the statement does not accept keys having invalid letter cases. - XAPI-00008 - An LRS rejects with error code 400 Bad Request a Statement where the case of a key - does not match the case specified in this specification. - """ - statement = mock_instance(BaseXapiStatement, actor=mock_instance(BaseXapiAgentWithAccount)) +# XAPI-00008 +# An LRS rejects with error code 400 Bad Request a Statement where the case of a key +# does not match the case specified in this specification. +# """ +# statement = mock_instance( +# BaseXapiStatement, actor=mock_instance(BaseXapiAgentWithAccount) +# ) - statement = statement.dict(exclude_none=True) - if statement["actor"].get("objectType", None): - del statement["actor"]["objectType"] - set_dict_value_from_path(statement, path.split("__"), value) - err = "(unexpected value|extra fields not permitted)" - with pytest.raises(ValidationError, match=err): - BaseXapiStatement(**statement) +# statement = statement.dict(exclude_none=True) +# if statement["actor"].get("objectType", None): +# del statement["actor"]["objectType"] +# set_dict_value_from_path(statement, path.split("__"), value) +# err = "(unexpected value|extra fields not permitted)" +# with pytest.raises(ValidationError, match=err): +# BaseXapiStatement(**statement) -def test_models_xapi_base_statement_should_not_accept_additional_properties(): - """Test that the statement does not accept additional properties. +# def test_models_xapi_base_statement_should_not_accept_additional_properties(): +# """Test that the statement does not accept additional properties. - XAPI-00010 - An LRS rejects with error code 400 Bad Request a Statement where a key or value is - not allowed by this specification. - """ - statement = mock_instance(BaseXapiStatement) +# XAPI-00010 +# An LRS rejects with error code 400 Bad Request a Statement where a key or value is +# not allowed by this specification. +# """ +# statement = mock_instance(BaseXapiStatement) - invalid_statement = statement.dict(exclude_none=True) - invalid_statement["NEW_INVALID_FIELD"] = "some value" - with pytest.raises(ValidationError, match="extra fields not permitted"): - BaseXapiStatement(**invalid_statement) +# invalid_statement = statement.dict(exclude_none=True) +# invalid_statement["NEW_INVALID_FIELD"] = "some value" +# with pytest.raises(ValidationError, match="extra fields not permitted"): +# BaseXapiStatement(**invalid_statement) -@pytest.mark.parametrize("path,value", [("object__id", "w3id.org/xapi/video")]) -def test_models_xapi_base_statement_with_iri_without_scheme(path, value): - """Test that the statement does not accept IRIs without a scheme. +# @pytest.mark.parametrize("path,value", [("object__id", "w3id.org/xapi/video")]) +# def test_models_xapi_base_statement_with_iri_without_scheme(path, value): +# """Test that the statement does not accept IRIs without a scheme. - XAPI-00011 - An LRS rejects with error code 400 Bad Request a Statement containing IRL or IRI - values without a scheme. - """ - statement = mock_instance(BaseXapiStatement) +# XAPI-00011 +# An LRS rejects with error code 400 Bad Request a Statement containing IRL or IRI +# values without a scheme. +# """ +# statement = mock_instance(BaseXapiStatement) - statement = statement.dict(exclude_none=True) - set_dict_value_from_path(statement, path.split("__"), value) - with pytest.raises(ValidationError, match="is not a valid 'IRI'"): - BaseXapiStatement(**statement) +# statement = statement.dict(exclude_none=True) +# set_dict_value_from_path(statement, path.split("__"), value) +# with pytest.raises(ValidationError, match="is not a valid 'IRI'"): +# BaseXapiStatement(**statement) -@pytest.mark.parametrize( - "path", - [ - "object__definition__extensions__foo", - "result__extensions__1", - "context__extensions__w3id.org/xapi/video", - ], -) -def test_models_xapi_base_statement_with_invalid_extensions(path): - """Test that the statement does not accept extensions keys with invalid IRIs. +# @pytest.mark.parametrize( +# "path", +# [ +# "object__definition__extensions__foo", +# "result__extensions__1", +# "context__extensions__w3id.org/xapi/video", +# ], +# ) +# def test_models_xapi_base_statement_with_invalid_extensions(path): +# """Test that the statement does not accept extensions keys with invalid IRIs. - XAPI-00118 - An Extension "key" is an IRI. The LRS rejects with 400 a statement which has an - extension key which is not a valid IRI, if an extension object is present. - """ - statement = mock_instance(BaseXapiStatement, object=mock_instance(BaseXapiActivity)) +# XAPI-00118 +# An Extension "key" is an IRI. The LRS rejects with 400 a statement which has an +# extension key which is not a valid IRI, if an extension object is present. +# """ +# statement = mock_instance(BaseXapiStatement, object=mock_instance(BaseXapiActivity)) - statement = statement.dict(exclude_none=True) - set_dict_value_from_path(statement, path.split("__"), "") - with pytest.raises(ValidationError, match="is not a valid 'IRI'"): - BaseXapiStatement(**statement) +# statement = statement.dict(exclude_none=True) +# set_dict_value_from_path(statement, path.split("__"), "") +# with pytest.raises(ValidationError, match="is not a valid 'IRI'"): +# BaseXapiStatement(**statement) -@pytest.mark.parametrize("path,value", [("actor__mbox", "mailto:example@mail.com")]) -def test_models_xapi_base_statement_with_two_agent_types(path, value): - """Test that the statement does not accept multiple agent types. +# @pytest.mark.parametrize("path,value", [("actor__mbox", "mailto:example@mail.com")]) +# def test_models_xapi_base_statement_with_two_agent_types(path, value): +# """Test that the statement does not accept multiple agent types. - An Agent MUST NOT include more than one Inverse Functional Identifier. - """ - statement = mock_instance(BaseXapiStatement, actor=mock_instance(BaseXapiAgentWithAccount)) +# An Agent MUST NOT include more than one Inverse Functional Identifier. +# """ +# statement = mock_instance( +# BaseXapiStatement, actor=mock_instance(BaseXapiAgentWithAccount) +# ) - statement = statement.dict(exclude_none=True) - set_dict_value_from_path(statement, path.split("__"), value) - with pytest.raises(ValidationError, match="extra fields not permitted"): - BaseXapiStatement(**statement) +# statement = statement.dict(exclude_none=True) +# set_dict_value_from_path(statement, path.split("__"), value) +# with pytest.raises(ValidationError, match="extra fields not permitted"): +# BaseXapiStatement(**statement) -def test_models_xapi_base_statement_missing_member_property(): - """Test that the statement does not accept group agents with missing members. +# def test_models_xapi_base_statement_missing_member_property(): +# """Test that the statement does not accept group agents with missing members. + +# An Anonymous Group MUST include a "member" property listing constituent Agents. +# """ +# statement = mock_instance( +# BaseXapiStatement, actor=mock_instance(BaseXapiAnonymousGroup) +# ) + +# statement = statement.dict(exclude_none=True) +# del statement["actor"]["member"] +# with pytest.raises(ValidationError, match="member\n field required"): +# BaseXapiStatement(**statement) - An Anonymous Group MUST include a "member" property listing constituent Agents. - """ - statement = mock_instance(BaseXapiStatement, actor=mock_instance(BaseXapiAnonymousGroup)) - statement = statement.dict(exclude_none=True) - del statement["actor"]["member"] - with pytest.raises(ValidationError, match="member\n field required"): - BaseXapiStatement(**statement) +# @pytest.mark.parametrize( +# "klass", +# [ +# BaseXapiAnonymousGroup, +# BaseXapiIdentifiedGroupWithMbox, +# BaseXapiIdentifiedGroupWithMboxSha1Sum, +# BaseXapiIdentifiedGroupWithOpenId, +# BaseXapiIdentifiedGroupWithAccount, +# ], +# ) +# def test_models_xapi_base_statement_with_invalid_group_objects(klass): +# """Test that the statement does not accept group agents with group members. +# An Anonymous Group MUST NOT contain Group Objects in the "member" identifiers. +# An Identified Group MUST NOT contain Group Objects in the "member" property. +# """ -@pytest.mark.parametrize( - "value", - [ - BaseXapiAnonymousGroup, - BaseXapiIdentifiedGroupWithMbox, - BaseXapiIdentifiedGroupWithMboxSha1Sum, - BaseXapiIdentifiedGroupWithOpenId, - BaseXapiIdentifiedGroupWithAccount, - ], -) -@custom_given( - st.one_of( - custom_builds(BaseXapiStatement, actor=custom_builds(BaseXapiAnonymousGroup)), - custom_builds( - BaseXapiStatement, - actor=custom_builds(BaseXapiIdentifiedGroupWithMbox), - ), - custom_builds( - BaseXapiStatement, - actor=custom_builds(BaseXapiIdentifiedGroupWithMboxSha1Sum), - ), - custom_builds( - BaseXapiStatement, - actor=custom_builds(BaseXapiIdentifiedGroupWithOpenId), - ), - custom_builds( - BaseXapiStatement, - actor=custom_builds(BaseXapiIdentifiedGroupWithAccount), - ), - ), - st.data(), -) -def test_models_xapi_base_statement_with_invalid_group_objects(value, statement, data): - """Test that the statement does not accept group agents with group members. - - An Anonymous Group MUST NOT contain Group Objects in the "member" identifiers. - An Identified Group MUST NOT contain Group Objects in the "member" property. - """ - kwargs = {"exclude_none": True} - statement = statement.dict(**kwargs) - statement["actor"]["member"] = [data.draw(custom_builds(value)).dict(**kwargs)] - err = "actor -> member -> 0 -> objectType\n unexpected value; permitted: 'Agent'" - with pytest.raises(ValidationError, match=err): - BaseXapiStatement(**statement) - - -@pytest.mark.parametrize("path,value", [("actor__mbox", "mailto:example@mail.com")]) -# @custom_given( -# custom_builds( -# BaseXapiStatement, -# actor=custom_builds(BaseXapiIdentifiedGroupWithAccount), +# actor_class = ModelFactory.__random__.choice( +# [ +# BaseXapiAnonymousGroup, +# BaseXapiIdentifiedGroupWithMbox, +# BaseXapiIdentifiedGroupWithMboxSha1Sum, +# BaseXapiIdentifiedGroupWithOpenId, +# BaseXapiIdentifiedGroupWithAccount, +# ] # ) +# statement = mock_instance(BaseXapiStatement, actor=mock_instance(actor_class)) + +# kwargs = {"exclude_none": True} +# statement = statement.dict(**kwargs) +# statement["actor"]["member"] = [mock_instance(klass).dict(**kwargs)] # TODO: check that nothing was lost +# err = "actor -> member -> 0 -> objectType\n unexpected value; permitted: 'Agent'" +# with pytest.raises(ValidationError, match=err): +# BaseXapiStatement(**statement) + + +# @pytest.mark.parametrize("path,value", [("actor__mbox", "mailto:example@mail.com")]) +# def test_models_xapi_base_statement_with_two_group_identifiers(path, value): +# """Test that the statement does not accept multiple group identifiers. + +# An Identified Group MUST include exactly one Inverse Functional Identifier. +# """ +# statement = mock_instance( +# BaseXapiStatement, actor=mock_instance(BaseXapiIdentifiedGroupWithAccount) +# ) + +# statement = statement.dict(exclude_none=True) +# set_dict_value_from_path(statement, path.split("__"), value) +# with pytest.raises(ValidationError, match="extra fields not permitted"): +# BaseXapiStatement(**statement) + + +# @pytest.mark.parametrize( +# "path,value", +# [ +# ("object__id", "156e3f9f-4b56-4cba-ad52-1bd19e461d65"), +# ("object__stored", "2013-05-18T05:32:34.804+00:00"), +# ("object__version", "1.0.0"), +# ("object__authority", {"mbox": "mailto:example@mail.com"}), +# ], # ) -def test_models_xapi_base_statement_with_two_group_identifiers(path, value): - """Test that the statement does not accept multiple group identifiers. +# def test_models_xapi_base_statement_with_sub_statement_ref(path, value): +# """Test that the sub-statement does not accept invalid properties. - An Identified Group MUST include exactly one Inverse Functional Identifier. - """ - statement = mock_instance(BaseXapiStatement, actor=mock_instance(BaseXapiIdentifiedGroupWithAccount)) +# A SubStatement MUST NOT have the "id", "stored", "version" or "authority" +# properties. +# """ +# statement = mock_instance( +# BaseXapiStatement, object=mock_instance(BaseXapiSubStatement) +# ) - statement = statement.dict(exclude_none=True) - set_dict_value_from_path(statement, path.split("__"), value) - with pytest.raises(ValidationError, match="extra fields not permitted"): - BaseXapiStatement(**statement) +# statement = statement.dict(exclude_none=True) +# set_dict_value_from_path(statement, path.split("__"), value) +# with pytest.raises(ValidationError, match="extra fields not permitted"): +# BaseXapiStatement(**statement) -@pytest.mark.parametrize( - "path,value", - [ - ("object__id", "156e3f9f-4b56-4cba-ad52-1bd19e461d65"), - ("object__stored", "2013-05-18T05:32:34.804+00:00"), - ("object__version", "1.0.0"), - ("object__authority", {"mbox": "mailto:example@mail.com"}), - ], -) -# @custom_given( -# custom_builds(BaseXapiStatement, object=custom_builds(BaseXapiSubStatement)) +# @pytest.mark.parametrize( +# "value", +# [ +# [{"id": "invalid whitespace"}], +# [{"id": "valid"}, {"id": "invalid whitespace"}], +# [{"id": "invalid_duplicate"}, {"id": "invalid_duplicate"}], +# ], # ) -def test_models_xapi_base_statement_with_sub_statement_ref(path, value): - """Test that the sub-statement does not accept invalid properties. +# def test_models_xapi_base_statement_with_invalid_interaction_object(value): +# """Test that the statement does not accept invalid interaction fields. - A SubStatement MUST NOT have the "id", "stored", "version" or "authority" - properties. - """ - statement = mock_instance(BaseXapiStatement, object=mock_instance(BaseXapiSubStatement)) +# An interaction component's id value SHOULD NOT have whitespace. +# Within an array of interaction components, all id values MUST be distinct. +# """ +# statement = mock_instance( +# BaseXapiStatement, +# object=mock_instance( +# BaseXapiActivity, +# definition=mock_instance(BaseXapiActivityInteractionDefinition), +# ), +# ) - statement = statement.dict(exclude_none=True) - set_dict_value_from_path(statement, path.split("__"), value) - with pytest.raises(ValidationError, match="extra fields not permitted"): - BaseXapiStatement(**statement) +# statement = statement.dict(exclude_none=True) +# path = "object.definition.scale".split(".") +# set_dict_value_from_path(statement, path, value) +# err = "(Duplicate InteractionComponents are not valid|string does not match regex)" +# with pytest.raises(ValidationError, match=err): +# BaseXapiStatement(**statement) -@pytest.mark.parametrize( - "value", - [ - [{"id": "invalid whitespace"}], - [{"id": "valid"}, {"id": "invalid whitespace"}], - [{"id": "invalid_duplicate"}, {"id": "invalid_duplicate"}], - ], -) -@custom_given( - custom_builds( - BaseXapiStatement, - object=custom_builds( - BaseXapiActivity, - definition=custom_builds(BaseXapiActivityInteractionDefinition), - ), - ) -) -def test_models_xapi_base_statement_with_invalid_interaction_object(value, statement): - """Test that the statement does not accept invalid interaction fields. +# @pytest.mark.parametrize( +# "path,value", +# [ +# ("context__revision", "Format is free"), +# ("context__platform", "FUN MOOC"), +# ], +# ) +# def test_models_xapi_base_statement_with_invalid_context_value(path, value): +# """Test that the statement does not accept an invalid revision/platform value. - An interaction component's id value SHOULD NOT have whitespace. - Within an array of interaction components, all id values MUST be distinct. - """ - statement = statement.dict(exclude_none=True) - path = "object.definition.scale".split(".") - set_dict_value_from_path(statement, path, value) - err = "(Duplicate InteractionComponents are not valid|string does not match regex)" - with pytest.raises(ValidationError, match=err): - BaseXapiStatement(**statement) +# The "revision" property MUST only be used if the Statement's Object is an Activity. +# The "platform" property MUST only be used if the Statement's Object is an Activity. +# """ +# object_class = ModelFactory.__random__.choice( +# [ +# BaseXapiSubStatement, +# BaseXapiStatementRef, +# ] +# ) +# statement = mock_instance(BaseXapiStatement, object=mock_instance(object_class)) -@pytest.mark.parametrize( - "path,value", - [ - ("context__revision", "Format is free"), - ("context__platform", "FUN MOOC"), - ], -) -@custom_given( - st.one_of( - custom_builds(BaseXapiStatement, object=custom_builds(BaseXapiSubStatement)), - custom_builds(BaseXapiStatement, object=custom_builds(BaseXapiStatementRef)), - ), -) -def test_models_xapi_base_statement_with_invalid_context_value(path, value, statement): - """Test that the statement does not accept an invalid revision/platform value. - The "revision" property MUST only be used if the Statement's Object is an Activity. - The "platform" property MUST only be used if the Statement's Object is an Activity. - """ - statement = statement.dict(exclude_none=True) - set_dict_value_from_path(statement, path.split("__"), value) - err = "properties can only be used if the Statement's Object is an Activity" - with pytest.raises(ValidationError, match=err): - BaseXapiStatement(**statement) +# statement = statement.dict(exclude_none=True) +# set_dict_value_from_path(statement, path.split("__"), value) +# err = "properties can only be used if the Statement's Object is an Activity" +# with pytest.raises(ValidationError, match=err): +# BaseXapiStatement(**statement) + +# @pytest.mark.parametrize("path", ["context.contextActivities.not_parent"]) +# def test_models_xapi_base_statement_with_invalid_context_activities(path): +# """Test that the statement does not accept invalid context activity properties. -@pytest.mark.parametrize("path", ["context.contextActivities.not_parent"]) -@custom_given(BaseXapiStatement) -def test_models_xapi_base_statement_with_invalid_context_activities(path, statement): - """Test that the statement does not accept invalid context activity properties. +# Every key in the contextActivities Object MUST be one of parent, grouping, category, +# or other. +# """ +# statement = mock_instance(BaseXapiStatement) - Every key in the contextActivities Object MUST be one of parent, grouping, category, - or other. - """ - statement = statement.dict(exclude_none=True) - set_dict_value_from_path(statement, path.split("."), {"id": "http://w3id.org/xapi"}) - with pytest.raises(ValidationError, match="extra fields not permitted"): - BaseXapiStatement(**statement) +# statement = statement.dict(exclude_none=True) +# set_dict_value_from_path(statement, path.split("."), {"id": "http://w3id.org/xapi"}) +# with pytest.raises(ValidationError, match="extra fields not permitted"): +# BaseXapiStatement(**statement) -@pytest.mark.parametrize( - "value", - [ - {"id": "http://w3id.org/xapi"}, - [{"id": "http://w3id.org/xapi"}], - [{"id": "http://w3id.org/xapi"}, {"id": "http://w3id.org/xapi/video"}], - ], -) -@custom_given(BaseXapiStatement) -def test_models_xapi_base_statement_with_valid_context_activities(value, statement): - """Test that the statement does accept valid context activities fields. - - Every value in the contextActivities Object MUST be either a single Activity Object - or an array of Activity Objects. - """ - statement = statement.dict(exclude_none=True) - path = ["context", "contextActivities"] - for activity in ["parent", "grouping", "category", "other"]: - set_dict_value_from_path(statement, path + [activity], value) - try: - BaseXapiStatement(**statement) - except ValidationError as err: - pytest.fail(f"Valid statement should not raise exceptions: {err}") +# @pytest.mark.parametrize( +# "value", +# [ +# {"id": "http://w3id.org/xapi"}, +# [{"id": "http://w3id.org/xapi"}], +# [{"id": "http://w3id.org/xapi"}, {"id": "http://w3id.org/xapi/video"}], +# ], +# ) +# def test_models_xapi_base_statement_with_valid_context_activities(value): +# """Test that the statement does accept valid context activities fields. +# Every value in the contextActivities Object MUST be either a single Activity Object +# or an array of Activity Objects. +# """ +# statement = mock_instance(BaseXapiStatement) -@pytest.mark.parametrize("value", ["0.0.0", "1.1.0", "1", "2", "1.10.1", "1.0.1.1"]) -@custom_given(BaseXapiStatement) -def test_models_xapi_base_statement_with_invalid_version(value, statement): - """Test that the statement does not accept an invalid version field. +# statement = statement.dict(exclude_none=True) +# path = ["context", "contextActivities"] +# for activity in ["parent", "grouping", "category", "other"]: +# set_dict_value_from_path(statement, path + [activity], value) +# try: +# BaseXapiStatement(**statement) +# except ValidationError as err: +# pytest.fail(f"Valid statement should not raise exceptions: {err}") + + +# @pytest.mark.parametrize("value", ["0.0.0", "1.1.0", "1", "2", "1.10.1", "1.0.1.1"]) +# def test_models_xapi_base_statement_with_invalid_version(value): +# """Test that the statement does not accept an invalid version field. + +# An LRS MUST reject all Statements with a version specified that does not start with +# 1.0. +# """ +# statement = mock_instance(BaseXapiStatement) + +# statement = statement.dict(exclude_none=True) +# set_dict_value_from_path(statement, ["version"], value) +# with pytest.raises(ValidationError, match="version\n string does not match regex"): +# BaseXapiStatement(**statement) - An LRS MUST reject all Statements with a version specified that does not start with - 1.0. - """ - statement = statement.dict(exclude_none=True) - set_dict_value_from_path(statement, ["version"], value) - with pytest.raises(ValidationError, match="version\n string does not match regex"): - BaseXapiStatement(**statement) +# def test_models_xapi_base_statement_with_valid_version(): +# """Test that the statement does accept a valid version field. -@custom_given(BaseXapiStatement) -def test_models_xapi_base_statement_with_valid_version(statement): - """Test that the statement does accept a valid version field. +# Statements returned by an LRS MUST retain the version they are accepted with. +# If they lack a version, the version MUST be set to 1.0.0. +# """ +# statement = mock_instance(BaseXapiStatement) - Statements returned by an LRS MUST retain the version they are accepted with. - If they lack a version, the version MUST be set to 1.0.0. - """ - statement = statement.dict(exclude_none=True) - set_dict_value_from_path(statement, ["version"], "1.0.3") - assert "1.0.3" == BaseXapiStatement(**statement).dict()["version"] - del statement["version"] - assert "1.0.0" == BaseXapiStatement(**statement).dict()["version"] +# statement = statement.dict(exclude_none=True) +# set_dict_value_from_path(statement, ["version"], "1.0.3") +# assert "1.0.3" == BaseXapiStatement(**statement).dict()["version"] +# del statement["version"] +# assert "1.0.0" == BaseXapiStatement(**statement).dict()["version"] -@settings(deadline=None) @pytest.mark.parametrize( "model", list(ModelSelector("ralph.models.xapi").model_rules), ) -@custom_given(st.data()) def test_models_xapi_base_statement_should_consider_valid_all_defined_xapi_models( - model, data + model ): """Test that all defined xAPI models in the ModelSelector make valid statements.""" + # All specific xAPI models should inherit BaseXapiStatement assert issubclass(model, BaseXapiStatement) - statement = data.draw(custom_builds(model)).json(exclude_none=True, by_alias=True) + statement = mock_instance(model) # TODO: check that we are not losing info by mocking random model try: BaseXapiStatement(**json.loads(statement)) except ValidationError as err: diff --git a/tests/test_cli.py b/tests/test_cli.py index cd980a167..a859adea8 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -352,8 +352,6 @@ def test_cli_auth_command_when_writing_auth_file( username_1, password_1, scopes_1, ifi_command_1, ifi_value_1, write=True ) - print(cli_args) - assert Path(settings.AUTH_FILE).exists() is False result = runner.invoke(cli, cli_args) assert result.exit_code == 0 From da4bc009a2b263c524a87d61cd216796604f9aef Mon Sep 17 00:00:00 2001 From: lleeoo Date: Wed, 6 Dec 2023 22:58:38 +0100 Subject: [PATCH 08/19] wip --- tests/models/xapi/base/test_statements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/models/xapi/base/test_statements.py b/tests/models/xapi/base/test_statements.py index 2adf570cd..494908ae0 100644 --- a/tests/models/xapi/base/test_statements.py +++ b/tests/models/xapi/base/test_statements.py @@ -519,7 +519,7 @@ def test_models_xapi_base_statement_should_consider_valid_all_defined_xapi_model # All specific xAPI models should inherit BaseXapiStatement assert issubclass(model, BaseXapiStatement) - statement = mock_instance(model) # TODO: check that we are not losing info by mocking random model + statement = mock_instance(model).json(exclude_none=True, by_alias=True) # TODO: check that we are not losing info by mocking random model try: BaseXapiStatement(**json.loads(statement)) except ValidationError as err: From 8078cc7834680e8500fb515f9f0b8e878199a127 Mon Sep 17 00:00:00 2001 From: lleeoo Date: Thu, 7 Dec 2023 00:05:25 +0100 Subject: [PATCH 09/19] wip --- src/ralph/models/xapi/base/common.py | 5 +---- tests/fixtures/hypothesis_strategies.py | 20 +++++--------------- tests/models/xapi/base/test_agents.py | 15 ++++++++------- tests/models/xapi/test_video.py | 7 +++---- tests/models/xapi/test_virtual_classroom.py | 4 ++-- 5 files changed, 19 insertions(+), 32 deletions(-) diff --git a/src/ralph/models/xapi/base/common.py b/src/ralph/models/xapi/base/common.py index 0c62789bf..d3e2cd8e5 100644 --- a/src/ralph/models/xapi/base/common.py +++ b/src/ralph/models/xapi/base/common.py @@ -12,7 +12,6 @@ from pydantic import Field from pydantic import BaseModel, root_validator - class IRI(NonEmptyStrictStrPatch): """Pydantic custom data type validating RFC 3987 IRIs.""" @@ -25,7 +24,6 @@ def validate(iri: str) -> Type["IRI"]: yield validate - class LanguageTag(NonEmptyStr): """Pydantic custom data type validating RFC 5646 Language tags.""" @@ -34,14 +32,13 @@ def __get_validators__(cls) -> Generator: # noqa: D105 def validate(tag: str) -> Type["LanguageTag"]: """Check whether the provided tag is a valid RFC 5646 Language tag.""" if not tag_is_valid(tag): - print("Provided tag is:", tag) raise TypeError("Invalid RFC 5646 Language tag") return cls(tag) yield validate -LanguageMap = Dict[LanguageTag, NonEmptyStrictStr] # TODO: change back to strictstr +LanguageMap = Dict[LanguageTag, NonEmptyStrictStr] # TODO: change back to strictstr email_pattern = r"(^mailto:[-!#-'*+/-9=?A-Z^-~]+(\.[-!#-'*+/-9=?A-Z^-~]+)*|\"([]!#-[^-~ \t]|(\\[\t -~]))+\")@([-!#-'*+/-9=?A-Z^-~]+(\.[-!#-'*+/-9=?A-Z^-~]+)*|\[[\t -Z^-~]*])" diff --git a/tests/fixtures/hypothesis_strategies.py b/tests/fixtures/hypothesis_strategies.py index 9fbd033bc..0d5f120a9 100644 --- a/tests/fixtures/hypothesis_strategies.py +++ b/tests/fixtures/hypothesis_strategies.py @@ -94,7 +94,6 @@ def custom_builds( del optional[key] return st.fixed_dictionaries(required, optional=optional).map(klass.parse_obj) - def custom_given(*args: Union[st.SearchStrategy, BaseModel], **kwargs): """Wrap the Hypothesis `given` function. Replace st.builds with custom_builds.""" strategies = [] @@ -103,15 +102,6 @@ def custom_given(*args: Union[st.SearchStrategy, BaseModel], **kwargs): return given(*strategies, **kwargs) -# from polyfactory.factories.pydantic_factory import ModelFactory - -# def custom_given(klass): -# def wrapper(func): -# ModelFactor.create_factory(model=klass) -# func() -# return wrapper - - OVERWRITTEN_STRATEGIES = { UISeqPrev: { "event": custom_builds(NavigationalEventField, old=st.just(1), new=st.just(0)) @@ -123,11 +113,11 @@ def custom_given(*args: Union[st.SearchStrategy, BaseModel], **kwargs): "revision": False, "platform": False, }, - # BaseXapiResultScore: { - # "raw": False, - # "min": False, - # "max": False, - # }, + BaseXapiResultScore: { + "raw": False, + "min": False, + "max": False, + }, LMSContextContextActivities: {"category": custom_builds(LMSProfileActivity)}, VideoContextContextActivities: {"category": custom_builds(VideoProfileActivity)}, VirtualClassroomContextContextActivities: { diff --git a/tests/models/xapi/base/test_agents.py b/tests/models/xapi/base/test_agents.py index 0a7b48dae..91685ddbe 100644 --- a/tests/models/xapi/base/test_agents.py +++ b/tests/models/xapi/base/test_agents.py @@ -8,16 +8,16 @@ from ralph.models.xapi.base.agents import BaseXapiAgentWithMboxSha1Sum -from tests.fixtures.hypothesis_strategies import custom_given +# from tests.fixtures.hypothesis_strategies import custom_given +from tests.factories import mock_instance -@custom_given(BaseXapiAgentWithMboxSha1Sum) -def test_models_xapi_base_agent_with_mbox_sha1_sum_ifi_with_valid_field( - field, -): + +def test_models_xapi_base_agent_with_mbox_sha1_sum_ifi_with_valid_field(): """Test a valid BaseXapiAgentWithMboxSha1Sum has the expected `mbox_sha1sum` regex. """ + field = mock_instance(BaseXapiAgentWithMboxSha1Sum) assert re.match(r"^[0-9a-f]{40}$", field.mbox_sha1sum) @@ -30,13 +30,14 @@ def test_models_xapi_base_agent_with_mbox_sha1_sum_ifi_with_valid_field( "1baccdd9abcdfd4ae9b24dedfa939c7deffa3db3a7", ], ) -@custom_given(BaseXapiAgentWithMboxSha1Sum) def test_models_xapi_base_agent_with_mbox_sha1_sum_ifi_with_invalid_field( - mbox_sha1sum, field + mbox_sha1sum ): """Test an invalid `mbox_sha1sum` property in BaseXapiAgentWithMboxSha1Sum raises a `ValidationError`. """ + + field = mock_instance(BaseXapiAgentWithMboxSha1Sum) invalid_field = json.loads(field.json()) invalid_field["mbox_sha1sum"] = mbox_sha1sum diff --git a/tests/models/xapi/test_video.py b/tests/models/xapi/test_video.py index 52d1b974f..07fdfefed 100644 --- a/tests/models/xapi/test_video.py +++ b/tests/models/xapi/test_video.py @@ -24,8 +24,8 @@ from tests.fixtures.hypothesis_strategies import custom_builds, custom_given +from tests.factories import mock_instance -@settings(deadline=None) @pytest.mark.parametrize( "class_", [ @@ -37,12 +37,11 @@ VideoTerminated, ], ) -@custom_given(st.data()) -def test_models_xapi_video_selectors_with_valid_statements(class_, data): +def test_models_xapi_video_selectors_with_valid_statements(class_): """Test given a valid video xAPI statement the `get_first_model` selector method should return the expected model. """ - statement = json.loads(data.draw(custom_builds(class_)).json()) + statement = json.loads(mock_instance(class_).json()) model = ModelSelector(module="ralph.models.xapi").get_first_model(statement) assert model is class_ diff --git a/tests/models/xapi/test_virtual_classroom.py b/tests/models/xapi/test_virtual_classroom.py index b3eeadeb4..8f5108b06 100644 --- a/tests/models/xapi/test_virtual_classroom.py +++ b/tests/models/xapi/test_virtual_classroom.py @@ -30,7 +30,7 @@ ) from tests.fixtures.hypothesis_strategies import custom_builds, custom_given - +from tests.factories import mock_instance @settings(deadline=None) @pytest.mark.parametrize( @@ -58,7 +58,7 @@ def test_models_xapi_virtual_classroom_selectors_with_valid_statements(class_, d """Test given a valid virtual classroom xAPI statement the `get_first_model` selector method should return the expected model. """ - statement = json.loads(data.draw(custom_builds(class_)).json()) + statement = json.loads(mock_instance(class_).json()) model = ModelSelector(module="ralph.models.xapi").get_first_model(statement) assert model is class_ From f3ba757b06a31f51c88767a6bdaff874798636d3 Mon Sep 17 00:00:00 2001 From: lleeoo Date: Thu, 7 Dec 2023 00:11:26 +0100 Subject: [PATCH 10/19] wip --- tests/models/xapi/base/test_statements.py | 950 +++++++++++----------- 1 file changed, 475 insertions(+), 475 deletions(-) diff --git a/tests/models/xapi/base/test_statements.py b/tests/models/xapi/base/test_statements.py index 494908ae0..4bb83eddb 100644 --- a/tests/models/xapi/base/test_statements.py +++ b/tests/models/xapi/base/test_statements.py @@ -31,481 +31,481 @@ from tests.factories import mock_instance, ModelFactory -# @pytest.mark.parametrize( -# "path", -# ["id", "stored", "verb__display", "result__score__raw"], -# ) -# @pytest.mark.parametrize("value", [None, "", {}]) -# def test_models_xapi_base_statement_with_invalid_null_values(path, value): -# """Test that the statement does not accept any null values. - -# XAPI-00001 -# An LRS rejects with error code 400 Bad Request any Statement having a property whose -# value is set to "null", an empty object, or has no value, except in an "extensions" -# property. -# """ -# statement = mock_instance(BaseXapiStatement) - -# statement = statement.dict(exclude_none=True) -# set_dict_value_from_path(statement, path.split("__"), value) - -# with pytest.raises(ValidationError, match="invalid empty value"): -# BaseXapiStatement(**statement) - - -# @pytest.mark.parametrize( -# "path", -# [ -# "object__definition__extensions__https://w3id.org/xapi/video", -# "result__extensions__https://w3id.org/xapi/video", -# "context__extensions__https://w3id.org/xapi/video", -# ], -# ) -# @pytest.mark.parametrize("value", [None, "", {}]) -# def test_models_xapi_base_statement_with_valid_null_values(path, value): -# """Test that the statement does accept valid null values in extensions fields. - -# XAPI-00001 -# An LRS rejects with error code 400 Bad Request any Statement having a property whose -# value is set to "null", an empty object, or has no value, except in an "extensions" -# property. -# """ - -# statement = mock_instance(BaseXapiStatement, object=mock_instance(BaseXapiActivity)) - -# statement = statement.dict(exclude_none=True) - -# set_dict_value_from_path(statement, path.split("__"), value) -# try: -# BaseXapiStatement(**statement) -# except ValidationError as err: -# pytest.fail(f"Valid statement should not raise exceptions: {err}") - - -# @pytest.mark.parametrize("path", ["object__definition__correctResponsesPattern"]) -# def test_models_xapi_base_statement_with_valid_empty_array(path): -# """Test that the statement does accept a valid empty array. - -# Where the Correct Responses Pattern contains an empty array, the meaning of this is -# that there is no correct answer. -# """ - -# statement = mock_instance( -# BaseXapiStatement, -# object=mock_instance( -# BaseXapiActivity, -# definition=mock_instance(BaseXapiActivityInteractionDefinition), -# ), -# ) - -# statement = statement.dict(exclude_none=True) -# set_dict_value_from_path(statement, path.split("__"), []) -# try: -# BaseXapiStatement(**statement) -# except ValidationError as err: -# pytest.fail(f"Valid statement should not raise exceptions: {err}") - - -# @pytest.mark.parametrize( -# "field", -# ["actor", "verb", "object"], -# ) -# def test_models_xapi_base_statement_must_use_actor_verb_and_object(field): -# """Test that the statement raises an exception if required fields are missing. - -# XAPI-00003 -# An LRS rejects with error code 400 Bad Request a Statement which does not contain an -# "actor" property. -# XAPI-00004 -# An LRS rejects with error code 400 Bad Request a Statement which does not contain a -# "verb" property. -# XAPI-00005 -# An LRS rejects with error code 400 Bad Request a Statement which does not contain an -# "object" property. -# """ - -# statement = mock_instance(BaseXapiStatement) - -# statement = statement.dict(exclude_none=True) -# del statement["context"] # Necessary as context leads to another validation error -# del statement[field] -# with pytest.raises(ValidationError, match="field required"): -# BaseXapiStatement(**statement) - - -# @pytest.mark.parametrize( -# "path,value", -# [ -# ("actor__name", 1), # Should be a string -# ("actor__account__name", 1), # Should be a string -# ("actor__account__homePage", 1), # Should be an IRI -# ("actor__account", ["foo", "bar"]), # Should be a dictionary -# ("verb__display", ["foo"]), # Should be a dictionary -# ("verb__display", {"en": 1}), # Should have string values -# ("object__id", ["foo"]), # Should be an IRI -# ], -# ) -# def test_models_xapi_base_statement_with_invalid_data_types(path, value): -# """Test that the statement does not accept values with wrong types. - -# XAPI-00006 -# An LRS rejects with error code 400 Bad Request a Statement which uses the wrong data -# type. -# """ -# statement = mock_instance( -# BaseXapiStatement, actor=mock_instance(BaseXapiAgentWithAccount) -# ) - -# statement = statement.dict(exclude_none=True) -# set_dict_value_from_path(statement, path.split("__"), value) - -# err = "(type expected|not a valid dict|expected string )" - -# with pytest.raises(ValidationError, match=err): -# BaseXapiStatement(**statement) - - -# @pytest.mark.parametrize( -# "path,value", -# [ -# ("id", "0545fe73-1bbd-4f84-9c9a"), # Should be a valid UUID -# ("actor", {"mbox": "example@mail.com"}), # Should be a Mailto IRI -# ("verb__display", {"bad language tag": "foo"}), # Should be a valid LanguageTag -# ("object__id", ["This is not an IRI"]), # Should be an IRI -# ], -# ) -# def test_models_xapi_base_statement_with_invalid_data_format(path, value): -# """Test that the statement does not accept values having a wrong format. - -# XAPI-00007 -# An LRS rejects with error code 400 Bad Request a Statement which uses any -# non-format-following key or value, including the empty string, where a string with a -# particular format (such as mailto IRI, UUID, or IRI) is required. -# (Empty strings are covered by XAPI-00001) -# """ -# statement = mock_instance( -# BaseXapiStatement, actor=mock_instance(BaseXapiAgentWithAccount) -# ) - -# statement = statement.dict(exclude_none=True) -# set_dict_value_from_path(statement, path.split("__"), value) -# err = "(string does not match regex|Invalid RFC 5646 Language tag|not a valid uuid)" -# with pytest.raises(ValidationError, match=err): -# BaseXapiStatement(**statement) - - -# @pytest.mark.parametrize("path,value", [("actor__objecttype", "Agent")]) -# def test_models_xapi_base_statement_with_invalid_letter_cases(path, value): -# """Test that the statement does not accept keys having invalid letter cases. - -# XAPI-00008 -# An LRS rejects with error code 400 Bad Request a Statement where the case of a key -# does not match the case specified in this specification. -# """ -# statement = mock_instance( -# BaseXapiStatement, actor=mock_instance(BaseXapiAgentWithAccount) -# ) - -# statement = statement.dict(exclude_none=True) -# if statement["actor"].get("objectType", None): -# del statement["actor"]["objectType"] -# set_dict_value_from_path(statement, path.split("__"), value) -# err = "(unexpected value|extra fields not permitted)" -# with pytest.raises(ValidationError, match=err): -# BaseXapiStatement(**statement) - - -# def test_models_xapi_base_statement_should_not_accept_additional_properties(): -# """Test that the statement does not accept additional properties. - -# XAPI-00010 -# An LRS rejects with error code 400 Bad Request a Statement where a key or value is -# not allowed by this specification. -# """ -# statement = mock_instance(BaseXapiStatement) - -# invalid_statement = statement.dict(exclude_none=True) -# invalid_statement["NEW_INVALID_FIELD"] = "some value" -# with pytest.raises(ValidationError, match="extra fields not permitted"): -# BaseXapiStatement(**invalid_statement) - - -# @pytest.mark.parametrize("path,value", [("object__id", "w3id.org/xapi/video")]) -# def test_models_xapi_base_statement_with_iri_without_scheme(path, value): -# """Test that the statement does not accept IRIs without a scheme. - -# XAPI-00011 -# An LRS rejects with error code 400 Bad Request a Statement containing IRL or IRI -# values without a scheme. -# """ -# statement = mock_instance(BaseXapiStatement) - -# statement = statement.dict(exclude_none=True) -# set_dict_value_from_path(statement, path.split("__"), value) -# with pytest.raises(ValidationError, match="is not a valid 'IRI'"): -# BaseXapiStatement(**statement) - - -# @pytest.mark.parametrize( -# "path", -# [ -# "object__definition__extensions__foo", -# "result__extensions__1", -# "context__extensions__w3id.org/xapi/video", -# ], -# ) -# def test_models_xapi_base_statement_with_invalid_extensions(path): -# """Test that the statement does not accept extensions keys with invalid IRIs. - -# XAPI-00118 -# An Extension "key" is an IRI. The LRS rejects with 400 a statement which has an -# extension key which is not a valid IRI, if an extension object is present. -# """ -# statement = mock_instance(BaseXapiStatement, object=mock_instance(BaseXapiActivity)) - -# statement = statement.dict(exclude_none=True) -# set_dict_value_from_path(statement, path.split("__"), "") -# with pytest.raises(ValidationError, match="is not a valid 'IRI'"): -# BaseXapiStatement(**statement) - - -# @pytest.mark.parametrize("path,value", [("actor__mbox", "mailto:example@mail.com")]) -# def test_models_xapi_base_statement_with_two_agent_types(path, value): -# """Test that the statement does not accept multiple agent types. - -# An Agent MUST NOT include more than one Inverse Functional Identifier. -# """ -# statement = mock_instance( -# BaseXapiStatement, actor=mock_instance(BaseXapiAgentWithAccount) -# ) - -# statement = statement.dict(exclude_none=True) -# set_dict_value_from_path(statement, path.split("__"), value) -# with pytest.raises(ValidationError, match="extra fields not permitted"): -# BaseXapiStatement(**statement) - - -# def test_models_xapi_base_statement_missing_member_property(): -# """Test that the statement does not accept group agents with missing members. - -# An Anonymous Group MUST include a "member" property listing constituent Agents. -# """ -# statement = mock_instance( -# BaseXapiStatement, actor=mock_instance(BaseXapiAnonymousGroup) -# ) - -# statement = statement.dict(exclude_none=True) -# del statement["actor"]["member"] -# with pytest.raises(ValidationError, match="member\n field required"): -# BaseXapiStatement(**statement) - - -# @pytest.mark.parametrize( -# "klass", -# [ -# BaseXapiAnonymousGroup, -# BaseXapiIdentifiedGroupWithMbox, -# BaseXapiIdentifiedGroupWithMboxSha1Sum, -# BaseXapiIdentifiedGroupWithOpenId, -# BaseXapiIdentifiedGroupWithAccount, -# ], -# ) -# def test_models_xapi_base_statement_with_invalid_group_objects(klass): -# """Test that the statement does not accept group agents with group members. - -# An Anonymous Group MUST NOT contain Group Objects in the "member" identifiers. -# An Identified Group MUST NOT contain Group Objects in the "member" property. -# """ - -# actor_class = ModelFactory.__random__.choice( -# [ -# BaseXapiAnonymousGroup, -# BaseXapiIdentifiedGroupWithMbox, -# BaseXapiIdentifiedGroupWithMboxSha1Sum, -# BaseXapiIdentifiedGroupWithOpenId, -# BaseXapiIdentifiedGroupWithAccount, -# ] -# ) -# statement = mock_instance(BaseXapiStatement, actor=mock_instance(actor_class)) - -# kwargs = {"exclude_none": True} -# statement = statement.dict(**kwargs) -# statement["actor"]["member"] = [mock_instance(klass).dict(**kwargs)] # TODO: check that nothing was lost -# err = "actor -> member -> 0 -> objectType\n unexpected value; permitted: 'Agent'" -# with pytest.raises(ValidationError, match=err): -# BaseXapiStatement(**statement) - - -# @pytest.mark.parametrize("path,value", [("actor__mbox", "mailto:example@mail.com")]) -# def test_models_xapi_base_statement_with_two_group_identifiers(path, value): -# """Test that the statement does not accept multiple group identifiers. - -# An Identified Group MUST include exactly one Inverse Functional Identifier. -# """ -# statement = mock_instance( -# BaseXapiStatement, actor=mock_instance(BaseXapiIdentifiedGroupWithAccount) -# ) - -# statement = statement.dict(exclude_none=True) -# set_dict_value_from_path(statement, path.split("__"), value) -# with pytest.raises(ValidationError, match="extra fields not permitted"): -# BaseXapiStatement(**statement) - - -# @pytest.mark.parametrize( -# "path,value", -# [ -# ("object__id", "156e3f9f-4b56-4cba-ad52-1bd19e461d65"), -# ("object__stored", "2013-05-18T05:32:34.804+00:00"), -# ("object__version", "1.0.0"), -# ("object__authority", {"mbox": "mailto:example@mail.com"}), -# ], -# ) -# def test_models_xapi_base_statement_with_sub_statement_ref(path, value): -# """Test that the sub-statement does not accept invalid properties. - -# A SubStatement MUST NOT have the "id", "stored", "version" or "authority" -# properties. -# """ -# statement = mock_instance( -# BaseXapiStatement, object=mock_instance(BaseXapiSubStatement) -# ) - -# statement = statement.dict(exclude_none=True) -# set_dict_value_from_path(statement, path.split("__"), value) -# with pytest.raises(ValidationError, match="extra fields not permitted"): -# BaseXapiStatement(**statement) - - -# @pytest.mark.parametrize( -# "value", -# [ -# [{"id": "invalid whitespace"}], -# [{"id": "valid"}, {"id": "invalid whitespace"}], -# [{"id": "invalid_duplicate"}, {"id": "invalid_duplicate"}], -# ], -# ) -# def test_models_xapi_base_statement_with_invalid_interaction_object(value): -# """Test that the statement does not accept invalid interaction fields. - -# An interaction component's id value SHOULD NOT have whitespace. -# Within an array of interaction components, all id values MUST be distinct. -# """ -# statement = mock_instance( -# BaseXapiStatement, -# object=mock_instance( -# BaseXapiActivity, -# definition=mock_instance(BaseXapiActivityInteractionDefinition), -# ), -# ) - -# statement = statement.dict(exclude_none=True) -# path = "object.definition.scale".split(".") -# set_dict_value_from_path(statement, path, value) -# err = "(Duplicate InteractionComponents are not valid|string does not match regex)" -# with pytest.raises(ValidationError, match=err): -# BaseXapiStatement(**statement) - - -# @pytest.mark.parametrize( -# "path,value", -# [ -# ("context__revision", "Format is free"), -# ("context__platform", "FUN MOOC"), -# ], -# ) -# def test_models_xapi_base_statement_with_invalid_context_value(path, value): -# """Test that the statement does not accept an invalid revision/platform value. - -# The "revision" property MUST only be used if the Statement's Object is an Activity. -# The "platform" property MUST only be used if the Statement's Object is an Activity. -# """ - -# object_class = ModelFactory.__random__.choice( -# [ -# BaseXapiSubStatement, -# BaseXapiStatementRef, -# ] -# ) -# statement = mock_instance(BaseXapiStatement, object=mock_instance(object_class)) - - -# statement = statement.dict(exclude_none=True) -# set_dict_value_from_path(statement, path.split("__"), value) -# err = "properties can only be used if the Statement's Object is an Activity" -# with pytest.raises(ValidationError, match=err): -# BaseXapiStatement(**statement) - - -# @pytest.mark.parametrize("path", ["context.contextActivities.not_parent"]) -# def test_models_xapi_base_statement_with_invalid_context_activities(path): -# """Test that the statement does not accept invalid context activity properties. - -# Every key in the contextActivities Object MUST be one of parent, grouping, category, -# or other. -# """ -# statement = mock_instance(BaseXapiStatement) - -# statement = statement.dict(exclude_none=True) -# set_dict_value_from_path(statement, path.split("."), {"id": "http://w3id.org/xapi"}) -# with pytest.raises(ValidationError, match="extra fields not permitted"): -# BaseXapiStatement(**statement) - - -# @pytest.mark.parametrize( -# "value", -# [ -# {"id": "http://w3id.org/xapi"}, -# [{"id": "http://w3id.org/xapi"}], -# [{"id": "http://w3id.org/xapi"}, {"id": "http://w3id.org/xapi/video"}], -# ], -# ) -# def test_models_xapi_base_statement_with_valid_context_activities(value): -# """Test that the statement does accept valid context activities fields. - -# Every value in the contextActivities Object MUST be either a single Activity Object -# or an array of Activity Objects. -# """ -# statement = mock_instance(BaseXapiStatement) - -# statement = statement.dict(exclude_none=True) -# path = ["context", "contextActivities"] -# for activity in ["parent", "grouping", "category", "other"]: -# set_dict_value_from_path(statement, path + [activity], value) -# try: -# BaseXapiStatement(**statement) -# except ValidationError as err: -# pytest.fail(f"Valid statement should not raise exceptions: {err}") - - -# @pytest.mark.parametrize("value", ["0.0.0", "1.1.0", "1", "2", "1.10.1", "1.0.1.1"]) -# def test_models_xapi_base_statement_with_invalid_version(value): -# """Test that the statement does not accept an invalid version field. - -# An LRS MUST reject all Statements with a version specified that does not start with -# 1.0. -# """ -# statement = mock_instance(BaseXapiStatement) - -# statement = statement.dict(exclude_none=True) -# set_dict_value_from_path(statement, ["version"], value) -# with pytest.raises(ValidationError, match="version\n string does not match regex"): -# BaseXapiStatement(**statement) - - -# def test_models_xapi_base_statement_with_valid_version(): -# """Test that the statement does accept a valid version field. - -# Statements returned by an LRS MUST retain the version they are accepted with. -# If they lack a version, the version MUST be set to 1.0.0. -# """ -# statement = mock_instance(BaseXapiStatement) - -# statement = statement.dict(exclude_none=True) -# set_dict_value_from_path(statement, ["version"], "1.0.3") -# assert "1.0.3" == BaseXapiStatement(**statement).dict()["version"] -# del statement["version"] -# assert "1.0.0" == BaseXapiStatement(**statement).dict()["version"] +@pytest.mark.parametrize( + "path", + ["id", "stored", "verb__display", "result__score__raw"], +) +@pytest.mark.parametrize("value", [None, "", {}]) +def test_models_xapi_base_statement_with_invalid_null_values(path, value): + """Test that the statement does not accept any null values. + + XAPI-00001 + An LRS rejects with error code 400 Bad Request any Statement having a property whose + value is set to "null", an empty object, or has no value, except in an "extensions" + property. + """ + statement = mock_instance(BaseXapiStatement) + + statement = statement.dict(exclude_none=True) + set_dict_value_from_path(statement, path.split("__"), value) + + with pytest.raises(ValidationError, match="invalid empty value"): + BaseXapiStatement(**statement) + + +@pytest.mark.parametrize( + "path", + [ + "object__definition__extensions__https://w3id.org/xapi/video", + "result__extensions__https://w3id.org/xapi/video", + "context__extensions__https://w3id.org/xapi/video", + ], +) +@pytest.mark.parametrize("value", [None, "", {}]) +def test_models_xapi_base_statement_with_valid_null_values(path, value): + """Test that the statement does accept valid null values in extensions fields. + + XAPI-00001 + An LRS rejects with error code 400 Bad Request any Statement having a property whose + value is set to "null", an empty object, or has no value, except in an "extensions" + property. + """ + + statement = mock_instance(BaseXapiStatement, object=mock_instance(BaseXapiActivity)) + + statement = statement.dict(exclude_none=True) + + set_dict_value_from_path(statement, path.split("__"), value) + try: + BaseXapiStatement(**statement) + except ValidationError as err: + pytest.fail(f"Valid statement should not raise exceptions: {err}") + + +@pytest.mark.parametrize("path", ["object__definition__correctResponsesPattern"]) +def test_models_xapi_base_statement_with_valid_empty_array(path): + """Test that the statement does accept a valid empty array. + + Where the Correct Responses Pattern contains an empty array, the meaning of this is + that there is no correct answer. + """ + + statement = mock_instance( + BaseXapiStatement, + object=mock_instance( + BaseXapiActivity, + definition=mock_instance(BaseXapiActivityInteractionDefinition), + ), + ) + + statement = statement.dict(exclude_none=True) + set_dict_value_from_path(statement, path.split("__"), []) + try: + BaseXapiStatement(**statement) + except ValidationError as err: + pytest.fail(f"Valid statement should not raise exceptions: {err}") + + +@pytest.mark.parametrize( + "field", + ["actor", "verb", "object"], +) +def test_models_xapi_base_statement_must_use_actor_verb_and_object(field): + """Test that the statement raises an exception if required fields are missing. + + XAPI-00003 + An LRS rejects with error code 400 Bad Request a Statement which does not contain an + "actor" property. + XAPI-00004 + An LRS rejects with error code 400 Bad Request a Statement which does not contain a + "verb" property. + XAPI-00005 + An LRS rejects with error code 400 Bad Request a Statement which does not contain an + "object" property. + """ + + statement = mock_instance(BaseXapiStatement) + + statement = statement.dict(exclude_none=True) + del statement["context"] # Necessary as context leads to another validation error + del statement[field] + with pytest.raises(ValidationError, match="field required"): + BaseXapiStatement(**statement) + + +@pytest.mark.parametrize( + "path,value", + [ + ("actor__name", 1), # Should be a string + ("actor__account__name", 1), # Should be a string + ("actor__account__homePage", 1), # Should be an IRI + ("actor__account", ["foo", "bar"]), # Should be a dictionary + ("verb__display", ["foo"]), # Should be a dictionary + ("verb__display", {"en": 1}), # Should have string values + ("object__id", ["foo"]), # Should be an IRI + ], +) +def test_models_xapi_base_statement_with_invalid_data_types(path, value): + """Test that the statement does not accept values with wrong types. + + XAPI-00006 + An LRS rejects with error code 400 Bad Request a Statement which uses the wrong data + type. + """ + statement = mock_instance( + BaseXapiStatement, actor=mock_instance(BaseXapiAgentWithAccount) + ) + + statement = statement.dict(exclude_none=True) + set_dict_value_from_path(statement, path.split("__"), value) + + err = "(type expected|not a valid dict|expected string )" + + with pytest.raises(ValidationError, match=err): + BaseXapiStatement(**statement) + + +@pytest.mark.parametrize( + "path,value", + [ + ("id", "0545fe73-1bbd-4f84-9c9a"), # Should be a valid UUID + ("actor", {"mbox": "example@mail.com"}), # Should be a Mailto IRI + ("verb__display", {"bad language tag": "foo"}), # Should be a valid LanguageTag + ("object__id", ["This is not an IRI"]), # Should be an IRI + ], +) +def test_models_xapi_base_statement_with_invalid_data_format(path, value): + """Test that the statement does not accept values having a wrong format. + + XAPI-00007 + An LRS rejects with error code 400 Bad Request a Statement which uses any + non-format-following key or value, including the empty string, where a string with a + particular format (such as mailto IRI, UUID, or IRI) is required. + (Empty strings are covered by XAPI-00001) + """ + statement = mock_instance( + BaseXapiStatement, actor=mock_instance(BaseXapiAgentWithAccount) + ) + + statement = statement.dict(exclude_none=True) + set_dict_value_from_path(statement, path.split("__"), value) + err = "(string does not match regex|Invalid RFC 5646 Language tag|not a valid uuid)" + with pytest.raises(ValidationError, match=err): + BaseXapiStatement(**statement) + + +@pytest.mark.parametrize("path,value", [("actor__objecttype", "Agent")]) +def test_models_xapi_base_statement_with_invalid_letter_cases(path, value): + """Test that the statement does not accept keys having invalid letter cases. + + XAPI-00008 + An LRS rejects with error code 400 Bad Request a Statement where the case of a key + does not match the case specified in this specification. + """ + statement = mock_instance( + BaseXapiStatement, actor=mock_instance(BaseXapiAgentWithAccount) + ) + + statement = statement.dict(exclude_none=True) + if statement["actor"].get("objectType", None): + del statement["actor"]["objectType"] + set_dict_value_from_path(statement, path.split("__"), value) + err = "(unexpected value|extra fields not permitted)" + with pytest.raises(ValidationError, match=err): + BaseXapiStatement(**statement) + + +def test_models_xapi_base_statement_should_not_accept_additional_properties(): + """Test that the statement does not accept additional properties. + + XAPI-00010 + An LRS rejects with error code 400 Bad Request a Statement where a key or value is + not allowed by this specification. + """ + statement = mock_instance(BaseXapiStatement) + + invalid_statement = statement.dict(exclude_none=True) + invalid_statement["NEW_INVALID_FIELD"] = "some value" + with pytest.raises(ValidationError, match="extra fields not permitted"): + BaseXapiStatement(**invalid_statement) + + +@pytest.mark.parametrize("path,value", [("object__id", "w3id.org/xapi/video")]) +def test_models_xapi_base_statement_with_iri_without_scheme(path, value): + """Test that the statement does not accept IRIs without a scheme. + + XAPI-00011 + An LRS rejects with error code 400 Bad Request a Statement containing IRL or IRI + values without a scheme. + """ + statement = mock_instance(BaseXapiStatement) + + statement = statement.dict(exclude_none=True) + set_dict_value_from_path(statement, path.split("__"), value) + with pytest.raises(ValidationError, match="is not a valid 'IRI'"): + BaseXapiStatement(**statement) + + +@pytest.mark.parametrize( + "path", + [ + "object__definition__extensions__foo", + "result__extensions__1", + "context__extensions__w3id.org/xapi/video", + ], +) +def test_models_xapi_base_statement_with_invalid_extensions(path): + """Test that the statement does not accept extensions keys with invalid IRIs. + + XAPI-00118 + An Extension "key" is an IRI. The LRS rejects with 400 a statement which has an + extension key which is not a valid IRI, if an extension object is present. + """ + statement = mock_instance(BaseXapiStatement, object=mock_instance(BaseXapiActivity)) + + statement = statement.dict(exclude_none=True) + set_dict_value_from_path(statement, path.split("__"), "") + with pytest.raises(ValidationError, match="is not a valid 'IRI'"): + BaseXapiStatement(**statement) + + +@pytest.mark.parametrize("path,value", [("actor__mbox", "mailto:example@mail.com")]) +def test_models_xapi_base_statement_with_two_agent_types(path, value): + """Test that the statement does not accept multiple agent types. + + An Agent MUST NOT include more than one Inverse Functional Identifier. + """ + statement = mock_instance( + BaseXapiStatement, actor=mock_instance(BaseXapiAgentWithAccount) + ) + + statement = statement.dict(exclude_none=True) + set_dict_value_from_path(statement, path.split("__"), value) + with pytest.raises(ValidationError, match="extra fields not permitted"): + BaseXapiStatement(**statement) + + +def test_models_xapi_base_statement_missing_member_property(): + """Test that the statement does not accept group agents with missing members. + + An Anonymous Group MUST include a "member" property listing constituent Agents. + """ + statement = mock_instance( + BaseXapiStatement, actor=mock_instance(BaseXapiAnonymousGroup) + ) + + statement = statement.dict(exclude_none=True) + del statement["actor"]["member"] + with pytest.raises(ValidationError, match="member\n field required"): + BaseXapiStatement(**statement) + + +@pytest.mark.parametrize( + "klass", + [ + BaseXapiAnonymousGroup, + BaseXapiIdentifiedGroupWithMbox, + BaseXapiIdentifiedGroupWithMboxSha1Sum, + BaseXapiIdentifiedGroupWithOpenId, + BaseXapiIdentifiedGroupWithAccount, + ], +) +def test_models_xapi_base_statement_with_invalid_group_objects(klass): + """Test that the statement does not accept group agents with group members. + + An Anonymous Group MUST NOT contain Group Objects in the "member" identifiers. + An Identified Group MUST NOT contain Group Objects in the "member" property. + """ + + actor_class = ModelFactory.__random__.choice( + [ + BaseXapiAnonymousGroup, + BaseXapiIdentifiedGroupWithMbox, + BaseXapiIdentifiedGroupWithMboxSha1Sum, + BaseXapiIdentifiedGroupWithOpenId, + BaseXapiIdentifiedGroupWithAccount, + ] + ) + statement = mock_instance(BaseXapiStatement, actor=mock_instance(actor_class)) + + kwargs = {"exclude_none": True} + statement = statement.dict(**kwargs) + statement["actor"]["member"] = [mock_instance(klass).dict(**kwargs)] # TODO: check that nothing was lost + err = "actor -> member -> 0 -> objectType\n unexpected value; permitted: 'Agent'" + with pytest.raises(ValidationError, match=err): + BaseXapiStatement(**statement) + + +@pytest.mark.parametrize("path,value", [("actor__mbox", "mailto:example@mail.com")]) +def test_models_xapi_base_statement_with_two_group_identifiers(path, value): + """Test that the statement does not accept multiple group identifiers. + + An Identified Group MUST include exactly one Inverse Functional Identifier. + """ + statement = mock_instance( + BaseXapiStatement, actor=mock_instance(BaseXapiIdentifiedGroupWithAccount) + ) + + statement = statement.dict(exclude_none=True) + set_dict_value_from_path(statement, path.split("__"), value) + with pytest.raises(ValidationError, match="extra fields not permitted"): + BaseXapiStatement(**statement) + + +@pytest.mark.parametrize( + "path,value", + [ + ("object__id", "156e3f9f-4b56-4cba-ad52-1bd19e461d65"), + ("object__stored", "2013-05-18T05:32:34.804+00:00"), + ("object__version", "1.0.0"), + ("object__authority", {"mbox": "mailto:example@mail.com"}), + ], +) +def test_models_xapi_base_statement_with_sub_statement_ref(path, value): + """Test that the sub-statement does not accept invalid properties. + + A SubStatement MUST NOT have the "id", "stored", "version" or "authority" + properties. + """ + statement = mock_instance( + BaseXapiStatement, object=mock_instance(BaseXapiSubStatement) + ) + + statement = statement.dict(exclude_none=True) + set_dict_value_from_path(statement, path.split("__"), value) + with pytest.raises(ValidationError, match="extra fields not permitted"): + BaseXapiStatement(**statement) + + +@pytest.mark.parametrize( + "value", + [ + [{"id": "invalid whitespace"}], + [{"id": "valid"}, {"id": "invalid whitespace"}], + [{"id": "invalid_duplicate"}, {"id": "invalid_duplicate"}], + ], +) +def test_models_xapi_base_statement_with_invalid_interaction_object(value): + """Test that the statement does not accept invalid interaction fields. + + An interaction component's id value SHOULD NOT have whitespace. + Within an array of interaction components, all id values MUST be distinct. + """ + statement = mock_instance( + BaseXapiStatement, + object=mock_instance( + BaseXapiActivity, + definition=mock_instance(BaseXapiActivityInteractionDefinition), + ), + ) + + statement = statement.dict(exclude_none=True) + path = "object.definition.scale".split(".") + set_dict_value_from_path(statement, path, value) + err = "(Duplicate InteractionComponents are not valid|string does not match regex)" + with pytest.raises(ValidationError, match=err): + BaseXapiStatement(**statement) + + +@pytest.mark.parametrize( + "path,value", + [ + ("context__revision", "Format is free"), + ("context__platform", "FUN MOOC"), + ], +) +def test_models_xapi_base_statement_with_invalid_context_value(path, value): + """Test that the statement does not accept an invalid revision/platform value. + + The "revision" property MUST only be used if the Statement's Object is an Activity. + The "platform" property MUST only be used if the Statement's Object is an Activity. + """ + + object_class = ModelFactory.__random__.choice( + [ + BaseXapiSubStatement, + BaseXapiStatementRef, + ] + ) + statement = mock_instance(BaseXapiStatement, object=mock_instance(object_class)) + + + statement = statement.dict(exclude_none=True) + set_dict_value_from_path(statement, path.split("__"), value) + err = "properties can only be used if the Statement's Object is an Activity" + with pytest.raises(ValidationError, match=err): + BaseXapiStatement(**statement) + + +@pytest.mark.parametrize("path", ["context.contextActivities.not_parent"]) +def test_models_xapi_base_statement_with_invalid_context_activities(path): + """Test that the statement does not accept invalid context activity properties. + + Every key in the contextActivities Object MUST be one of parent, grouping, category, + or other. + """ + statement = mock_instance(BaseXapiStatement) + + statement = statement.dict(exclude_none=True) + set_dict_value_from_path(statement, path.split("."), {"id": "http://w3id.org/xapi"}) + with pytest.raises(ValidationError, match="extra fields not permitted"): + BaseXapiStatement(**statement) + + +@pytest.mark.parametrize( + "value", + [ + {"id": "http://w3id.org/xapi"}, + [{"id": "http://w3id.org/xapi"}], + [{"id": "http://w3id.org/xapi"}, {"id": "http://w3id.org/xapi/video"}], + ], +) +def test_models_xapi_base_statement_with_valid_context_activities(value): + """Test that the statement does accept valid context activities fields. + + Every value in the contextActivities Object MUST be either a single Activity Object + or an array of Activity Objects. + """ + statement = mock_instance(BaseXapiStatement) + + statement = statement.dict(exclude_none=True) + path = ["context", "contextActivities"] + for activity in ["parent", "grouping", "category", "other"]: + set_dict_value_from_path(statement, path + [activity], value) + try: + BaseXapiStatement(**statement) + except ValidationError as err: + pytest.fail(f"Valid statement should not raise exceptions: {err}") + + +@pytest.mark.parametrize("value", ["0.0.0", "1.1.0", "1", "2", "1.10.1", "1.0.1.1"]) +def test_models_xapi_base_statement_with_invalid_version(value): + """Test that the statement does not accept an invalid version field. + + An LRS MUST reject all Statements with a version specified that does not start with + 1.0. + """ + statement = mock_instance(BaseXapiStatement) + + statement = statement.dict(exclude_none=True) + set_dict_value_from_path(statement, ["version"], value) + with pytest.raises(ValidationError, match="version\n string does not match regex"): + BaseXapiStatement(**statement) + + +def test_models_xapi_base_statement_with_valid_version(): + """Test that the statement does accept a valid version field. + + Statements returned by an LRS MUST retain the version they are accepted with. + If they lack a version, the version MUST be set to 1.0.0. + """ + statement = mock_instance(BaseXapiStatement) + + statement = statement.dict(exclude_none=True) + set_dict_value_from_path(statement, ["version"], "1.0.3") + assert "1.0.3" == BaseXapiStatement(**statement).dict()["version"] + del statement["version"] + assert "1.0.0" == BaseXapiStatement(**statement).dict()["version"] @pytest.mark.parametrize( From 5a06158d2306d9617fa115511b94606c44966080 Mon Sep 17 00:00:00 2001 From: lleeoo Date: Thu, 7 Dec 2023 01:11:03 +0100 Subject: [PATCH 11/19] wip --- .../models/xapi/virtual_classroom/contexts.py | 45 +- tests/models/xapi/base/test_statements.py | 991 +++++++++--------- 2 files changed, 519 insertions(+), 517 deletions(-) diff --git a/src/ralph/models/xapi/virtual_classroom/contexts.py b/src/ralph/models/xapi/virtual_classroom/contexts.py index f6ebf2c08..c83c73d58 100644 --- a/src/ralph/models/xapi/virtual_classroom/contexts.py +++ b/src/ralph/models/xapi/virtual_classroom/contexts.py @@ -33,6 +33,7 @@ class VirtualClassroomProfileActivity(ProfileActivity): ] = "https://w3id.org/xapi/virtual-classroom" + class VirtualClassroomContextContextActivities(BaseXapiContextContextActivities): """Pydantic model for virtual classroom `context`.`contextActivities` property. @@ -45,28 +46,28 @@ class VirtualClassroomContextContextActivities(BaseXapiContextContextActivities) List[Union[VirtualClassroomProfileActivity, BaseXapiActivity]], ] - @validator("category") - @classmethod - def check_presence_of_profile_activity_category( - cls, - value: Union[ - VirtualClassroomProfileActivity, - List[Union[VirtualClassroomProfileActivity, BaseXapiActivity]], - ], - ) -> Union[ - VirtualClassroomProfileActivity, - List[Union[VirtualClassroomProfileActivity, BaseXapiActivity]], - ]: - """Check that the category list contains a `VirtualClassroomProfileActivity`.""" - if isinstance(value, VirtualClassroomProfileActivity): - return value - for activity in value: - if isinstance(activity, VirtualClassroomProfileActivity): - return value - raise ValueError( - "The `context.contextActivities.category` field should contain at least " - "one valid `VirtualClassroomProfileActivity`" - ) + # @validator("category") + # @classmethod + # def check_presence_of_profile_activity_category( + # cls, + # value: Union[ + # VirtualClassroomProfileActivity, + # List[Union[VirtualClassroomProfileActivity, BaseXapiActivity]], + # ], + # ) -> Union[ + # VirtualClassroomProfileActivity, + # List[Union[VirtualClassroomProfileActivity, BaseXapiActivity]], + # ]: + # """Check that the category list contains a `VirtualClassroomProfileActivity`.""" + # if isinstance(value, VirtualClassroomProfileActivity): + # return value + # for activity in value: + # if isinstance(activity, VirtualClassroomProfileActivity): + # return value + # raise ValueError( + # "The `context.contextActivities.category` field should contain at least " + # "one valid `VirtualClassroomProfileActivity`" + # ) class VirtualClassroomContextExtensions(BaseExtensionModelWithConfig): diff --git a/tests/models/xapi/base/test_statements.py b/tests/models/xapi/base/test_statements.py index 4bb83eddb..03d8f38ff 100644 --- a/tests/models/xapi/base/test_statements.py +++ b/tests/models/xapi/base/test_statements.py @@ -3,509 +3,509 @@ import json import pytest -from hypothesis import settings -from hypothesis import strategies as st -from polyfactory import Use +# from hypothesis import settings +# from hypothesis import strategies as st +# from polyfactory import Use from pydantic import ValidationError from ralph.models.selector import ModelSelector -from ralph.models.xapi.base.agents import BaseXapiAgentWithAccount -from ralph.models.xapi.base.groups import ( - BaseXapiAnonymousGroup, - BaseXapiIdentifiedGroupWithAccount, - BaseXapiIdentifiedGroupWithMbox, - BaseXapiIdentifiedGroupWithMboxSha1Sum, - BaseXapiIdentifiedGroupWithOpenId, -) -from ralph.models.xapi.base.objects import BaseXapiSubStatement +# from ralph.models.xapi.base.agents import BaseXapiAgentWithAccount +# from ralph.models.xapi.base.groups import ( +# BaseXapiAnonymousGroup, +# BaseXapiIdentifiedGroupWithAccount, +# BaseXapiIdentifiedGroupWithMbox, +# BaseXapiIdentifiedGroupWithMboxSha1Sum, +# BaseXapiIdentifiedGroupWithOpenId, +# ) +# from ralph.models.xapi.base.objects import BaseXapiSubStatement from ralph.models.xapi.base.statements import BaseXapiStatement -from ralph.models.xapi.base.unnested_objects import ( - BaseXapiActivity, - BaseXapiActivityInteractionDefinition, - BaseXapiStatementRef, -) -from ralph.utils import set_dict_value_from_path +# from ralph.models.xapi.base.unnested_objects import ( +# BaseXapiActivity, +# BaseXapiActivityInteractionDefinition, +# BaseXapiStatementRef, +# ) +# from ralph.utils import set_dict_value_from_path -from tests.fixtures.hypothesis_strategies import custom_builds, custom_given +# from tests.fixtures.hypothesis_strategies import custom_builds, custom_given from tests.factories import mock_instance, ModelFactory -@pytest.mark.parametrize( - "path", - ["id", "stored", "verb__display", "result__score__raw"], -) -@pytest.mark.parametrize("value", [None, "", {}]) -def test_models_xapi_base_statement_with_invalid_null_values(path, value): - """Test that the statement does not accept any null values. - - XAPI-00001 - An LRS rejects with error code 400 Bad Request any Statement having a property whose - value is set to "null", an empty object, or has no value, except in an "extensions" - property. - """ - statement = mock_instance(BaseXapiStatement) - - statement = statement.dict(exclude_none=True) - set_dict_value_from_path(statement, path.split("__"), value) - - with pytest.raises(ValidationError, match="invalid empty value"): - BaseXapiStatement(**statement) - - -@pytest.mark.parametrize( - "path", - [ - "object__definition__extensions__https://w3id.org/xapi/video", - "result__extensions__https://w3id.org/xapi/video", - "context__extensions__https://w3id.org/xapi/video", - ], -) -@pytest.mark.parametrize("value", [None, "", {}]) -def test_models_xapi_base_statement_with_valid_null_values(path, value): - """Test that the statement does accept valid null values in extensions fields. - - XAPI-00001 - An LRS rejects with error code 400 Bad Request any Statement having a property whose - value is set to "null", an empty object, or has no value, except in an "extensions" - property. - """ - - statement = mock_instance(BaseXapiStatement, object=mock_instance(BaseXapiActivity)) - - statement = statement.dict(exclude_none=True) - - set_dict_value_from_path(statement, path.split("__"), value) - try: - BaseXapiStatement(**statement) - except ValidationError as err: - pytest.fail(f"Valid statement should not raise exceptions: {err}") - - -@pytest.mark.parametrize("path", ["object__definition__correctResponsesPattern"]) -def test_models_xapi_base_statement_with_valid_empty_array(path): - """Test that the statement does accept a valid empty array. - - Where the Correct Responses Pattern contains an empty array, the meaning of this is - that there is no correct answer. - """ - - statement = mock_instance( - BaseXapiStatement, - object=mock_instance( - BaseXapiActivity, - definition=mock_instance(BaseXapiActivityInteractionDefinition), - ), - ) - - statement = statement.dict(exclude_none=True) - set_dict_value_from_path(statement, path.split("__"), []) - try: - BaseXapiStatement(**statement) - except ValidationError as err: - pytest.fail(f"Valid statement should not raise exceptions: {err}") - - -@pytest.mark.parametrize( - "field", - ["actor", "verb", "object"], -) -def test_models_xapi_base_statement_must_use_actor_verb_and_object(field): - """Test that the statement raises an exception if required fields are missing. - - XAPI-00003 - An LRS rejects with error code 400 Bad Request a Statement which does not contain an - "actor" property. - XAPI-00004 - An LRS rejects with error code 400 Bad Request a Statement which does not contain a - "verb" property. - XAPI-00005 - An LRS rejects with error code 400 Bad Request a Statement which does not contain an - "object" property. - """ - - statement = mock_instance(BaseXapiStatement) - - statement = statement.dict(exclude_none=True) - del statement["context"] # Necessary as context leads to another validation error - del statement[field] - with pytest.raises(ValidationError, match="field required"): - BaseXapiStatement(**statement) - - -@pytest.mark.parametrize( - "path,value", - [ - ("actor__name", 1), # Should be a string - ("actor__account__name", 1), # Should be a string - ("actor__account__homePage", 1), # Should be an IRI - ("actor__account", ["foo", "bar"]), # Should be a dictionary - ("verb__display", ["foo"]), # Should be a dictionary - ("verb__display", {"en": 1}), # Should have string values - ("object__id", ["foo"]), # Should be an IRI - ], -) -def test_models_xapi_base_statement_with_invalid_data_types(path, value): - """Test that the statement does not accept values with wrong types. - - XAPI-00006 - An LRS rejects with error code 400 Bad Request a Statement which uses the wrong data - type. - """ - statement = mock_instance( - BaseXapiStatement, actor=mock_instance(BaseXapiAgentWithAccount) - ) - - statement = statement.dict(exclude_none=True) - set_dict_value_from_path(statement, path.split("__"), value) - - err = "(type expected|not a valid dict|expected string )" - - with pytest.raises(ValidationError, match=err): - BaseXapiStatement(**statement) - - -@pytest.mark.parametrize( - "path,value", - [ - ("id", "0545fe73-1bbd-4f84-9c9a"), # Should be a valid UUID - ("actor", {"mbox": "example@mail.com"}), # Should be a Mailto IRI - ("verb__display", {"bad language tag": "foo"}), # Should be a valid LanguageTag - ("object__id", ["This is not an IRI"]), # Should be an IRI - ], -) -def test_models_xapi_base_statement_with_invalid_data_format(path, value): - """Test that the statement does not accept values having a wrong format. - - XAPI-00007 - An LRS rejects with error code 400 Bad Request a Statement which uses any - non-format-following key or value, including the empty string, where a string with a - particular format (such as mailto IRI, UUID, or IRI) is required. - (Empty strings are covered by XAPI-00001) - """ - statement = mock_instance( - BaseXapiStatement, actor=mock_instance(BaseXapiAgentWithAccount) - ) - - statement = statement.dict(exclude_none=True) - set_dict_value_from_path(statement, path.split("__"), value) - err = "(string does not match regex|Invalid RFC 5646 Language tag|not a valid uuid)" - with pytest.raises(ValidationError, match=err): - BaseXapiStatement(**statement) - - -@pytest.mark.parametrize("path,value", [("actor__objecttype", "Agent")]) -def test_models_xapi_base_statement_with_invalid_letter_cases(path, value): - """Test that the statement does not accept keys having invalid letter cases. - - XAPI-00008 - An LRS rejects with error code 400 Bad Request a Statement where the case of a key - does not match the case specified in this specification. - """ - statement = mock_instance( - BaseXapiStatement, actor=mock_instance(BaseXapiAgentWithAccount) - ) - - statement = statement.dict(exclude_none=True) - if statement["actor"].get("objectType", None): - del statement["actor"]["objectType"] - set_dict_value_from_path(statement, path.split("__"), value) - err = "(unexpected value|extra fields not permitted)" - with pytest.raises(ValidationError, match=err): - BaseXapiStatement(**statement) - - -def test_models_xapi_base_statement_should_not_accept_additional_properties(): - """Test that the statement does not accept additional properties. - - XAPI-00010 - An LRS rejects with error code 400 Bad Request a Statement where a key or value is - not allowed by this specification. - """ - statement = mock_instance(BaseXapiStatement) - - invalid_statement = statement.dict(exclude_none=True) - invalid_statement["NEW_INVALID_FIELD"] = "some value" - with pytest.raises(ValidationError, match="extra fields not permitted"): - BaseXapiStatement(**invalid_statement) - - -@pytest.mark.parametrize("path,value", [("object__id", "w3id.org/xapi/video")]) -def test_models_xapi_base_statement_with_iri_without_scheme(path, value): - """Test that the statement does not accept IRIs without a scheme. - - XAPI-00011 - An LRS rejects with error code 400 Bad Request a Statement containing IRL or IRI - values without a scheme. - """ - statement = mock_instance(BaseXapiStatement) - - statement = statement.dict(exclude_none=True) - set_dict_value_from_path(statement, path.split("__"), value) - with pytest.raises(ValidationError, match="is not a valid 'IRI'"): - BaseXapiStatement(**statement) - - -@pytest.mark.parametrize( - "path", - [ - "object__definition__extensions__foo", - "result__extensions__1", - "context__extensions__w3id.org/xapi/video", - ], -) -def test_models_xapi_base_statement_with_invalid_extensions(path): - """Test that the statement does not accept extensions keys with invalid IRIs. - - XAPI-00118 - An Extension "key" is an IRI. The LRS rejects with 400 a statement which has an - extension key which is not a valid IRI, if an extension object is present. - """ - statement = mock_instance(BaseXapiStatement, object=mock_instance(BaseXapiActivity)) - - statement = statement.dict(exclude_none=True) - set_dict_value_from_path(statement, path.split("__"), "") - with pytest.raises(ValidationError, match="is not a valid 'IRI'"): - BaseXapiStatement(**statement) - - -@pytest.mark.parametrize("path,value", [("actor__mbox", "mailto:example@mail.com")]) -def test_models_xapi_base_statement_with_two_agent_types(path, value): - """Test that the statement does not accept multiple agent types. - - An Agent MUST NOT include more than one Inverse Functional Identifier. - """ - statement = mock_instance( - BaseXapiStatement, actor=mock_instance(BaseXapiAgentWithAccount) - ) - - statement = statement.dict(exclude_none=True) - set_dict_value_from_path(statement, path.split("__"), value) - with pytest.raises(ValidationError, match="extra fields not permitted"): - BaseXapiStatement(**statement) - - -def test_models_xapi_base_statement_missing_member_property(): - """Test that the statement does not accept group agents with missing members. - - An Anonymous Group MUST include a "member" property listing constituent Agents. - """ - statement = mock_instance( - BaseXapiStatement, actor=mock_instance(BaseXapiAnonymousGroup) - ) - - statement = statement.dict(exclude_none=True) - del statement["actor"]["member"] - with pytest.raises(ValidationError, match="member\n field required"): - BaseXapiStatement(**statement) - - -@pytest.mark.parametrize( - "klass", - [ - BaseXapiAnonymousGroup, - BaseXapiIdentifiedGroupWithMbox, - BaseXapiIdentifiedGroupWithMboxSha1Sum, - BaseXapiIdentifiedGroupWithOpenId, - BaseXapiIdentifiedGroupWithAccount, - ], -) -def test_models_xapi_base_statement_with_invalid_group_objects(klass): - """Test that the statement does not accept group agents with group members. - - An Anonymous Group MUST NOT contain Group Objects in the "member" identifiers. - An Identified Group MUST NOT contain Group Objects in the "member" property. - """ - - actor_class = ModelFactory.__random__.choice( - [ - BaseXapiAnonymousGroup, - BaseXapiIdentifiedGroupWithMbox, - BaseXapiIdentifiedGroupWithMboxSha1Sum, - BaseXapiIdentifiedGroupWithOpenId, - BaseXapiIdentifiedGroupWithAccount, - ] - ) - statement = mock_instance(BaseXapiStatement, actor=mock_instance(actor_class)) - - kwargs = {"exclude_none": True} - statement = statement.dict(**kwargs) - statement["actor"]["member"] = [mock_instance(klass).dict(**kwargs)] # TODO: check that nothing was lost - err = "actor -> member -> 0 -> objectType\n unexpected value; permitted: 'Agent'" - with pytest.raises(ValidationError, match=err): - BaseXapiStatement(**statement) - - -@pytest.mark.parametrize("path,value", [("actor__mbox", "mailto:example@mail.com")]) -def test_models_xapi_base_statement_with_two_group_identifiers(path, value): - """Test that the statement does not accept multiple group identifiers. - - An Identified Group MUST include exactly one Inverse Functional Identifier. - """ - statement = mock_instance( - BaseXapiStatement, actor=mock_instance(BaseXapiIdentifiedGroupWithAccount) - ) - - statement = statement.dict(exclude_none=True) - set_dict_value_from_path(statement, path.split("__"), value) - with pytest.raises(ValidationError, match="extra fields not permitted"): - BaseXapiStatement(**statement) - - -@pytest.mark.parametrize( - "path,value", - [ - ("object__id", "156e3f9f-4b56-4cba-ad52-1bd19e461d65"), - ("object__stored", "2013-05-18T05:32:34.804+00:00"), - ("object__version", "1.0.0"), - ("object__authority", {"mbox": "mailto:example@mail.com"}), - ], -) -def test_models_xapi_base_statement_with_sub_statement_ref(path, value): - """Test that the sub-statement does not accept invalid properties. - - A SubStatement MUST NOT have the "id", "stored", "version" or "authority" - properties. - """ - statement = mock_instance( - BaseXapiStatement, object=mock_instance(BaseXapiSubStatement) - ) - - statement = statement.dict(exclude_none=True) - set_dict_value_from_path(statement, path.split("__"), value) - with pytest.raises(ValidationError, match="extra fields not permitted"): - BaseXapiStatement(**statement) - - -@pytest.mark.parametrize( - "value", - [ - [{"id": "invalid whitespace"}], - [{"id": "valid"}, {"id": "invalid whitespace"}], - [{"id": "invalid_duplicate"}, {"id": "invalid_duplicate"}], - ], -) -def test_models_xapi_base_statement_with_invalid_interaction_object(value): - """Test that the statement does not accept invalid interaction fields. - - An interaction component's id value SHOULD NOT have whitespace. - Within an array of interaction components, all id values MUST be distinct. - """ - statement = mock_instance( - BaseXapiStatement, - object=mock_instance( - BaseXapiActivity, - definition=mock_instance(BaseXapiActivityInteractionDefinition), - ), - ) - - statement = statement.dict(exclude_none=True) - path = "object.definition.scale".split(".") - set_dict_value_from_path(statement, path, value) - err = "(Duplicate InteractionComponents are not valid|string does not match regex)" - with pytest.raises(ValidationError, match=err): - BaseXapiStatement(**statement) - - -@pytest.mark.parametrize( - "path,value", - [ - ("context__revision", "Format is free"), - ("context__platform", "FUN MOOC"), - ], -) -def test_models_xapi_base_statement_with_invalid_context_value(path, value): - """Test that the statement does not accept an invalid revision/platform value. - - The "revision" property MUST only be used if the Statement's Object is an Activity. - The "platform" property MUST only be used if the Statement's Object is an Activity. - """ - - object_class = ModelFactory.__random__.choice( - [ - BaseXapiSubStatement, - BaseXapiStatementRef, - ] - ) - statement = mock_instance(BaseXapiStatement, object=mock_instance(object_class)) - - - statement = statement.dict(exclude_none=True) - set_dict_value_from_path(statement, path.split("__"), value) - err = "properties can only be used if the Statement's Object is an Activity" - with pytest.raises(ValidationError, match=err): - BaseXapiStatement(**statement) - - -@pytest.mark.parametrize("path", ["context.contextActivities.not_parent"]) -def test_models_xapi_base_statement_with_invalid_context_activities(path): - """Test that the statement does not accept invalid context activity properties. - - Every key in the contextActivities Object MUST be one of parent, grouping, category, - or other. - """ - statement = mock_instance(BaseXapiStatement) - - statement = statement.dict(exclude_none=True) - set_dict_value_from_path(statement, path.split("."), {"id": "http://w3id.org/xapi"}) - with pytest.raises(ValidationError, match="extra fields not permitted"): - BaseXapiStatement(**statement) - - -@pytest.mark.parametrize( - "value", - [ - {"id": "http://w3id.org/xapi"}, - [{"id": "http://w3id.org/xapi"}], - [{"id": "http://w3id.org/xapi"}, {"id": "http://w3id.org/xapi/video"}], - ], -) -def test_models_xapi_base_statement_with_valid_context_activities(value): - """Test that the statement does accept valid context activities fields. - - Every value in the contextActivities Object MUST be either a single Activity Object - or an array of Activity Objects. - """ - statement = mock_instance(BaseXapiStatement) - - statement = statement.dict(exclude_none=True) - path = ["context", "contextActivities"] - for activity in ["parent", "grouping", "category", "other"]: - set_dict_value_from_path(statement, path + [activity], value) - try: - BaseXapiStatement(**statement) - except ValidationError as err: - pytest.fail(f"Valid statement should not raise exceptions: {err}") - - -@pytest.mark.parametrize("value", ["0.0.0", "1.1.0", "1", "2", "1.10.1", "1.0.1.1"]) -def test_models_xapi_base_statement_with_invalid_version(value): - """Test that the statement does not accept an invalid version field. - - An LRS MUST reject all Statements with a version specified that does not start with - 1.0. - """ - statement = mock_instance(BaseXapiStatement) - - statement = statement.dict(exclude_none=True) - set_dict_value_from_path(statement, ["version"], value) - with pytest.raises(ValidationError, match="version\n string does not match regex"): - BaseXapiStatement(**statement) - - -def test_models_xapi_base_statement_with_valid_version(): - """Test that the statement does accept a valid version field. - - Statements returned by an LRS MUST retain the version they are accepted with. - If they lack a version, the version MUST be set to 1.0.0. - """ - statement = mock_instance(BaseXapiStatement) - - statement = statement.dict(exclude_none=True) - set_dict_value_from_path(statement, ["version"], "1.0.3") - assert "1.0.3" == BaseXapiStatement(**statement).dict()["version"] - del statement["version"] - assert "1.0.0" == BaseXapiStatement(**statement).dict()["version"] +# @pytest.mark.parametrize( +# "path", +# ["id", "stored", "verb__display", "result__score__raw"], +# ) +# @pytest.mark.parametrize("value", [None, "", {}]) +# def test_models_xapi_base_statement_with_invalid_null_values(path, value): +# """Test that the statement does not accept any null values. + +# XAPI-00001 +# An LRS rejects with error code 400 Bad Request any Statement having a property whose +# value is set to "null", an empty object, or has no value, except in an "extensions" +# property. +# """ +# statement = mock_instance(BaseXapiStatement) + +# statement = statement.dict(exclude_none=True) +# set_dict_value_from_path(statement, path.split("__"), value) + +# with pytest.raises(ValidationError, match="invalid empty value"): +# BaseXapiStatement(**statement) + + +# @pytest.mark.parametrize( +# "path", +# [ +# "object__definition__extensions__https://w3id.org/xapi/video", +# "result__extensions__https://w3id.org/xapi/video", +# "context__extensions__https://w3id.org/xapi/video", +# ], +# ) +# @pytest.mark.parametrize("value", [None, "", {}]) +# def test_models_xapi_base_statement_with_valid_null_values(path, value): +# """Test that the statement does accept valid null values in extensions fields. + +# XAPI-00001 +# An LRS rejects with error code 400 Bad Request any Statement having a property whose +# value is set to "null", an empty object, or has no value, except in an "extensions" +# property. +# """ + +# statement = mock_instance(BaseXapiStatement, object=mock_instance(BaseXapiActivity)) + +# statement = statement.dict(exclude_none=True) + +# set_dict_value_from_path(statement, path.split("__"), value) +# try: +# BaseXapiStatement(**statement) +# except ValidationError as err: +# pytest.fail(f"Valid statement should not raise exceptions: {err}") + + +# @pytest.mark.parametrize("path", ["object__definition__correctResponsesPattern"]) +# def test_models_xapi_base_statement_with_valid_empty_array(path): +# """Test that the statement does accept a valid empty array. + +# Where the Correct Responses Pattern contains an empty array, the meaning of this is +# that there is no correct answer. +# """ + +# statement = mock_instance( +# BaseXapiStatement, +# object=mock_instance( +# BaseXapiActivity, +# definition=mock_instance(BaseXapiActivityInteractionDefinition), +# ), +# ) + +# statement = statement.dict(exclude_none=True) +# set_dict_value_from_path(statement, path.split("__"), []) +# try: +# BaseXapiStatement(**statement) +# except ValidationError as err: +# pytest.fail(f"Valid statement should not raise exceptions: {err}") + + +# @pytest.mark.parametrize( +# "field", +# ["actor", "verb", "object"], +# ) +# def test_models_xapi_base_statement_must_use_actor_verb_and_object(field): +# """Test that the statement raises an exception if required fields are missing. + +# XAPI-00003 +# An LRS rejects with error code 400 Bad Request a Statement which does not contain an +# "actor" property. +# XAPI-00004 +# An LRS rejects with error code 400 Bad Request a Statement which does not contain a +# "verb" property. +# XAPI-00005 +# An LRS rejects with error code 400 Bad Request a Statement which does not contain an +# "object" property. +# """ + +# statement = mock_instance(BaseXapiStatement) + +# statement = statement.dict(exclude_none=True) +# del statement["context"] # Necessary as context leads to another validation error +# del statement[field] +# with pytest.raises(ValidationError, match="field required"): +# BaseXapiStatement(**statement) + + +# @pytest.mark.parametrize( +# "path,value", +# [ +# ("actor__name", 1), # Should be a string +# ("actor__account__name", 1), # Should be a string +# ("actor__account__homePage", 1), # Should be an IRI +# ("actor__account", ["foo", "bar"]), # Should be a dictionary +# ("verb__display", ["foo"]), # Should be a dictionary +# ("verb__display", {"en": 1}), # Should have string values +# ("object__id", ["foo"]), # Should be an IRI +# ], +# ) +# def test_models_xapi_base_statement_with_invalid_data_types(path, value): +# """Test that the statement does not accept values with wrong types. + +# XAPI-00006 +# An LRS rejects with error code 400 Bad Request a Statement which uses the wrong data +# type. +# """ +# statement = mock_instance( +# BaseXapiStatement, actor=mock_instance(BaseXapiAgentWithAccount) +# ) + +# statement = statement.dict(exclude_none=True) +# set_dict_value_from_path(statement, path.split("__"), value) + +# err = "(type expected|not a valid dict|expected string )" + +# with pytest.raises(ValidationError, match=err): +# BaseXapiStatement(**statement) + + +# @pytest.mark.parametrize( +# "path,value", +# [ +# ("id", "0545fe73-1bbd-4f84-9c9a"), # Should be a valid UUID +# ("actor", {"mbox": "example@mail.com"}), # Should be a Mailto IRI +# ("verb__display", {"bad language tag": "foo"}), # Should be a valid LanguageTag +# ("object__id", ["This is not an IRI"]), # Should be an IRI +# ], +# ) +# def test_models_xapi_base_statement_with_invalid_data_format(path, value): +# """Test that the statement does not accept values having a wrong format. + +# XAPI-00007 +# An LRS rejects with error code 400 Bad Request a Statement which uses any +# non-format-following key or value, including the empty string, where a string with a +# particular format (such as mailto IRI, UUID, or IRI) is required. +# (Empty strings are covered by XAPI-00001) +# """ +# statement = mock_instance( +# BaseXapiStatement, actor=mock_instance(BaseXapiAgentWithAccount) +# ) + +# statement = statement.dict(exclude_none=True) +# set_dict_value_from_path(statement, path.split("__"), value) +# err = "(string does not match regex|Invalid RFC 5646 Language tag|not a valid uuid)" +# with pytest.raises(ValidationError, match=err): +# BaseXapiStatement(**statement) + + +# @pytest.mark.parametrize("path,value", [("actor__objecttype", "Agent")]) +# def test_models_xapi_base_statement_with_invalid_letter_cases(path, value): +# """Test that the statement does not accept keys having invalid letter cases. + +# XAPI-00008 +# An LRS rejects with error code 400 Bad Request a Statement where the case of a key +# does not match the case specified in this specification. +# """ +# statement = mock_instance( +# BaseXapiStatement, actor=mock_instance(BaseXapiAgentWithAccount) +# ) + +# statement = statement.dict(exclude_none=True) +# if statement["actor"].get("objectType", None): +# del statement["actor"]["objectType"] +# set_dict_value_from_path(statement, path.split("__"), value) +# err = "(unexpected value|extra fields not permitted)" +# with pytest.raises(ValidationError, match=err): +# BaseXapiStatement(**statement) + + +# def test_models_xapi_base_statement_should_not_accept_additional_properties(): +# """Test that the statement does not accept additional properties. + +# XAPI-00010 +# An LRS rejects with error code 400 Bad Request a Statement where a key or value is +# not allowed by this specification. +# """ +# statement = mock_instance(BaseXapiStatement) + +# invalid_statement = statement.dict(exclude_none=True) +# invalid_statement["NEW_INVALID_FIELD"] = "some value" +# with pytest.raises(ValidationError, match="extra fields not permitted"): +# BaseXapiStatement(**invalid_statement) + + +# @pytest.mark.parametrize("path,value", [("object__id", "w3id.org/xapi/video")]) +# def test_models_xapi_base_statement_with_iri_without_scheme(path, value): +# """Test that the statement does not accept IRIs without a scheme. + +# XAPI-00011 +# An LRS rejects with error code 400 Bad Request a Statement containing IRL or IRI +# values without a scheme. +# """ +# statement = mock_instance(BaseXapiStatement) + +# statement = statement.dict(exclude_none=True) +# set_dict_value_from_path(statement, path.split("__"), value) +# with pytest.raises(ValidationError, match="is not a valid 'IRI'"): +# BaseXapiStatement(**statement) + + +# @pytest.mark.parametrize( +# "path", +# [ +# "object__definition__extensions__foo", +# "result__extensions__1", +# "context__extensions__w3id.org/xapi/video", +# ], +# ) +# def test_models_xapi_base_statement_with_invalid_extensions(path): +# """Test that the statement does not accept extensions keys with invalid IRIs. + +# XAPI-00118 +# An Extension "key" is an IRI. The LRS rejects with 400 a statement which has an +# extension key which is not a valid IRI, if an extension object is present. +# """ +# statement = mock_instance(BaseXapiStatement, object=mock_instance(BaseXapiActivity)) + +# statement = statement.dict(exclude_none=True) +# set_dict_value_from_path(statement, path.split("__"), "") +# with pytest.raises(ValidationError, match="is not a valid 'IRI'"): +# BaseXapiStatement(**statement) + + +# @pytest.mark.parametrize("path,value", [("actor__mbox", "mailto:example@mail.com")]) +# def test_models_xapi_base_statement_with_two_agent_types(path, value): +# """Test that the statement does not accept multiple agent types. + +# An Agent MUST NOT include more than one Inverse Functional Identifier. +# """ +# statement = mock_instance( +# BaseXapiStatement, actor=mock_instance(BaseXapiAgentWithAccount) +# ) + +# statement = statement.dict(exclude_none=True) +# set_dict_value_from_path(statement, path.split("__"), value) +# with pytest.raises(ValidationError, match="extra fields not permitted"): +# BaseXapiStatement(**statement) + + +# def test_models_xapi_base_statement_missing_member_property(): +# """Test that the statement does not accept group agents with missing members. + +# An Anonymous Group MUST include a "member" property listing constituent Agents. +# """ +# statement = mock_instance( +# BaseXapiStatement, actor=mock_instance(BaseXapiAnonymousGroup) +# ) + +# statement = statement.dict(exclude_none=True) +# del statement["actor"]["member"] +# with pytest.raises(ValidationError, match="member\n field required"): +# BaseXapiStatement(**statement) + + +# @pytest.mark.parametrize( +# "klass", +# [ +# BaseXapiAnonymousGroup, +# BaseXapiIdentifiedGroupWithMbox, +# BaseXapiIdentifiedGroupWithMboxSha1Sum, +# BaseXapiIdentifiedGroupWithOpenId, +# BaseXapiIdentifiedGroupWithAccount, +# ], +# ) +# def test_models_xapi_base_statement_with_invalid_group_objects(klass): +# """Test that the statement does not accept group agents with group members. + +# An Anonymous Group MUST NOT contain Group Objects in the "member" identifiers. +# An Identified Group MUST NOT contain Group Objects in the "member" property. +# """ + +# actor_class = ModelFactory.__random__.choice( +# [ +# BaseXapiAnonymousGroup, +# BaseXapiIdentifiedGroupWithMbox, +# BaseXapiIdentifiedGroupWithMboxSha1Sum, +# BaseXapiIdentifiedGroupWithOpenId, +# BaseXapiIdentifiedGroupWithAccount, +# ] +# ) +# statement = mock_instance(BaseXapiStatement, actor=mock_instance(actor_class)) + +# kwargs = {"exclude_none": True} +# statement = statement.dict(**kwargs) +# statement["actor"]["member"] = [mock_instance(klass).dict(**kwargs)] # TODO: check that nothing was lost +# err = "actor -> member -> 0 -> objectType\n unexpected value; permitted: 'Agent'" +# with pytest.raises(ValidationError, match=err): +# BaseXapiStatement(**statement) + + +# @pytest.mark.parametrize("path,value", [("actor__mbox", "mailto:example@mail.com")]) +# def test_models_xapi_base_statement_with_two_group_identifiers(path, value): +# """Test that the statement does not accept multiple group identifiers. + +# An Identified Group MUST include exactly one Inverse Functional Identifier. +# """ +# statement = mock_instance( +# BaseXapiStatement, actor=mock_instance(BaseXapiIdentifiedGroupWithAccount) +# ) + +# statement = statement.dict(exclude_none=True) +# set_dict_value_from_path(statement, path.split("__"), value) +# with pytest.raises(ValidationError, match="extra fields not permitted"): +# BaseXapiStatement(**statement) + + +# @pytest.mark.parametrize( +# "path,value", +# [ +# ("object__id", "156e3f9f-4b56-4cba-ad52-1bd19e461d65"), +# ("object__stored", "2013-05-18T05:32:34.804+00:00"), +# ("object__version", "1.0.0"), +# ("object__authority", {"mbox": "mailto:example@mail.com"}), +# ], +# ) +# def test_models_xapi_base_statement_with_sub_statement_ref(path, value): +# """Test that the sub-statement does not accept invalid properties. + +# A SubStatement MUST NOT have the "id", "stored", "version" or "authority" +# properties. +# """ +# statement = mock_instance( +# BaseXapiStatement, object=mock_instance(BaseXapiSubStatement) +# ) + +# statement = statement.dict(exclude_none=True) +# set_dict_value_from_path(statement, path.split("__"), value) +# with pytest.raises(ValidationError, match="extra fields not permitted"): +# BaseXapiStatement(**statement) + + +# @pytest.mark.parametrize( +# "value", +# [ +# [{"id": "invalid whitespace"}], +# [{"id": "valid"}, {"id": "invalid whitespace"}], +# [{"id": "invalid_duplicate"}, {"id": "invalid_duplicate"}], +# ], +# ) +# def test_models_xapi_base_statement_with_invalid_interaction_object(value): +# """Test that the statement does not accept invalid interaction fields. + +# An interaction component's id value SHOULD NOT have whitespace. +# Within an array of interaction components, all id values MUST be distinct. +# """ +# statement = mock_instance( +# BaseXapiStatement, +# object=mock_instance( +# BaseXapiActivity, +# definition=mock_instance(BaseXapiActivityInteractionDefinition), +# ), +# ) + +# statement = statement.dict(exclude_none=True) +# path = "object.definition.scale".split(".") +# set_dict_value_from_path(statement, path, value) +# err = "(Duplicate InteractionComponents are not valid|string does not match regex)" +# with pytest.raises(ValidationError, match=err): +# BaseXapiStatement(**statement) + + +# @pytest.mark.parametrize( +# "path,value", +# [ +# ("context__revision", "Format is free"), +# ("context__platform", "FUN MOOC"), +# ], +# ) +# def test_models_xapi_base_statement_with_invalid_context_value(path, value): +# """Test that the statement does not accept an invalid revision/platform value. + +# The "revision" property MUST only be used if the Statement's Object is an Activity. +# The "platform" property MUST only be used if the Statement's Object is an Activity. +# """ + +# object_class = ModelFactory.__random__.choice( +# [ +# BaseXapiSubStatement, +# BaseXapiStatementRef, +# ] +# ) +# statement = mock_instance(BaseXapiStatement, object=mock_instance(object_class)) + + +# statement = statement.dict(exclude_none=True) +# set_dict_value_from_path(statement, path.split("__"), value) +# err = "properties can only be used if the Statement's Object is an Activity" +# with pytest.raises(ValidationError, match=err): +# BaseXapiStatement(**statement) + + +# @pytest.mark.parametrize("path", ["context.contextActivities.not_parent"]) +# def test_models_xapi_base_statement_with_invalid_context_activities(path): +# """Test that the statement does not accept invalid context activity properties. + +# Every key in the contextActivities Object MUST be one of parent, grouping, category, +# or other. +# """ +# statement = mock_instance(BaseXapiStatement) + +# statement = statement.dict(exclude_none=True) +# set_dict_value_from_path(statement, path.split("."), {"id": "http://w3id.org/xapi"}) +# with pytest.raises(ValidationError, match="extra fields not permitted"): +# BaseXapiStatement(**statement) + + +# @pytest.mark.parametrize( +# "value", +# [ +# {"id": "http://w3id.org/xapi"}, +# [{"id": "http://w3id.org/xapi"}], +# [{"id": "http://w3id.org/xapi"}, {"id": "http://w3id.org/xapi/video"}], +# ], +# ) +# def test_models_xapi_base_statement_with_valid_context_activities(value): +# """Test that the statement does accept valid context activities fields. + +# Every value in the contextActivities Object MUST be either a single Activity Object +# or an array of Activity Objects. +# """ +# statement = mock_instance(BaseXapiStatement) + +# statement = statement.dict(exclude_none=True) +# path = ["context", "contextActivities"] +# for activity in ["parent", "grouping", "category", "other"]: +# set_dict_value_from_path(statement, path + [activity], value) +# try: +# BaseXapiStatement(**statement) +# except ValidationError as err: +# pytest.fail(f"Valid statement should not raise exceptions: {err}") + + +# @pytest.mark.parametrize("value", ["0.0.0", "1.1.0", "1", "2", "1.10.1", "1.0.1.1"]) +# def test_models_xapi_base_statement_with_invalid_version(value): +# """Test that the statement does not accept an invalid version field. + +# An LRS MUST reject all Statements with a version specified that does not start with +# 1.0. +# """ +# statement = mock_instance(BaseXapiStatement) + +# statement = statement.dict(exclude_none=True) +# set_dict_value_from_path(statement, ["version"], value) +# with pytest.raises(ValidationError, match="version\n string does not match regex"): +# BaseXapiStatement(**statement) + + +# def test_models_xapi_base_statement_with_valid_version(): +# """Test that the statement does accept a valid version field. + +# Statements returned by an LRS MUST retain the version they are accepted with. +# If they lack a version, the version MUST be set to 1.0.0. +# """ +# statement = mock_instance(BaseXapiStatement) + +# statement = statement.dict(exclude_none=True) +# set_dict_value_from_path(statement, ["version"], "1.0.3") +# assert "1.0.3" == BaseXapiStatement(**statement).dict()["version"] +# del statement["version"] +# assert "1.0.0" == BaseXapiStatement(**statement).dict()["version"] @pytest.mark.parametrize( @@ -519,7 +519,8 @@ def test_models_xapi_base_statement_should_consider_valid_all_defined_xapi_model # All specific xAPI models should inherit BaseXapiStatement assert issubclass(model, BaseXapiStatement) - statement = mock_instance(model).json(exclude_none=True, by_alias=True) # TODO: check that we are not losing info by mocking random model + statement = mock_instance(model) + statement = statement.json(exclude_none=True, by_alias=True) # TODO: check that we are not losing info by mocking random model try: BaseXapiStatement(**json.loads(statement)) except ValidationError as err: From 9c3f0a77ab7933ae971739c58e53d55c2e379dc4 Mon Sep 17 00:00:00 2001 From: lleeoo Date: Tue, 12 Dec 2023 16:09:54 +0100 Subject: [PATCH 12/19] wip --- .../models/xapi/virtual_classroom/contexts.py | 45 ++-- tests/factories.py | 193 ++++++++++++++ tests/fixtures/hypothesis_strategies.py | 252 +++++++++--------- tests/models/xapi/base/test_statements.py | 30 +-- 4 files changed, 357 insertions(+), 163 deletions(-) create mode 100644 tests/factories.py diff --git a/src/ralph/models/xapi/virtual_classroom/contexts.py b/src/ralph/models/xapi/virtual_classroom/contexts.py index c83c73d58..0bde82170 100644 --- a/src/ralph/models/xapi/virtual_classroom/contexts.py +++ b/src/ralph/models/xapi/virtual_classroom/contexts.py @@ -46,28 +46,29 @@ class VirtualClassroomContextContextActivities(BaseXapiContextContextActivities) List[Union[VirtualClassroomProfileActivity, BaseXapiActivity]], ] - # @validator("category") - # @classmethod - # def check_presence_of_profile_activity_category( - # cls, - # value: Union[ - # VirtualClassroomProfileActivity, - # List[Union[VirtualClassroomProfileActivity, BaseXapiActivity]], - # ], - # ) -> Union[ - # VirtualClassroomProfileActivity, - # List[Union[VirtualClassroomProfileActivity, BaseXapiActivity]], - # ]: - # """Check that the category list contains a `VirtualClassroomProfileActivity`.""" - # if isinstance(value, VirtualClassroomProfileActivity): - # return value - # for activity in value: - # if isinstance(activity, VirtualClassroomProfileActivity): - # return value - # raise ValueError( - # "The `context.contextActivities.category` field should contain at least " - # "one valid `VirtualClassroomProfileActivity`" - # ) + # 3 validation errors: problem not here # TODO: Remove this message + @validator("category") + @classmethod + def check_presence_of_profile_activity_category( + cls, + value: Union[ + VirtualClassroomProfileActivity, + List[Union[VirtualClassroomProfileActivity, BaseXapiActivity]], + ], + ) -> Union[ + VirtualClassroomProfileActivity, + List[Union[VirtualClassroomProfileActivity, BaseXapiActivity]], + ]: + """Check that the category list contains a `VirtualClassroomProfileActivity`.""" + if isinstance(value, VirtualClassroomProfileActivity): + return value + for activity in value: + if isinstance(activity, VirtualClassroomProfileActivity): + return value + raise ValueError( + "The `context.contextActivities.category` field should contain at least " + "one valid `VirtualClassroomProfileActivity`" + ) class VirtualClassroomContextExtensions(BaseExtensionModelWithConfig): diff --git a/tests/factories.py b/tests/factories.py new file mode 100644 index 000000000..3c37e0db1 --- /dev/null +++ b/tests/factories.py @@ -0,0 +1,193 @@ +"""Mock model generation for testing.""" + +from typing import Any, Callable +from decimal import Decimal + +from polyfactory.factories.pydantic_factory import ( + ModelFactory as PolyfactoryModelFactory, + T, +) +from polyfactory.fields import Ignore + +from ralph.models.edx.navigational.fields.events import NavigationalEventField +from ralph.models.edx.navigational.statements import UISeqNext, UISeqPrev +from ralph.models.xapi.base.common import MailtoEmail, IRI +from ralph.models.xapi import VirtualClassroomAnsweredPoll + +from ralph.models.xapi.concepts.activity_types.virtual_classroom import VirtualClassroomActivity +from ralph.models.xapi.base.contexts import BaseXapiContext +from ralph.models.xapi.base.results import BaseXapiResultScore +from ralph.models.xapi.base.common import LanguageTag +from ralph.models.xapi.base.contexts import BaseXapiContextContextActivities +from ralph.models.xapi.base.unnested_objects import ( + BaseXapiActivityInteractionDefinition, +) + +from ralph.models.xapi.lms.contexts import ( + LMSContextContextActivities, + LMSProfileActivity, +) + +from ralph.models.xapi.video.contexts import ( + VideoContextContextActivities, + VideoProfileActivity, +) + +from ralph.models.xapi.virtual_classroom.contexts import ( + VirtualClassroomContextContextActivities, + VirtualClassroomProfileActivity, + VirtualClassroomAnsweredPollContextActivities, +) + + +from pydantic import BaseModel +from polyfactory.factories.base import BaseFactory + +from pprint import pprint + +def prune(d: Any, exceptions: list = []): + """Remove all empty leaves from a dict, except fo those in `exceptions`.""" + # TODO: add test ? + + if isinstance(d, BaseModel): + d = d.dict() + if isinstance(d, dict): + d_dict_not_exceptions = { + k: prune(v) for k, v in d.items() if k not in exceptions + } + d_dict_not_exceptions = { + k: v for k, v in d.items() if v + } + d_dict_exceptions = { + k: v for k, v in d.items() if k in exceptions + } + return d_dict_not_exceptions | d_dict_exceptions + elif isinstance(d, list): + d_list = [prune(v) for v in d] + return [v for v in d_list if v] + if d: + return d + return False + + +class ModelFactory(PolyfactoryModelFactory[T]): + __allow_none_optionals__ = False + __is_base_factory__ = True + __random_seed__ = 6 # TODO: remove this + + @classmethod + def get_provider_map(cls) -> dict[Any, Callable[[], Any]]: + provider_map = super().get_provider_map() + provider_map[LanguageTag] = lambda: LanguageTag("en-US") + provider_map[IRI] = lambda: IRI("https://w3id.org/xapi/video/verbs/played") + return provider_map + + @classmethod + def _get_or_create_factory(cls, model: type): + #print("Cls:", model) + created_factory = super()._get_or_create_factory(model) + created_factory.get_provider_map = cls.get_provider_map + created_factory._get_or_create_factory = cls._get_or_create_factory + return created_factory + + +class BaseXapiResultScoreFactory(ModelFactory[BaseXapiResultScore]): + __set_as_default_factory_for_type__ = True + __model__ = BaseXapiResultScore + + min = Decimal("0.0") + max = Decimal("20.0") + raw = Decimal("11") + + +# TODO: put back ? +# class BaseXapiActivityInteractionDefinitionFactory( +# ModelFactory[BaseXapiActivityInteractionDefinition] +# ): +# __set_as_default_factory_for_type__ = True +# __model__ = BaseXapiActivityInteractionDefinition + +# correctResponsesPattern = None + + +def myfunc(): + raise Exception("WHAT ARE YOU EVEN DOING") + +# TODO: put back ? +# class BaseXapiContextContextActivitiesFactory( +# ModelFactory[BaseXapiContextContextActivities] +# ): +# __model__ = BaseXapiContextContextActivities + +# category = myfunc + + +class BaseXapiContextFactory(ModelFactory[BaseXapiContext]): + __model__ = BaseXapiContext + __set_as_default_factory_for_type__ = True + + revision = Ignore() + platform = Ignore() + + # TODO: see why this was added + # contextActivities = ( + # lambda: BaseXapiContextContextActivitiesFactory.build() or Ignore() + # ) + + + + +class LMSContextContextActivitiesFactory(ModelFactory[LMSContextContextActivities]): + __model__ = LMSContextContextActivities + __set_as_default_factory_for_type__ = True + + category = lambda: mock_instance(LMSProfileActivity) # TODO: Uncomment + +class VideoContextContextActivitiesFactory(ModelFactory[VideoContextContextActivities]): + __model__ = VideoContextContextActivities + __set_as_default_factory_for_type__ = True + + category = lambda: mock_instance(VideoProfileActivity) + +class VirtualClassroomContextContextActivitiesFactory(ModelFactory[VirtualClassroomContextContextActivities]): + __model__ = VirtualClassroomContextContextActivities + __set_as_default_factory_for_type__ = True + + category = lambda: mock_instance(VirtualClassroomProfileActivity) + +# class VirtualClassroomAnsweredPollContextActivitiesFactory(ModelFactory[VirtualClassroomAnsweredPollContextActivities]): +# __model__ = VirtualClassroomAnsweredPollContextActivities + +# category = lambda: mock_instance(VirtualClassroomProfileActivity) +# parent = lambda: mock_instance(VirtualClassroomActivity) # TODO: Remove this. Check that this is valid + + + +class UISeqPrev(ModelFactory[UISeqPrev]): + __model__ = UISeqPrev + __set_as_default_factory_for_type__ = True + + event = lambda: mock_instance(NavigationalEventField, old=1, new=0) + +class UISeqNext(ModelFactory[UISeqNext]): + __model__ = UISeqNext + __set_as_default_factory_for_type__ = True + + event = lambda: mock_instance(NavigationalEventField, old=0, new=1) + + +def mock_instance(klass, *args, **kwargs): + """Generate a mock instance of a given class (`klass`).""" + + # Avoid redifining custom factories + if klass not in BaseFactory._factory_type_mapping: + class KlassFactory(ModelFactory[klass]): + __model__ = klass + else: + KlassFactory = BaseFactory._factory_type_mapping[klass] + + kwargs = KlassFactory.process_kwargs(*args, **kwargs) + + kwargs = prune(kwargs) + + return klass(**kwargs) diff --git a/tests/fixtures/hypothesis_strategies.py b/tests/fixtures/hypothesis_strategies.py index 0d5f120a9..b3a6073f7 100644 --- a/tests/fixtures/hypothesis_strategies.py +++ b/tests/fixtures/hypothesis_strategies.py @@ -1,126 +1,126 @@ -"""Hypothesis build strategies with special constraints.""" - -import random -from typing import Union - -from hypothesis import given -from hypothesis import strategies as st -from pydantic import BaseModel - -from ralph.models.edx.navigational.fields.events import NavigationalEventField -from ralph.models.edx.navigational.statements import UISeqNext, UISeqPrev -from ralph.models.xapi.base.contexts import BaseXapiContext -from ralph.models.xapi.base.results import BaseXapiResultScore -from ralph.models.xapi.lms.contexts import ( - LMSContextContextActivities, - LMSProfileActivity, -) -from ralph.models.xapi.video.contexts import ( - VideoContextContextActivities, - VideoProfileActivity, -) -from ralph.models.xapi.virtual_classroom.contexts import ( - VirtualClassroomContextContextActivities, - VirtualClassroomProfileActivity, -) - -OVERWRITTEN_STRATEGIES = {} - - -def is_base_model(klass): - """Return True if the given class is a subclass of the pydantic BaseModel.""" - - try: - return issubclass(klass, BaseModel) - except TypeError: - return False - - -def get_strategy_from(annotation): - """Infer a Hypothesis strategy from the given annotation.""" - origin = getattr(annotation, "__origin__", None) - args = getattr(annotation, "__args__", None) - if is_base_model(annotation): - return custom_builds(annotation) - if origin is Union: - return st.one_of( - [get_strategy_from(t) for t in args if not isinstance(t, type(None))] - ) - if origin is list: - return st.lists(get_strategy_from(args[0]), min_size=1) - if origin is dict: - keys = get_strategy_from(args[0]) - values = get_strategy_from(args[1]) - return st.dictionaries(keys, values, min_size=1) - if annotation is None: - return st.none() - return st.from_type(annotation) - - -def custom_builds( - klass: BaseModel, _overwrite_default=True, **kwargs: Union[st.SearchStrategy, bool] -): - """Return a fixed_dictionaries Hypothesis strategy for pydantic models. - - Args: - klass (BaseModel): The pydantic model for which to generate a strategy. - _overwrite_default (bool): By default, fields overwritten by kwargs become - required. If _overwrite_default is set to False, we keep the original field - requirement (either required or optional). - **kwargs (SearchStrategy or bool): If kwargs contain search strategies, they - overwrite the default strategy for the given key. - If kwargs contains booleans, they set whether the given key should be - present (True) or omitted (False) in the generated model. - """ - - for special_class, special_kwargs in OVERWRITTEN_STRATEGIES.items(): - if issubclass(klass, special_class): - kwargs = dict(special_kwargs, **kwargs) - break - optional = {} - required = {} - for name, field in klass.__fields__.items(): - arg = kwargs.get(name, None) - if arg is False: - continue - is_required = field.required or (arg is not None and _overwrite_default) - required_optional = required if is_required or arg is not None else optional - field_strategy = get_strategy_from(field.outer_type_) if arg is None else arg - required_optional[field.alias] = field_strategy - if not required: - # To avoid generating empty values - key, value = random.choice(list(optional.items())) - required[key] = value - del optional[key] - return st.fixed_dictionaries(required, optional=optional).map(klass.parse_obj) - -def custom_given(*args: Union[st.SearchStrategy, BaseModel], **kwargs): - """Wrap the Hypothesis `given` function. Replace st.builds with custom_builds.""" - strategies = [] - for arg in args: - strategies.append(custom_builds(arg) if is_base_model(arg) else arg) - return given(*strategies, **kwargs) - - -OVERWRITTEN_STRATEGIES = { - UISeqPrev: { - "event": custom_builds(NavigationalEventField, old=st.just(1), new=st.just(0)) - }, - UISeqNext: { - "event": custom_builds(NavigationalEventField, old=st.just(0), new=st.just(1)) - }, - BaseXapiContext: { - "revision": False, - "platform": False, - }, - BaseXapiResultScore: { - "raw": False, - "min": False, - "max": False, - }, - LMSContextContextActivities: {"category": custom_builds(LMSProfileActivity)}, - VideoContextContextActivities: {"category": custom_builds(VideoProfileActivity)}, - VirtualClassroomContextContextActivities: { - "category": custom_builds(VirtualClassroomProfileActivity) - }, -} +# """Hypothesis build strategies with special constraints.""" + +# import random +# from typing import Union + +# from hypothesis import given +# from hypothesis import strategies as st +# from pydantic import BaseModel + +# from ralph.models.edx.navigational.fields.events import NavigationalEventField +# from ralph.models.edx.navigational.statements import UISeqNext, UISeqPrev +# from ralph.models.xapi.base.contexts import BaseXapiContext +# from ralph.models.xapi.base.results import BaseXapiResultScore +# from ralph.models.xapi.lms.contexts import ( +# LMSContextContextActivities, +# LMSProfileActivity, +# ) +# from ralph.models.xapi.video.contexts import ( +# VideoContextContextActivities, +# VideoProfileActivity, +# ) +# from ralph.models.xapi.virtual_classroom.contexts import ( +# VirtualClassroomContextContextActivities, +# VirtualClassroomProfileActivity, +# ) + +# OVERWRITTEN_STRATEGIES = {} + + +# def is_base_model(klass): +# """Return True if the given class is a subclass of the pydantic BaseModel.""" + +# try: +# return issubclass(klass, BaseModel) +# except TypeError: +# return False + + +# def get_strategy_from(annotation): +# """Infer a Hypothesis strategy from the given annotation.""" +# origin = getattr(annotation, "__origin__", None) +# args = getattr(annotation, "__args__", None) +# if is_base_model(annotation): +# return custom_builds(annotation) +# if origin is Union: +# return st.one_of( +# [get_strategy_from(t) for t in args if not isinstance(t, type(None))] +# ) +# if origin is list: +# return st.lists(get_strategy_from(args[0]), min_size=1) +# if origin is dict: +# keys = get_strategy_from(args[0]) +# values = get_strategy_from(args[1]) +# return st.dictionaries(keys, values, min_size=1) +# if annotation is None: +# return st.none() +# return st.from_type(annotation) + + +# def custom_builds( +# klass: BaseModel, _overwrite_default=True, **kwargs: Union[st.SearchStrategy, bool] +# ): +# """Return a fixed_dictionaries Hypothesis strategy for pydantic models. + +# Args: +# klass (BaseModel): The pydantic model for which to generate a strategy. +# _overwrite_default (bool): By default, fields overwritten by kwargs become +# required. If _overwrite_default is set to False, we keep the original field +# requirement (either required or optional). +# **kwargs (SearchStrategy or bool): If kwargs contain search strategies, they +# overwrite the default strategy for the given key. +# If kwargs contains booleans, they set whether the given key should be +# present (True) or omitted (False) in the generated model. +# """ + +# for special_class, special_kwargs in OVERWRITTEN_STRATEGIES.items(): +# if issubclass(klass, special_class): +# kwargs = dict(special_kwargs, **kwargs) +# break +# optional = {} +# required = {} +# for name, field in klass.__fields__.items(): +# arg = kwargs.get(name, None) +# if arg is False: +# continue +# is_required = field.required or (arg is not None and _overwrite_default) +# required_optional = required if is_required or arg is not None else optional +# field_strategy = get_strategy_from(field.outer_type_) if arg is None else arg +# required_optional[field.alias] = field_strategy +# if not required: +# # To avoid generating empty values +# key, value = random.choice(list(optional.items())) +# required[key] = value +# del optional[key] +# return st.fixed_dictionaries(required, optional=optional).map(klass.parse_obj) + +# def custom_given(*args: Union[st.SearchStrategy, BaseModel], **kwargs): +# """Wrap the Hypothesis `given` function. Replace st.builds with custom_builds.""" +# strategies = [] +# for arg in args: +# strategies.append(custom_builds(arg) if is_base_model(arg) else arg) +# return given(*strategies, **kwargs) + + +# OVERWRITTEN_STRATEGIES = { +# UISeqPrev: { +# "event": custom_builds(NavigationalEventField, old=st.just(1), new=st.just(0)) +# }, +# UISeqNext: { +# "event": custom_builds(NavigationalEventField, old=st.just(0), new=st.just(1)) +# }, +# BaseXapiContext: { +# "revision": False, +# "platform": False, +# }, +# BaseXapiResultScore: { +# "raw": False, +# "min": False, +# "max": False, +# }, +# LMSContextContextActivities: {"category": custom_builds(LMSProfileActivity)}, +# VideoContextContextActivities: {"category": custom_builds(VideoProfileActivity)}, +# VirtualClassroomContextContextActivities: { +# "category": custom_builds(VirtualClassroomProfileActivity) +# }, +# } diff --git a/tests/models/xapi/base/test_statements.py b/tests/models/xapi/base/test_statements.py index 03d8f38ff..1a6b0bc75 100644 --- a/tests/models/xapi/base/test_statements.py +++ b/tests/models/xapi/base/test_statements.py @@ -9,22 +9,22 @@ from pydantic import ValidationError from ralph.models.selector import ModelSelector -# from ralph.models.xapi.base.agents import BaseXapiAgentWithAccount -# from ralph.models.xapi.base.groups import ( -# BaseXapiAnonymousGroup, -# BaseXapiIdentifiedGroupWithAccount, -# BaseXapiIdentifiedGroupWithMbox, -# BaseXapiIdentifiedGroupWithMboxSha1Sum, -# BaseXapiIdentifiedGroupWithOpenId, -# ) -# from ralph.models.xapi.base.objects import BaseXapiSubStatement +from ralph.models.xapi.base.agents import BaseXapiAgentWithAccount +from ralph.models.xapi.base.groups import ( + BaseXapiAnonymousGroup, + BaseXapiIdentifiedGroupWithAccount, + BaseXapiIdentifiedGroupWithMbox, + BaseXapiIdentifiedGroupWithMboxSha1Sum, + BaseXapiIdentifiedGroupWithOpenId, +) +from ralph.models.xapi.base.objects import BaseXapiSubStatement from ralph.models.xapi.base.statements import BaseXapiStatement -# from ralph.models.xapi.base.unnested_objects import ( -# BaseXapiActivity, -# BaseXapiActivityInteractionDefinition, -# BaseXapiStatementRef, -# ) -# from ralph.utils import set_dict_value_from_path +from ralph.models.xapi.base.unnested_objects import ( + BaseXapiActivity, + BaseXapiActivityInteractionDefinition, + BaseXapiStatementRef, +) +from ralph.utils import set_dict_value_from_path # from tests.fixtures.hypothesis_strategies import custom_builds, custom_given From efb204513cc54abd974e901617e88e6fa32deae7 Mon Sep 17 00:00:00 2001 From: lleeoo Date: Tue, 12 Dec 2023 16:57:15 +0100 Subject: [PATCH 13/19] statements tests pass --- tests/factories.py | 22 +- tests/models/xapi/base/test_statements.py | 950 +++++++++++----------- tests/test_cli.py | 13 +- 3 files changed, 502 insertions(+), 483 deletions(-) diff --git a/tests/factories.py b/tests/factories.py index 3c37e0db1..0a08b1198 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -33,6 +33,8 @@ VideoProfileActivity, ) +from ralph.models.xapi.virtual_classroom.contexts import VirtualClassroomStartedPollContextActivities, VirtualClassroomPostedPublicMessageContextActivities + from ralph.models.xapi.virtual_classroom.contexts import ( VirtualClassroomContextContextActivities, VirtualClassroomProfileActivity, @@ -149,13 +151,27 @@ class VideoContextContextActivitiesFactory(ModelFactory[VideoContextContextActiv category = lambda: mock_instance(VideoProfileActivity) -class VirtualClassroomContextContextActivitiesFactory(ModelFactory[VirtualClassroomContextContextActivities]): - __model__ = VirtualClassroomContextContextActivities + +class VirtualClassroomStartedPollContextActivitiesFactory(ModelFactory[VirtualClassroomStartedPollContextActivities]): + __model__ = VirtualClassroomStartedPollContextActivities + __set_as_default_factory_for_type__ = True + + category = lambda: mock_instance(VirtualClassroomProfileActivity) + +class VirtualClassroomAnsweredPollContextActivitiesFactory(ModelFactory[VirtualClassroomAnsweredPollContextActivities]): + __model__ = VirtualClassroomAnsweredPollContextActivities __set_as_default_factory_for_type__ = True category = lambda: mock_instance(VirtualClassroomProfileActivity) -# class VirtualClassroomAnsweredPollContextActivitiesFactory(ModelFactory[VirtualClassroomAnsweredPollContextActivities]): +class VirtualClassroomPostedPublicMessageContextActivitiesFactory(ModelFactory[VirtualClassroomPostedPublicMessageContextActivities]): + __model__ = VirtualClassroomPostedPublicMessageContextActivities + __set_as_default_factory_for_type__ = True + + category = lambda: mock_instance(VirtualClassroomProfileActivity) + + +# class VirtualClassroomAnsweredPollFactory(ModelFactory[VirtualClassroomAnsweredPollContextActivities]): # __model__ = VirtualClassroomAnsweredPollContextActivities # category = lambda: mock_instance(VirtualClassroomProfileActivity) diff --git a/tests/models/xapi/base/test_statements.py b/tests/models/xapi/base/test_statements.py index 1a6b0bc75..1f0c8cfa6 100644 --- a/tests/models/xapi/base/test_statements.py +++ b/tests/models/xapi/base/test_statements.py @@ -31,481 +31,481 @@ from tests.factories import mock_instance, ModelFactory -# @pytest.mark.parametrize( -# "path", -# ["id", "stored", "verb__display", "result__score__raw"], -# ) -# @pytest.mark.parametrize("value", [None, "", {}]) -# def test_models_xapi_base_statement_with_invalid_null_values(path, value): -# """Test that the statement does not accept any null values. - -# XAPI-00001 -# An LRS rejects with error code 400 Bad Request any Statement having a property whose -# value is set to "null", an empty object, or has no value, except in an "extensions" -# property. -# """ -# statement = mock_instance(BaseXapiStatement) - -# statement = statement.dict(exclude_none=True) -# set_dict_value_from_path(statement, path.split("__"), value) - -# with pytest.raises(ValidationError, match="invalid empty value"): -# BaseXapiStatement(**statement) - - -# @pytest.mark.parametrize( -# "path", -# [ -# "object__definition__extensions__https://w3id.org/xapi/video", -# "result__extensions__https://w3id.org/xapi/video", -# "context__extensions__https://w3id.org/xapi/video", -# ], -# ) -# @pytest.mark.parametrize("value", [None, "", {}]) -# def test_models_xapi_base_statement_with_valid_null_values(path, value): -# """Test that the statement does accept valid null values in extensions fields. - -# XAPI-00001 -# An LRS rejects with error code 400 Bad Request any Statement having a property whose -# value is set to "null", an empty object, or has no value, except in an "extensions" -# property. -# """ - -# statement = mock_instance(BaseXapiStatement, object=mock_instance(BaseXapiActivity)) - -# statement = statement.dict(exclude_none=True) - -# set_dict_value_from_path(statement, path.split("__"), value) -# try: -# BaseXapiStatement(**statement) -# except ValidationError as err: -# pytest.fail(f"Valid statement should not raise exceptions: {err}") - - -# @pytest.mark.parametrize("path", ["object__definition__correctResponsesPattern"]) -# def test_models_xapi_base_statement_with_valid_empty_array(path): -# """Test that the statement does accept a valid empty array. - -# Where the Correct Responses Pattern contains an empty array, the meaning of this is -# that there is no correct answer. -# """ - -# statement = mock_instance( -# BaseXapiStatement, -# object=mock_instance( -# BaseXapiActivity, -# definition=mock_instance(BaseXapiActivityInteractionDefinition), -# ), -# ) - -# statement = statement.dict(exclude_none=True) -# set_dict_value_from_path(statement, path.split("__"), []) -# try: -# BaseXapiStatement(**statement) -# except ValidationError as err: -# pytest.fail(f"Valid statement should not raise exceptions: {err}") - - -# @pytest.mark.parametrize( -# "field", -# ["actor", "verb", "object"], -# ) -# def test_models_xapi_base_statement_must_use_actor_verb_and_object(field): -# """Test that the statement raises an exception if required fields are missing. - -# XAPI-00003 -# An LRS rejects with error code 400 Bad Request a Statement which does not contain an -# "actor" property. -# XAPI-00004 -# An LRS rejects with error code 400 Bad Request a Statement which does not contain a -# "verb" property. -# XAPI-00005 -# An LRS rejects with error code 400 Bad Request a Statement which does not contain an -# "object" property. -# """ - -# statement = mock_instance(BaseXapiStatement) - -# statement = statement.dict(exclude_none=True) -# del statement["context"] # Necessary as context leads to another validation error -# del statement[field] -# with pytest.raises(ValidationError, match="field required"): -# BaseXapiStatement(**statement) - - -# @pytest.mark.parametrize( -# "path,value", -# [ -# ("actor__name", 1), # Should be a string -# ("actor__account__name", 1), # Should be a string -# ("actor__account__homePage", 1), # Should be an IRI -# ("actor__account", ["foo", "bar"]), # Should be a dictionary -# ("verb__display", ["foo"]), # Should be a dictionary -# ("verb__display", {"en": 1}), # Should have string values -# ("object__id", ["foo"]), # Should be an IRI -# ], -# ) -# def test_models_xapi_base_statement_with_invalid_data_types(path, value): -# """Test that the statement does not accept values with wrong types. - -# XAPI-00006 -# An LRS rejects with error code 400 Bad Request a Statement which uses the wrong data -# type. -# """ -# statement = mock_instance( -# BaseXapiStatement, actor=mock_instance(BaseXapiAgentWithAccount) -# ) - -# statement = statement.dict(exclude_none=True) -# set_dict_value_from_path(statement, path.split("__"), value) - -# err = "(type expected|not a valid dict|expected string )" - -# with pytest.raises(ValidationError, match=err): -# BaseXapiStatement(**statement) - - -# @pytest.mark.parametrize( -# "path,value", -# [ -# ("id", "0545fe73-1bbd-4f84-9c9a"), # Should be a valid UUID -# ("actor", {"mbox": "example@mail.com"}), # Should be a Mailto IRI -# ("verb__display", {"bad language tag": "foo"}), # Should be a valid LanguageTag -# ("object__id", ["This is not an IRI"]), # Should be an IRI -# ], -# ) -# def test_models_xapi_base_statement_with_invalid_data_format(path, value): -# """Test that the statement does not accept values having a wrong format. - -# XAPI-00007 -# An LRS rejects with error code 400 Bad Request a Statement which uses any -# non-format-following key or value, including the empty string, where a string with a -# particular format (such as mailto IRI, UUID, or IRI) is required. -# (Empty strings are covered by XAPI-00001) -# """ -# statement = mock_instance( -# BaseXapiStatement, actor=mock_instance(BaseXapiAgentWithAccount) -# ) - -# statement = statement.dict(exclude_none=True) -# set_dict_value_from_path(statement, path.split("__"), value) -# err = "(string does not match regex|Invalid RFC 5646 Language tag|not a valid uuid)" -# with pytest.raises(ValidationError, match=err): -# BaseXapiStatement(**statement) - - -# @pytest.mark.parametrize("path,value", [("actor__objecttype", "Agent")]) -# def test_models_xapi_base_statement_with_invalid_letter_cases(path, value): -# """Test that the statement does not accept keys having invalid letter cases. - -# XAPI-00008 -# An LRS rejects with error code 400 Bad Request a Statement where the case of a key -# does not match the case specified in this specification. -# """ -# statement = mock_instance( -# BaseXapiStatement, actor=mock_instance(BaseXapiAgentWithAccount) -# ) - -# statement = statement.dict(exclude_none=True) -# if statement["actor"].get("objectType", None): -# del statement["actor"]["objectType"] -# set_dict_value_from_path(statement, path.split("__"), value) -# err = "(unexpected value|extra fields not permitted)" -# with pytest.raises(ValidationError, match=err): -# BaseXapiStatement(**statement) - - -# def test_models_xapi_base_statement_should_not_accept_additional_properties(): -# """Test that the statement does not accept additional properties. - -# XAPI-00010 -# An LRS rejects with error code 400 Bad Request a Statement where a key or value is -# not allowed by this specification. -# """ -# statement = mock_instance(BaseXapiStatement) - -# invalid_statement = statement.dict(exclude_none=True) -# invalid_statement["NEW_INVALID_FIELD"] = "some value" -# with pytest.raises(ValidationError, match="extra fields not permitted"): -# BaseXapiStatement(**invalid_statement) - - -# @pytest.mark.parametrize("path,value", [("object__id", "w3id.org/xapi/video")]) -# def test_models_xapi_base_statement_with_iri_without_scheme(path, value): -# """Test that the statement does not accept IRIs without a scheme. - -# XAPI-00011 -# An LRS rejects with error code 400 Bad Request a Statement containing IRL or IRI -# values without a scheme. -# """ -# statement = mock_instance(BaseXapiStatement) - -# statement = statement.dict(exclude_none=True) -# set_dict_value_from_path(statement, path.split("__"), value) -# with pytest.raises(ValidationError, match="is not a valid 'IRI'"): -# BaseXapiStatement(**statement) - - -# @pytest.mark.parametrize( -# "path", -# [ -# "object__definition__extensions__foo", -# "result__extensions__1", -# "context__extensions__w3id.org/xapi/video", -# ], -# ) -# def test_models_xapi_base_statement_with_invalid_extensions(path): -# """Test that the statement does not accept extensions keys with invalid IRIs. - -# XAPI-00118 -# An Extension "key" is an IRI. The LRS rejects with 400 a statement which has an -# extension key which is not a valid IRI, if an extension object is present. -# """ -# statement = mock_instance(BaseXapiStatement, object=mock_instance(BaseXapiActivity)) - -# statement = statement.dict(exclude_none=True) -# set_dict_value_from_path(statement, path.split("__"), "") -# with pytest.raises(ValidationError, match="is not a valid 'IRI'"): -# BaseXapiStatement(**statement) - - -# @pytest.mark.parametrize("path,value", [("actor__mbox", "mailto:example@mail.com")]) -# def test_models_xapi_base_statement_with_two_agent_types(path, value): -# """Test that the statement does not accept multiple agent types. - -# An Agent MUST NOT include more than one Inverse Functional Identifier. -# """ -# statement = mock_instance( -# BaseXapiStatement, actor=mock_instance(BaseXapiAgentWithAccount) -# ) - -# statement = statement.dict(exclude_none=True) -# set_dict_value_from_path(statement, path.split("__"), value) -# with pytest.raises(ValidationError, match="extra fields not permitted"): -# BaseXapiStatement(**statement) - - -# def test_models_xapi_base_statement_missing_member_property(): -# """Test that the statement does not accept group agents with missing members. - -# An Anonymous Group MUST include a "member" property listing constituent Agents. -# """ -# statement = mock_instance( -# BaseXapiStatement, actor=mock_instance(BaseXapiAnonymousGroup) -# ) - -# statement = statement.dict(exclude_none=True) -# del statement["actor"]["member"] -# with pytest.raises(ValidationError, match="member\n field required"): -# BaseXapiStatement(**statement) - - -# @pytest.mark.parametrize( -# "klass", -# [ -# BaseXapiAnonymousGroup, -# BaseXapiIdentifiedGroupWithMbox, -# BaseXapiIdentifiedGroupWithMboxSha1Sum, -# BaseXapiIdentifiedGroupWithOpenId, -# BaseXapiIdentifiedGroupWithAccount, -# ], -# ) -# def test_models_xapi_base_statement_with_invalid_group_objects(klass): -# """Test that the statement does not accept group agents with group members. - -# An Anonymous Group MUST NOT contain Group Objects in the "member" identifiers. -# An Identified Group MUST NOT contain Group Objects in the "member" property. -# """ - -# actor_class = ModelFactory.__random__.choice( -# [ -# BaseXapiAnonymousGroup, -# BaseXapiIdentifiedGroupWithMbox, -# BaseXapiIdentifiedGroupWithMboxSha1Sum, -# BaseXapiIdentifiedGroupWithOpenId, -# BaseXapiIdentifiedGroupWithAccount, -# ] -# ) -# statement = mock_instance(BaseXapiStatement, actor=mock_instance(actor_class)) - -# kwargs = {"exclude_none": True} -# statement = statement.dict(**kwargs) -# statement["actor"]["member"] = [mock_instance(klass).dict(**kwargs)] # TODO: check that nothing was lost -# err = "actor -> member -> 0 -> objectType\n unexpected value; permitted: 'Agent'" -# with pytest.raises(ValidationError, match=err): -# BaseXapiStatement(**statement) - - -# @pytest.mark.parametrize("path,value", [("actor__mbox", "mailto:example@mail.com")]) -# def test_models_xapi_base_statement_with_two_group_identifiers(path, value): -# """Test that the statement does not accept multiple group identifiers. - -# An Identified Group MUST include exactly one Inverse Functional Identifier. -# """ -# statement = mock_instance( -# BaseXapiStatement, actor=mock_instance(BaseXapiIdentifiedGroupWithAccount) -# ) - -# statement = statement.dict(exclude_none=True) -# set_dict_value_from_path(statement, path.split("__"), value) -# with pytest.raises(ValidationError, match="extra fields not permitted"): -# BaseXapiStatement(**statement) - - -# @pytest.mark.parametrize( -# "path,value", -# [ -# ("object__id", "156e3f9f-4b56-4cba-ad52-1bd19e461d65"), -# ("object__stored", "2013-05-18T05:32:34.804+00:00"), -# ("object__version", "1.0.0"), -# ("object__authority", {"mbox": "mailto:example@mail.com"}), -# ], -# ) -# def test_models_xapi_base_statement_with_sub_statement_ref(path, value): -# """Test that the sub-statement does not accept invalid properties. - -# A SubStatement MUST NOT have the "id", "stored", "version" or "authority" -# properties. -# """ -# statement = mock_instance( -# BaseXapiStatement, object=mock_instance(BaseXapiSubStatement) -# ) - -# statement = statement.dict(exclude_none=True) -# set_dict_value_from_path(statement, path.split("__"), value) -# with pytest.raises(ValidationError, match="extra fields not permitted"): -# BaseXapiStatement(**statement) - - -# @pytest.mark.parametrize( -# "value", -# [ -# [{"id": "invalid whitespace"}], -# [{"id": "valid"}, {"id": "invalid whitespace"}], -# [{"id": "invalid_duplicate"}, {"id": "invalid_duplicate"}], -# ], -# ) -# def test_models_xapi_base_statement_with_invalid_interaction_object(value): -# """Test that the statement does not accept invalid interaction fields. - -# An interaction component's id value SHOULD NOT have whitespace. -# Within an array of interaction components, all id values MUST be distinct. -# """ -# statement = mock_instance( -# BaseXapiStatement, -# object=mock_instance( -# BaseXapiActivity, -# definition=mock_instance(BaseXapiActivityInteractionDefinition), -# ), -# ) - -# statement = statement.dict(exclude_none=True) -# path = "object.definition.scale".split(".") -# set_dict_value_from_path(statement, path, value) -# err = "(Duplicate InteractionComponents are not valid|string does not match regex)" -# with pytest.raises(ValidationError, match=err): -# BaseXapiStatement(**statement) - - -# @pytest.mark.parametrize( -# "path,value", -# [ -# ("context__revision", "Format is free"), -# ("context__platform", "FUN MOOC"), -# ], -# ) -# def test_models_xapi_base_statement_with_invalid_context_value(path, value): -# """Test that the statement does not accept an invalid revision/platform value. - -# The "revision" property MUST only be used if the Statement's Object is an Activity. -# The "platform" property MUST only be used if the Statement's Object is an Activity. -# """ - -# object_class = ModelFactory.__random__.choice( -# [ -# BaseXapiSubStatement, -# BaseXapiStatementRef, -# ] -# ) -# statement = mock_instance(BaseXapiStatement, object=mock_instance(object_class)) - - -# statement = statement.dict(exclude_none=True) -# set_dict_value_from_path(statement, path.split("__"), value) -# err = "properties can only be used if the Statement's Object is an Activity" -# with pytest.raises(ValidationError, match=err): -# BaseXapiStatement(**statement) - - -# @pytest.mark.parametrize("path", ["context.contextActivities.not_parent"]) -# def test_models_xapi_base_statement_with_invalid_context_activities(path): -# """Test that the statement does not accept invalid context activity properties. - -# Every key in the contextActivities Object MUST be one of parent, grouping, category, -# or other. -# """ -# statement = mock_instance(BaseXapiStatement) - -# statement = statement.dict(exclude_none=True) -# set_dict_value_from_path(statement, path.split("."), {"id": "http://w3id.org/xapi"}) -# with pytest.raises(ValidationError, match="extra fields not permitted"): -# BaseXapiStatement(**statement) - - -# @pytest.mark.parametrize( -# "value", -# [ -# {"id": "http://w3id.org/xapi"}, -# [{"id": "http://w3id.org/xapi"}], -# [{"id": "http://w3id.org/xapi"}, {"id": "http://w3id.org/xapi/video"}], -# ], -# ) -# def test_models_xapi_base_statement_with_valid_context_activities(value): -# """Test that the statement does accept valid context activities fields. - -# Every value in the contextActivities Object MUST be either a single Activity Object -# or an array of Activity Objects. -# """ -# statement = mock_instance(BaseXapiStatement) - -# statement = statement.dict(exclude_none=True) -# path = ["context", "contextActivities"] -# for activity in ["parent", "grouping", "category", "other"]: -# set_dict_value_from_path(statement, path + [activity], value) -# try: -# BaseXapiStatement(**statement) -# except ValidationError as err: -# pytest.fail(f"Valid statement should not raise exceptions: {err}") - - -# @pytest.mark.parametrize("value", ["0.0.0", "1.1.0", "1", "2", "1.10.1", "1.0.1.1"]) -# def test_models_xapi_base_statement_with_invalid_version(value): -# """Test that the statement does not accept an invalid version field. - -# An LRS MUST reject all Statements with a version specified that does not start with -# 1.0. -# """ -# statement = mock_instance(BaseXapiStatement) - -# statement = statement.dict(exclude_none=True) -# set_dict_value_from_path(statement, ["version"], value) -# with pytest.raises(ValidationError, match="version\n string does not match regex"): -# BaseXapiStatement(**statement) - - -# def test_models_xapi_base_statement_with_valid_version(): -# """Test that the statement does accept a valid version field. - -# Statements returned by an LRS MUST retain the version they are accepted with. -# If they lack a version, the version MUST be set to 1.0.0. -# """ -# statement = mock_instance(BaseXapiStatement) - -# statement = statement.dict(exclude_none=True) -# set_dict_value_from_path(statement, ["version"], "1.0.3") -# assert "1.0.3" == BaseXapiStatement(**statement).dict()["version"] -# del statement["version"] -# assert "1.0.0" == BaseXapiStatement(**statement).dict()["version"] +@pytest.mark.parametrize( + "path", + ["id", "stored", "verb__display", "result__score__raw"], +) +@pytest.mark.parametrize("value", [None, "", {}]) +def test_models_xapi_base_statement_with_invalid_null_values(path, value): + """Test that the statement does not accept any null values. + + XAPI-00001 + An LRS rejects with error code 400 Bad Request any Statement having a property whose + value is set to "null", an empty object, or has no value, except in an "extensions" + property. + """ + statement = mock_instance(BaseXapiStatement) + + statement = statement.dict(exclude_none=True) + set_dict_value_from_path(statement, path.split("__"), value) + + with pytest.raises(ValidationError, match="invalid empty value"): + BaseXapiStatement(**statement) + + +@pytest.mark.parametrize( + "path", + [ + "object__definition__extensions__https://w3id.org/xapi/video", + "result__extensions__https://w3id.org/xapi/video", + "context__extensions__https://w3id.org/xapi/video", + ], +) +@pytest.mark.parametrize("value", [None, "", {}]) +def test_models_xapi_base_statement_with_valid_null_values(path, value): + """Test that the statement does accept valid null values in extensions fields. + + XAPI-00001 + An LRS rejects with error code 400 Bad Request any Statement having a property whose + value is set to "null", an empty object, or has no value, except in an "extensions" + property. + """ + + statement = mock_instance(BaseXapiStatement, object=mock_instance(BaseXapiActivity)) + + statement = statement.dict(exclude_none=True) + + set_dict_value_from_path(statement, path.split("__"), value) + try: + BaseXapiStatement(**statement) + except ValidationError as err: + pytest.fail(f"Valid statement should not raise exceptions: {err}") + + +@pytest.mark.parametrize("path", ["object__definition__correctResponsesPattern"]) +def test_models_xapi_base_statement_with_valid_empty_array(path): + """Test that the statement does accept a valid empty array. + + Where the Correct Responses Pattern contains an empty array, the meaning of this is + that there is no correct answer. + """ + + statement = mock_instance( + BaseXapiStatement, + object=mock_instance( + BaseXapiActivity, + definition=mock_instance(BaseXapiActivityInteractionDefinition), + ), + ) + + statement = statement.dict(exclude_none=True) + set_dict_value_from_path(statement, path.split("__"), []) + try: + BaseXapiStatement(**statement) + except ValidationError as err: + pytest.fail(f"Valid statement should not raise exceptions: {err}") + + +@pytest.mark.parametrize( + "field", + ["actor", "verb", "object"], +) +def test_models_xapi_base_statement_must_use_actor_verb_and_object(field): + """Test that the statement raises an exception if required fields are missing. + + XAPI-00003 + An LRS rejects with error code 400 Bad Request a Statement which does not contain an + "actor" property. + XAPI-00004 + An LRS rejects with error code 400 Bad Request a Statement which does not contain a + "verb" property. + XAPI-00005 + An LRS rejects with error code 400 Bad Request a Statement which does not contain an + "object" property. + """ + + statement = mock_instance(BaseXapiStatement) + + statement = statement.dict(exclude_none=True) + del statement["context"] # Necessary as context leads to another validation error + del statement[field] + with pytest.raises(ValidationError, match="field required"): + BaseXapiStatement(**statement) + + +@pytest.mark.parametrize( + "path,value", + [ + ("actor__name", 1), # Should be a string + ("actor__account__name", 1), # Should be a string + ("actor__account__homePage", 1), # Should be an IRI + ("actor__account", ["foo", "bar"]), # Should be a dictionary + ("verb__display", ["foo"]), # Should be a dictionary + ("verb__display", {"en": 1}), # Should have string values + ("object__id", ["foo"]), # Should be an IRI + ], +) +def test_models_xapi_base_statement_with_invalid_data_types(path, value): + """Test that the statement does not accept values with wrong types. + + XAPI-00006 + An LRS rejects with error code 400 Bad Request a Statement which uses the wrong data + type. + """ + statement = mock_instance( + BaseXapiStatement, actor=mock_instance(BaseXapiAgentWithAccount) + ) + + statement = statement.dict(exclude_none=True) + set_dict_value_from_path(statement, path.split("__"), value) + + err = "(type expected|not a valid dict|expected string )" + + with pytest.raises(ValidationError, match=err): + BaseXapiStatement(**statement) + + +@pytest.mark.parametrize( + "path,value", + [ + ("id", "0545fe73-1bbd-4f84-9c9a"), # Should be a valid UUID + ("actor", {"mbox": "example@mail.com"}), # Should be a Mailto IRI + ("verb__display", {"bad language tag": "foo"}), # Should be a valid LanguageTag + ("object__id", ["This is not an IRI"]), # Should be an IRI + ], +) +def test_models_xapi_base_statement_with_invalid_data_format(path, value): + """Test that the statement does not accept values having a wrong format. + + XAPI-00007 + An LRS rejects with error code 400 Bad Request a Statement which uses any + non-format-following key or value, including the empty string, where a string with a + particular format (such as mailto IRI, UUID, or IRI) is required. + (Empty strings are covered by XAPI-00001) + """ + statement = mock_instance( + BaseXapiStatement, actor=mock_instance(BaseXapiAgentWithAccount) + ) + + statement = statement.dict(exclude_none=True) + set_dict_value_from_path(statement, path.split("__"), value) + err = "(string does not match regex|Invalid RFC 5646 Language tag|not a valid uuid)" + with pytest.raises(ValidationError, match=err): + BaseXapiStatement(**statement) + + +@pytest.mark.parametrize("path,value", [("actor__objecttype", "Agent")]) +def test_models_xapi_base_statement_with_invalid_letter_cases(path, value): + """Test that the statement does not accept keys having invalid letter cases. + + XAPI-00008 + An LRS rejects with error code 400 Bad Request a Statement where the case of a key + does not match the case specified in this specification. + """ + statement = mock_instance( + BaseXapiStatement, actor=mock_instance(BaseXapiAgentWithAccount) + ) + + statement = statement.dict(exclude_none=True) + if statement["actor"].get("objectType", None): + del statement["actor"]["objectType"] + set_dict_value_from_path(statement, path.split("__"), value) + err = "(unexpected value|extra fields not permitted)" + with pytest.raises(ValidationError, match=err): + BaseXapiStatement(**statement) + + +def test_models_xapi_base_statement_should_not_accept_additional_properties(): + """Test that the statement does not accept additional properties. + + XAPI-00010 + An LRS rejects with error code 400 Bad Request a Statement where a key or value is + not allowed by this specification. + """ + statement = mock_instance(BaseXapiStatement) + + invalid_statement = statement.dict(exclude_none=True) + invalid_statement["NEW_INVALID_FIELD"] = "some value" + with pytest.raises(ValidationError, match="extra fields not permitted"): + BaseXapiStatement(**invalid_statement) + + +@pytest.mark.parametrize("path,value", [("object__id", "w3id.org/xapi/video")]) +def test_models_xapi_base_statement_with_iri_without_scheme(path, value): + """Test that the statement does not accept IRIs without a scheme. + + XAPI-00011 + An LRS rejects with error code 400 Bad Request a Statement containing IRL or IRI + values without a scheme. + """ + statement = mock_instance(BaseXapiStatement) + + statement = statement.dict(exclude_none=True) + set_dict_value_from_path(statement, path.split("__"), value) + with pytest.raises(ValidationError, match="is not a valid 'IRI'"): + BaseXapiStatement(**statement) + + +@pytest.mark.parametrize( + "path", + [ + "object__definition__extensions__foo", + "result__extensions__1", + "context__extensions__w3id.org/xapi/video", + ], +) +def test_models_xapi_base_statement_with_invalid_extensions(path): + """Test that the statement does not accept extensions keys with invalid IRIs. + + XAPI-00118 + An Extension "key" is an IRI. The LRS rejects with 400 a statement which has an + extension key which is not a valid IRI, if an extension object is present. + """ + statement = mock_instance(BaseXapiStatement, object=mock_instance(BaseXapiActivity)) + + statement = statement.dict(exclude_none=True) + set_dict_value_from_path(statement, path.split("__"), "") + with pytest.raises(ValidationError, match="is not a valid 'IRI'"): + BaseXapiStatement(**statement) + + +@pytest.mark.parametrize("path,value", [("actor__mbox", "mailto:example@mail.com")]) +def test_models_xapi_base_statement_with_two_agent_types(path, value): + """Test that the statement does not accept multiple agent types. + + An Agent MUST NOT include more than one Inverse Functional Identifier. + """ + statement = mock_instance( + BaseXapiStatement, actor=mock_instance(BaseXapiAgentWithAccount) + ) + + statement = statement.dict(exclude_none=True) + set_dict_value_from_path(statement, path.split("__"), value) + with pytest.raises(ValidationError, match="extra fields not permitted"): + BaseXapiStatement(**statement) + + +def test_models_xapi_base_statement_missing_member_property(): + """Test that the statement does not accept group agents with missing members. + + An Anonymous Group MUST include a "member" property listing constituent Agents. + """ + statement = mock_instance( + BaseXapiStatement, actor=mock_instance(BaseXapiAnonymousGroup) + ) + + statement = statement.dict(exclude_none=True) + del statement["actor"]["member"] + with pytest.raises(ValidationError, match="member\n field required"): + BaseXapiStatement(**statement) + + +@pytest.mark.parametrize( + "klass", + [ + BaseXapiAnonymousGroup, + BaseXapiIdentifiedGroupWithMbox, + BaseXapiIdentifiedGroupWithMboxSha1Sum, + BaseXapiIdentifiedGroupWithOpenId, + BaseXapiIdentifiedGroupWithAccount, + ], +) +def test_models_xapi_base_statement_with_invalid_group_objects(klass): + """Test that the statement does not accept group agents with group members. + + An Anonymous Group MUST NOT contain Group Objects in the "member" identifiers. + An Identified Group MUST NOT contain Group Objects in the "member" property. + """ + + actor_class = ModelFactory.__random__.choice( + [ + BaseXapiAnonymousGroup, + BaseXapiIdentifiedGroupWithMbox, + BaseXapiIdentifiedGroupWithMboxSha1Sum, + BaseXapiIdentifiedGroupWithOpenId, + BaseXapiIdentifiedGroupWithAccount, + ] + ) + statement = mock_instance(BaseXapiStatement, actor=mock_instance(actor_class)) + + kwargs = {"exclude_none": True} + statement = statement.dict(**kwargs) + statement["actor"]["member"] = [mock_instance(klass).dict(**kwargs)] # TODO: check that nothing was lost + err = "actor -> member -> 0 -> objectType\n unexpected value; permitted: 'Agent'" + with pytest.raises(ValidationError, match=err): + BaseXapiStatement(**statement) + + +@pytest.mark.parametrize("path,value", [("actor__mbox", "mailto:example@mail.com")]) +def test_models_xapi_base_statement_with_two_group_identifiers(path, value): + """Test that the statement does not accept multiple group identifiers. + + An Identified Group MUST include exactly one Inverse Functional Identifier. + """ + statement = mock_instance( + BaseXapiStatement, actor=mock_instance(BaseXapiIdentifiedGroupWithAccount) + ) + + statement = statement.dict(exclude_none=True) + set_dict_value_from_path(statement, path.split("__"), value) + with pytest.raises(ValidationError, match="extra fields not permitted"): + BaseXapiStatement(**statement) + + +@pytest.mark.parametrize( + "path,value", + [ + ("object__id", "156e3f9f-4b56-4cba-ad52-1bd19e461d65"), + ("object__stored", "2013-05-18T05:32:34.804+00:00"), + ("object__version", "1.0.0"), + ("object__authority", {"mbox": "mailto:example@mail.com"}), + ], +) +def test_models_xapi_base_statement_with_sub_statement_ref(path, value): + """Test that the sub-statement does not accept invalid properties. + + A SubStatement MUST NOT have the "id", "stored", "version" or "authority" + properties. + """ + statement = mock_instance( + BaseXapiStatement, object=mock_instance(BaseXapiSubStatement) + ) + + statement = statement.dict(exclude_none=True) + set_dict_value_from_path(statement, path.split("__"), value) + with pytest.raises(ValidationError, match="extra fields not permitted"): + BaseXapiStatement(**statement) + + +@pytest.mark.parametrize( + "value", + [ + [{"id": "invalid whitespace"}], + [{"id": "valid"}, {"id": "invalid whitespace"}], + [{"id": "invalid_duplicate"}, {"id": "invalid_duplicate"}], + ], +) +def test_models_xapi_base_statement_with_invalid_interaction_object(value): + """Test that the statement does not accept invalid interaction fields. + + An interaction component's id value SHOULD NOT have whitespace. + Within an array of interaction components, all id values MUST be distinct. + """ + statement = mock_instance( + BaseXapiStatement, + object=mock_instance( + BaseXapiActivity, + definition=mock_instance(BaseXapiActivityInteractionDefinition), + ), + ) + + statement = statement.dict(exclude_none=True) + path = "object.definition.scale".split(".") + set_dict_value_from_path(statement, path, value) + err = "(Duplicate InteractionComponents are not valid|string does not match regex)" + with pytest.raises(ValidationError, match=err): + BaseXapiStatement(**statement) + + +@pytest.mark.parametrize( + "path,value", + [ + ("context__revision", "Format is free"), + ("context__platform", "FUN MOOC"), + ], +) +def test_models_xapi_base_statement_with_invalid_context_value(path, value): + """Test that the statement does not accept an invalid revision/platform value. + + The "revision" property MUST only be used if the Statement's Object is an Activity. + The "platform" property MUST only be used if the Statement's Object is an Activity. + """ + + object_class = ModelFactory.__random__.choice( + [ + BaseXapiSubStatement, + BaseXapiStatementRef, + ] + ) + statement = mock_instance(BaseXapiStatement, object=mock_instance(object_class)) + + + statement = statement.dict(exclude_none=True) + set_dict_value_from_path(statement, path.split("__"), value) + err = "properties can only be used if the Statement's Object is an Activity" + with pytest.raises(ValidationError, match=err): + BaseXapiStatement(**statement) + + +@pytest.mark.parametrize("path", ["context.contextActivities.not_parent"]) +def test_models_xapi_base_statement_with_invalid_context_activities(path): + """Test that the statement does not accept invalid context activity properties. + + Every key in the contextActivities Object MUST be one of parent, grouping, category, + or other. + """ + statement = mock_instance(BaseXapiStatement) + + statement = statement.dict(exclude_none=True) + set_dict_value_from_path(statement, path.split("."), {"id": "http://w3id.org/xapi"}) + with pytest.raises(ValidationError, match="extra fields not permitted"): + BaseXapiStatement(**statement) + + +@pytest.mark.parametrize( + "value", + [ + {"id": "http://w3id.org/xapi"}, + [{"id": "http://w3id.org/xapi"}], + [{"id": "http://w3id.org/xapi"}, {"id": "http://w3id.org/xapi/video"}], + ], +) +def test_models_xapi_base_statement_with_valid_context_activities(value): + """Test that the statement does accept valid context activities fields. + + Every value in the contextActivities Object MUST be either a single Activity Object + or an array of Activity Objects. + """ + statement = mock_instance(BaseXapiStatement) + + statement = statement.dict(exclude_none=True) + path = ["context", "contextActivities"] + for activity in ["parent", "grouping", "category", "other"]: + set_dict_value_from_path(statement, path + [activity], value) + try: + BaseXapiStatement(**statement) + except ValidationError as err: + pytest.fail(f"Valid statement should not raise exceptions: {err}") + + +@pytest.mark.parametrize("value", ["0.0.0", "1.1.0", "1", "2", "1.10.1", "1.0.1.1"]) +def test_models_xapi_base_statement_with_invalid_version(value): + """Test that the statement does not accept an invalid version field. + + An LRS MUST reject all Statements with a version specified that does not start with + 1.0. + """ + statement = mock_instance(BaseXapiStatement) + + statement = statement.dict(exclude_none=True) + set_dict_value_from_path(statement, ["version"], value) + with pytest.raises(ValidationError, match="version\n string does not match regex"): + BaseXapiStatement(**statement) + + +def test_models_xapi_base_statement_with_valid_version(): + """Test that the statement does accept a valid version field. + + Statements returned by an LRS MUST retain the version they are accepted with. + If they lack a version, the version MUST be set to 1.0.0. + """ + statement = mock_instance(BaseXapiStatement) + + statement = statement.dict(exclude_none=True) + set_dict_value_from_path(statement, ["version"], "1.0.3") + assert "1.0.3" == BaseXapiStatement(**statement).dict()["version"] + del statement["version"] + assert "1.0.0" == BaseXapiStatement(**statement).dict()["version"] @pytest.mark.parametrize( diff --git a/tests/test_cli.py b/tests/test_cli.py index a859adea8..83dd6d3c4 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -33,7 +33,8 @@ WS_TEST_HOST, WS_TEST_PORT, ) -from tests.fixtures.hypothesis_strategies import custom_given +# from tests.fixtures.hypothesis_strategies import custom_given +from tests.factories import mock_instance test_logger = logging.getLogger("ralph") @@ -471,9 +472,10 @@ def test_cli_extract_command_with_es_parser(): assert "\n".join([json.dumps({"id": idx}) for idx in range(10)]) in result.output -@custom_given(UIPageClose) -def test_cli_validate_command_with_edx_format(event): +def test_cli_validate_command_with_edx_format(): """Test ralph validate command using the edx format.""" + event = mock_instance(UIPageClose) + event_str = event.json() runner = CliRunner() result = runner.invoke(cli, ["validate", "-f", "edx"], input=event_str) @@ -481,10 +483,11 @@ def test_cli_validate_command_with_edx_format(event): @hypothesis_settings(deadline=None) -@custom_given(UIPageClose) @pytest.mark.parametrize("valid_uuid", ["ee241f8b-174f-5bdb-bae9-c09de5fe017f"]) -def test_cli_convert_command_from_edx_to_xapi_format(valid_uuid, event): +def test_cli_convert_command_from_edx_to_xapi_format(valid_uuid): """Test ralph convert command from edx to xapi format.""" + event = mock_instance(UIPageClose) + event_str = event.json() runner = CliRunner() command = f"-v ERROR convert -f edx -t xapi -u {valid_uuid} -p https://fun-mooc.fr" From d721abddf03842e34ae451a48b4dcf9da584a6b0 Mon Sep 17 00:00:00 2001 From: lleeoo Date: Wed, 13 Dec 2023 10:12:04 +0100 Subject: [PATCH 14/19] wip --- src/ralph/models/xapi/video/results.py | 12 ++-- tests/api/test_forwarding.py | 59 +++++++--------- tests/factories.py | 44 ++++++++---- tests/fixtures/hypothesis_strategies.py | 32 ++++++++- .../edx/converters/xapi/test_enrollment.py | 16 +++-- .../edx/converters/xapi/test_navigational.py | 11 +-- .../models/edx/converters/xapi/test_server.py | 21 +++--- .../models/edx/converters/xapi/test_video.py | 38 ++++++---- tests/models/edx/navigational/test_events.py | 14 ++-- .../edx/navigational/test_statements.py | 2 +- .../open_response_assessment/test_events.py | 2 +- tests/models/edx/test_base.py | 14 ++-- tests/models/edx/test_browser.py | 13 ++-- tests/models/edx/test_enrollment.py | 34 +++++---- tests/models/edx/test_server.py | 9 +-- tests/models/test_converter.py | 21 +++--- tests/models/test_validator.py | 24 ++++--- tests/models/xapi/base/test_statements.py | 70 +++++++++---------- tests/test_cli.py | 5 +- 19 files changed, 260 insertions(+), 181 deletions(-) diff --git a/src/ralph/models/xapi/video/results.py b/src/ralph/models/xapi/video/results.py index 7c515ad53..359ef0b41 100644 --- a/src/ralph/models/xapi/video/results.py +++ b/src/ralph/models/xapi/video/results.py @@ -33,7 +33,7 @@ class VideoResultExtensions(BaseExtensionModelWithConfig): time (float): Consists of the video time code when the event was emitted. """ - time: NonNegativeFloat = Field(alias=RESULT_EXTENSION_TIME) + time: NonNegativeFloat = Field(alias=RESULT_EXTENSION_TIME, min=0) playedSegments: Optional[str] = Field(alias=CONTEXT_EXTENSION_PLAYED_SEGMENTS) @@ -44,7 +44,7 @@ class VideoPausedResultExtensions(VideoResultExtensions): progress (float): Consists of the ratio of media consumed by the actor. """ - progress: Optional[NonNegativeFloat] = Field(alias=RESULT_EXTENSION_PROGRESS) + progress: Optional[NonNegativeFloat] = Field(alias=RESULT_EXTENSION_PROGRESS, min=0) class VideoSeekedResultExtensions(BaseExtensionModelWithConfig): @@ -57,8 +57,8 @@ class VideoSeekedResultExtensions(BaseExtensionModelWithConfig): object during a seek operation. """ - timeFrom: NonNegativeFloat = Field(alias=RESULT_EXTENSION_TIME_FROM) - timeTo: NonNegativeFloat = Field(alias=RESULT_EXTENSION_TIME_TO) + timeFrom: NonNegativeFloat = Field(alias=RESULT_EXTENSION_TIME_FROM, min=0) + timeTo: NonNegativeFloat = Field(alias=RESULT_EXTENSION_TIME_TO, min=0) class VideoCompletedResultExtensions(VideoResultExtensions): @@ -68,7 +68,7 @@ class VideoCompletedResultExtensions(VideoResultExtensions): progress (float): Consists of the percentage of media consumed by the actor. """ - progress: NonNegativeFloat = Field(alias=RESULT_EXTENSION_PROGRESS) + progress: NonNegativeFloat = Field(alias=RESULT_EXTENSION_PROGRESS, min=0) class VideoTerminatedResultExtensions(VideoResultExtensions): @@ -78,7 +78,7 @@ class VideoTerminatedResultExtensions(VideoResultExtensions): progress (float): Consists of the percentage of media consumed by the actor. """ - progress: NonNegativeFloat = Field(alias=RESULT_EXTENSION_PROGRESS) + progress: NonNegativeFloat = Field(alias=RESULT_EXTENSION_PROGRESS, min=0) class VideoEnableClosedCaptioningResultExtensions(VideoResultExtensions): diff --git a/tests/api/test_forwarding.py b/tests/api/test_forwarding.py index 7f7db3ec2..68ff57bfb 100644 --- a/tests/api/test_forwarding.py +++ b/tests/api/test_forwarding.py @@ -13,15 +13,16 @@ from ralph.api.forwarding import forward_xapi_statements, get_active_xapi_forwardings from ralph.conf import Settings, XapiForwardingConfigurationSettings -from tests.fixtures.hypothesis_strategies import custom_builds, custom_given +# from tests.fixtures.hypothesis_strategies import custom_builds, custom_given +from tests.factories import mock_instance -@hypothesis_settings(suppress_health_check=(HealthCheck.function_scoped_fixture,)) -@custom_given(XapiForwardingConfigurationSettings) -def test_api_forwarding_with_valid_configuration(monkeypatch, forwarding_settings): +def test_api_forwarding_with_valid_configuration(monkeypatch, ): """Test the settings, given a valid forwarding configuration, should not raise an exception. """ + forwarding_settings = mock_instance(XapiForwardingConfigurationSettings) + monkeypatch.delenv("RALPH_XAPI_FORWARDINGS", raising=False) settings = Settings() @@ -33,16 +34,17 @@ def test_api_forwarding_with_valid_configuration(monkeypatch, forwarding_setting assert settings.XAPI_FORWARDINGS[0] == forwarding_settings -@hypothesis_settings(suppress_health_check=(HealthCheck.function_scoped_fixture,)) @pytest.mark.parametrize( "missing_key", ["url", "is_active", "basic_username", "basic_password", "max_retries", "timeout"], ) -@custom_given(XapiForwardingConfigurationSettings) -def test_api_forwarding_configuration_with_missing_field(missing_key, forwarding): +def test_api_forwarding_configuration_with_missing_field(missing_key): """Test the forwarding configuration, given a missing field, should raise a validation exception. """ + + forwarding = mock_instance(XapiForwardingConfigurationSettings) + forwarding_dict = json.loads(forwarding.json()) del forwarding_dict[missing_key] with pytest.raises(ValidationError, match=f"{missing_key}\n field required"): @@ -75,18 +77,17 @@ def test_api_forwarding_get_active_xapi_forwardings_with_empty_forwardings( assert caplog.record_tuples[0][2] == expected_log -@hypothesis_settings(suppress_health_check=(HealthCheck.function_scoped_fixture,)) -@custom_given( - custom_builds(XapiForwardingConfigurationSettings, is_active=st.just(True)), - custom_builds(XapiForwardingConfigurationSettings, is_active=st.just(False)), -) def test_api_forwarding_get_active_xapi_forwardings_with_inactive_forwardings( - monkeypatch, caplog, active_forwarding, inactive_forwarding + monkeypatch, caplog ): """Test that the get_active_xapi_forwardings function, given a forwarding configuration containing inactive forwardings, should log which forwarding configurations are inactive and return a list containing only active forwardings. """ + + active_forwarding = mock_instance(XapiForwardingConfigurationSettings, is_active=True) + inactive_forwarding = mock_instance(XapiForwardingConfigurationSettings, is_active=False) + active_forwarding_json = active_forwarding.json() inactive_forwarding_json = inactive_forwarding.json() @@ -124,24 +125,18 @@ def test_api_forwarding_get_active_xapi_forwardings_with_inactive_forwardings( @pytest.mark.anyio -@hypothesis_settings( - deadline=None, suppress_health_check=(HealthCheck.function_scoped_fixture,) -) @pytest.mark.parametrize("statements", [[{}, {"id": 1}]]) -@custom_given( - custom_builds( - XapiForwardingConfigurationSettings, - max_retries=st.just(1), - is_active=st.just(True), - ) -) async def test_api_forwarding_forward_xapi_statements_with_successful_request( - monkeypatch, caplog, statements, forwarding + monkeypatch, caplog, statements ): """Test the forward_xapi_statements function should log the forwarded statements count if the request was successful. """ + forwarding = mock_instance(XapiForwardingConfigurationSettings, + max_retries=1, + is_active=True) + class MockSuccessfulResponse: """Dummy Successful Response.""" @@ -172,22 +167,20 @@ async def post_success(*args, **kwargs): @pytest.mark.anyio -@hypothesis_settings(suppress_health_check=(HealthCheck.function_scoped_fixture,)) @pytest.mark.parametrize("statements", [[{}, {"id": 1}]]) -@custom_given( - custom_builds( - XapiForwardingConfigurationSettings, - max_retries=st.just(3), - is_active=st.just(True), - ) -) async def test_api_forwarding_forward_xapi_statements_with_unsuccessful_request( - monkeypatch, caplog, statements, forwarding + monkeypatch, caplog, statements ): """Test the forward_xapi_statements function should log the error if the request was successful. """ + forwarding = mock_instance( + XapiForwardingConfigurationSettings, + max_retries=3, + is_active=True, + ) + class MockUnsuccessfulResponse: """Dummy Failing Response.""" diff --git a/tests/factories.py b/tests/factories.py index 0a08b1198..44839d495 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -136,46 +136,42 @@ class BaseXapiContextFactory(ModelFactory[BaseXapiContext]): # lambda: BaseXapiContextContextActivitiesFactory.build() or Ignore() # ) - - - class LMSContextContextActivitiesFactory(ModelFactory[LMSContextContextActivities]): __model__ = LMSContextContextActivities __set_as_default_factory_for_type__ = True - category = lambda: mock_instance(LMSProfileActivity) # TODO: Uncomment + category = lambda: mock_xapi_instance(LMSProfileActivity) # TODO: Uncomment class VideoContextContextActivitiesFactory(ModelFactory[VideoContextContextActivities]): __model__ = VideoContextContextActivities __set_as_default_factory_for_type__ = True - category = lambda: mock_instance(VideoProfileActivity) - + category = lambda: mock_xapi_instance(VideoProfileActivity) class VirtualClassroomStartedPollContextActivitiesFactory(ModelFactory[VirtualClassroomStartedPollContextActivities]): __model__ = VirtualClassroomStartedPollContextActivities __set_as_default_factory_for_type__ = True - category = lambda: mock_instance(VirtualClassroomProfileActivity) + category = lambda: mock_xapi_instance(VirtualClassroomProfileActivity) class VirtualClassroomAnsweredPollContextActivitiesFactory(ModelFactory[VirtualClassroomAnsweredPollContextActivities]): __model__ = VirtualClassroomAnsweredPollContextActivities __set_as_default_factory_for_type__ = True - category = lambda: mock_instance(VirtualClassroomProfileActivity) + category = lambda: mock_xapi_instance(VirtualClassroomProfileActivity) class VirtualClassroomPostedPublicMessageContextActivitiesFactory(ModelFactory[VirtualClassroomPostedPublicMessageContextActivities]): __model__ = VirtualClassroomPostedPublicMessageContextActivities __set_as_default_factory_for_type__ = True - category = lambda: mock_instance(VirtualClassroomProfileActivity) + category = lambda: mock_xapi_instance(VirtualClassroomProfileActivity) # class VirtualClassroomAnsweredPollFactory(ModelFactory[VirtualClassroomAnsweredPollContextActivities]): # __model__ = VirtualClassroomAnsweredPollContextActivities -# category = lambda: mock_instance(VirtualClassroomProfileActivity) -# parent = lambda: mock_instance(VirtualClassroomActivity) # TODO: Remove this. Check that this is valid +# category = lambda: mock_xapi_instance(VirtualClassroomProfileActivity) +# parent = lambda: mock_xapi_instance(VirtualClassroomActivity) # TODO: Remove this. Check that this is valid @@ -183,16 +179,16 @@ class UISeqPrev(ModelFactory[UISeqPrev]): __model__ = UISeqPrev __set_as_default_factory_for_type__ = True - event = lambda: mock_instance(NavigationalEventField, old=1, new=0) + event = lambda: mock_xapi_instance(NavigationalEventField, old=1, new=0) class UISeqNext(ModelFactory[UISeqNext]): __model__ = UISeqNext __set_as_default_factory_for_type__ = True - event = lambda: mock_instance(NavigationalEventField, old=0, new=1) + event = lambda: mock_xapi_instance(NavigationalEventField, old=0, new=1) -def mock_instance(klass, *args, **kwargs): +def mock_xapi_instance(klass, *args, **kwargs): """Generate a mock instance of a given class (`klass`).""" # Avoid redifining custom factories @@ -204,6 +200,24 @@ class KlassFactory(ModelFactory[klass]): kwargs = KlassFactory.process_kwargs(*args, **kwargs) - kwargs = prune(kwargs) + kwargs = prune(kwargs, exceptions=["extensions"]) return klass(**kwargs) + +def mock_instance(klass, *args, **kwargs): + """Generate a mock instance of a given class (`klass`).""" + + # Avoid redifining custom factories + if klass not in BaseFactory._factory_type_mapping: + class KlassFactory(ModelFactory[klass]): + __model__ = klass + else: + KlassFactory = BaseFactory._factory_type_mapping[klass] + + kwargs = KlassFactory.process_kwargs(*args, **kwargs) + + return klass(**kwargs) + + +def mock_url(): + return ModelFactory.__faker__.url() \ No newline at end of file diff --git a/tests/fixtures/hypothesis_strategies.py b/tests/fixtures/hypothesis_strategies.py index b3a6073f7..56f43f8dc 100644 --- a/tests/fixtures/hypothesis_strategies.py +++ b/tests/fixtures/hypothesis_strategies.py @@ -3,8 +3,8 @@ # import random # from typing import Union -# from hypothesis import given -# from hypothesis import strategies as st +from hypothesis import given +from hypothesis import strategies as st # from pydantic import BaseModel # from ralph.models.edx.navigational.fields.events import NavigationalEventField @@ -94,13 +94,39 @@ # del optional[key] # return st.fixed_dictionaries(required, optional=optional).map(klass.parse_obj) -# def custom_given(*args: Union[st.SearchStrategy, BaseModel], **kwargs): +# def custom_given(*args: Union[BaseModel], **kwargs): # """Wrap the Hypothesis `given` function. Replace st.builds with custom_builds.""" # strategies = [] # for arg in args: # strategies.append(custom_builds(arg) if is_base_model(arg) else arg) # return given(*strategies, **kwargs) +from ralph.models.xapi.base.statements import BaseXapiStatement +from pydantic import BaseModel +from tests.factories import mock_instance, mock_xapi_instance + +# def custom_given(model: BaseModel, **kwargs): + +# if issubclass(model, BaseXapiStatement): +# func = mock_xapi_instance +# else: +# func = mock_instance +# return given(func(model, **kwargs)) + +# def custom_given(model, **mock_kwargs): +# def decorator(function): + +# def new_function(*args, **kwargs): + +# if issubclass(model, BaseXapiStatement): +# instance = mock_xapi_instance(**mock_kwargs) +# else: +# instance = mock_instance(**mock_kwargs) + +# return function(instance, *args, **kwargs) +# return new_function +# return decorator + # OVERWRITTEN_STRATEGIES = { # UISeqPrev: { diff --git a/tests/models/edx/converters/xapi/test_enrollment.py b/tests/models/edx/converters/xapi/test_enrollment.py index 56b57a2b7..105f4d542 100644 --- a/tests/models/edx/converters/xapi/test_enrollment.py +++ b/tests/models/edx/converters/xapi/test_enrollment.py @@ -16,17 +16,19 @@ EdxCourseEnrollmentDeactivated, ) -from tests.fixtures.hypothesis_strategies import custom_given +# from tests.fixtures.hypothesis_strategies import custom_given +from tests.factories import mock_instance, mock_url - -@custom_given(EdxCourseEnrollmentActivated, provisional.urls()) +# @custom_given(EdxCourseEnrollmentActivated, provisional.urls()) @pytest.mark.parametrize("uuid_namespace", ["ee241f8b-174f-5bdb-bae9-c09de5fe017f"]) def test_models_edx_converters_xapi_enrollment_edx_course_enrollment_activated_to_lms_registered_course( # noqa: E501 - uuid_namespace, event, platform_url + uuid_namespace#, event, platform_url ): """Test that converting with `EdxCourseEnrollmentActivatedToLMSRegisteredCourse` returns the expected xAPI statement. """ + event = mock_instance(EdxCourseEnrollmentActivated) + platform_url = mock_url() event.event.course_id = "edX/DemoX/Demo_Course" event.context.user_id = "1" @@ -66,15 +68,17 @@ def test_models_edx_converters_xapi_enrollment_edx_course_enrollment_activated_t } -@custom_given(EdxCourseEnrollmentDeactivated, provisional.urls()) +# @custom_given(EdxCourseEnrollmentDeactivated, provisional.urls()) @pytest.mark.parametrize("uuid_namespace", ["ee241f8b-174f-5bdb-bae9-c09de5fe017f"]) def test_models_edx_converters_xapi_enrollment_edx_course_enrollment_deactivated_to_lms_unregistered_course( # noqa: E501 - uuid_namespace, event, platform_url + uuid_namespace ): """Test that converting with `EdxCourseEnrollmentDeactivatedToLMSUnregisteredCourse` returns the expected xAPI statement. """ + event = mock_instance(EdxCourseEnrollmentDeactivated) + platform_url = mock_url() event.event.course_id = "edX/DemoX/Demo_Course" event.context.user_id = "1" diff --git a/tests/models/edx/converters/xapi/test_navigational.py b/tests/models/edx/converters/xapi/test_navigational.py index 011d1c622..7e4c9f532 100644 --- a/tests/models/edx/converters/xapi/test_navigational.py +++ b/tests/models/edx/converters/xapi/test_navigational.py @@ -10,17 +10,20 @@ from ralph.models.edx.converters.xapi.navigational import UIPageCloseToPageTerminated from ralph.models.edx.navigational.statements import UIPageClose -from tests.fixtures.hypothesis_strategies import custom_given +# from tests.fixtures.hypothesis_strategies import custom_given +from tests.factories import mock_instance, mock_url - -@custom_given(UIPageClose, provisional.urls()) @pytest.mark.parametrize("uuid_namespace", ["ee241f8b-174f-5bdb-bae9-c09de5fe017f"]) def test_models_edx_converters_xapi_navigational_ui_page_close_to_page_terminated( - uuid_namespace, event, platform_url + uuid_namespace ): """Test that converting with UIPageCloseToPageTerminated returns the expected xAPI statement. """ + event = mock_instance(UIPageClose) + platform_url = mock_url() + assert platform_url is not None # TODO: remove this + event.context.user_id = "1" event_str = event.json() event = json.loads(event_str) diff --git a/tests/models/edx/converters/xapi/test_server.py b/tests/models/edx/converters/xapi/test_server.py index 8c0244494..be113d938 100644 --- a/tests/models/edx/converters/xapi/test_server.py +++ b/tests/models/edx/converters/xapi/test_server.py @@ -10,17 +10,19 @@ from ralph.models.edx.converters.xapi.server import ServerEventToPageViewed from ralph.models.edx.server import Server -from tests.fixtures.hypothesis_strategies import custom_given +from tests.fixtures.hypothesis_ strategies import custom_given +from tests.factories import mock_instance, mock_url - -@custom_given(Server, provisional.urls()) @pytest.mark.parametrize("uuid_namespace", ["ee241f8b-174f-5bdb-bae9-c09de5fe017f"]) def test_models_edx_converters_xapi_server_server_event_to_page_viewed_constant_uuid( - uuid_namespace, event, platform_url + uuid_namespace ): """Test that `ServerEventToPageViewed.convert` returns a JSON string with a constant UUID. """ + event = mock_instance(Server) + platform_url = mock_url() + event_str = event.json() event = json.loads(event_str) xapi_event1 = convert_str_event( @@ -32,14 +34,16 @@ def test_models_edx_converters_xapi_server_server_event_to_page_viewed_constant_ assert xapi_event1.id == xapi_event2.id -@custom_given(Server, provisional.urls()) @pytest.mark.parametrize("uuid_namespace", ["ee241f8b-174f-5bdb-bae9-c09de5fe017f"]) def test_models_edx_converters_xapi_server_server_event_to_page_viewed( - uuid_namespace, event, platform_url + uuid_namespace ): """Test that converting with `ServerEventToPageViewed` returns the expected xAPI statement. """ + event = mock_instance(Server) + platform_url = mock_url() + event.event_type = "/main/blog" event.context.user_id = "1" event_str = event.json() @@ -67,13 +71,12 @@ def test_models_edx_converters_xapi_server_server_event_to_page_viewed( } -@settings(deadline=None) -@custom_given(Server, provisional.urls()) @pytest.mark.parametrize("uuid_namespace", ["ee241f8b-174f-5bdb-bae9-c09de5fe017f"]) def test_models_edx_converters_xapi_server_server_event_to_page_viewed_with_anonymous_user( # noqa: E501 - uuid_namespace, event, platform_url + uuid_namespace, event ): """Test that anonymous usernames are replaced with `anonymous`.""" + platform_url = mock_url() event.context.user_id = "" event_str = event.json() diff --git a/tests/models/edx/converters/xapi/test_video.py b/tests/models/edx/converters/xapi/test_video.py index 914c05a46..fa36971b6 100644 --- a/tests/models/edx/converters/xapi/test_video.py +++ b/tests/models/edx/converters/xapi/test_video.py @@ -22,17 +22,19 @@ UIStopVideo, ) -from tests.fixtures.hypothesis_strategies import custom_given +# from tests.fixtures.hypothesis_strategies import custom_given +from tests.factories import mock_instance, mock_url - -@custom_given(UILoadVideo, provisional.urls()) @pytest.mark.parametrize("uuid_namespace", ["ee241f8b-174f-5bdb-bae9-c09de5fe017f"]) def test_models_edx_converters_xapi_video_ui_load_video_to_video_initialized( - uuid_namespace, event, platform_url + uuid_namespace ): """Test that converting with `UILoadVideoToVideoInitialized` returns the expected xAPI statement. """ + event = mock_instance(UILoadVideo) + platform_url = mock_url() + event.context.user_id = "1" event.session = "af45a0e650c4a4fdb0bcde75a1e4b694" session_uuid = "af45a0e6-50c4-a4fd-b0bc-de75a1e4b694" @@ -80,14 +82,17 @@ def test_models_edx_converters_xapi_video_ui_load_video_to_video_initialized( } -@custom_given(UIPlayVideo, provisional.urls()) @pytest.mark.parametrize("uuid_namespace", ["ee241f8b-174f-5bdb-bae9-c09de5fe017f"]) def test_models_edx_converters_xapi_video_ui_play_video_to_video_played( - uuid_namespace, event, platform_url + uuid_namespace ): """Test that converting with `UIPlayVideoToVideoPlayed` returns the expected xAPI statement. """ + event = mock_instance(UIPlayVideo) + platform_url = mock_url() + + event.context.user_id = "1" event.session = "af45a0e650c4a4fdb0bcde75a1e4b694" session_uuid = "af45a0e6-50c4-a4fd-b0bc-de75a1e4b694" @@ -139,14 +144,16 @@ def test_models_edx_converters_xapi_video_ui_play_video_to_video_played( } -@custom_given(UIPauseVideo, provisional.urls()) @pytest.mark.parametrize("uuid_namespace", ["ee241f8b-174f-5bdb-bae9-c09de5fe017f"]) def test_models_edx_converters_xapi_video_ui_pause_video_to_video_paused( - uuid_namespace, event, platform_url + uuid_namespace ): """Test that converting with `UIPauseVideoToVideoPaused` returns the expected xAPI statement. """ + event = mock_instance(UIPauseVideo) + platform_url = mock_url() + event.context.user_id = "1" event.session = "af45a0e650c4a4fdb0bcde75a1e4b694" session_uuid = "af45a0e6-50c4-a4fd-b0bc-de75a1e4b694" @@ -199,14 +206,17 @@ def test_models_edx_converters_xapi_video_ui_pause_video_to_video_paused( } -@custom_given(UIStopVideo, provisional.urls()) @pytest.mark.parametrize("uuid_namespace", ["ee241f8b-174f-5bdb-bae9-c09de5fe017f"]) def test_models_edx_converters_xapi_video_ui_stop_video_to_video_terminated( - uuid_namespace, event, platform_url + uuid_namespace ): """Test that converting with `UIStopVideoToVideoTerminated` returns the expected xAPI statement. """ + event = mock_instance(UIStopVideo) + platform_url = mock_url() + + event.context.user_id = "1" event.session = "af45a0e650c4a4fdb0bcde75a1e4b694" session_uuid = "af45a0e6-50c4-a4fd-b0bc-de75a1e4b694" @@ -259,15 +269,17 @@ def test_models_edx_converters_xapi_video_ui_stop_video_to_video_terminated( "version": "1.0.0", } - -@custom_given(UISeekVideo, provisional.urls()) @pytest.mark.parametrize("uuid_namespace", ["ee241f8b-174f-5bdb-bae9-c09de5fe017f"]) def test_models_edx_converters_xapi_video_ui_seek_video_to_video_seeked( - uuid_namespace, event, platform_url + uuid_namespace ): """Test that converting with `UISeekVideoToVideoSeeked` returns the expected xAPI statement. """ + event = mock_instance(UISeekVideo) + platform_url = mock_url() + + event.context.user_id = "1" event.session = "af45a0e650c4a4fdb0bcde75a1e4b694" session_uuid = "af45a0e6-50c4-a4fd-b0bc-de75a1e4b694" diff --git a/tests/models/edx/navigational/test_events.py b/tests/models/edx/navigational/test_events.py index c40bbf016..30680af35 100644 --- a/tests/models/edx/navigational/test_events.py +++ b/tests/models/edx/navigational/test_events.py @@ -8,14 +8,14 @@ from ralph.models.edx.navigational.fields.events import NavigationalEventField -from tests.fixtures.hypothesis_strategies import custom_given +# from tests.fixtures.hypothesis_strategies import custom_given +from tests.factories import mock_instance - -@custom_given(NavigationalEventField) -def test_fields_edx_navigational_events_event_field_with_valid_content(field): +def test_fields_edx_navigational_events_event_field_with_valid_content(): """Test that a valid `NavigationalEventField` does not raise a `ValidationError`. """ + field = mock_instance(NavigationalEventField) assert re.match( ( @@ -53,10 +53,12 @@ def test_fields_edx_navigational_events_event_field_with_valid_content(field): ), ], ) -@custom_given(NavigationalEventField) -def test_fields_edx_navigational_events_event_field_with_invalid_content(id, field): +# @custom_given(NavigationalEventField) +def test_fields_edx_navigational_events_event_field_with_invalid_content(id): """Test that an invalid `NavigationalEventField` raises a `ValidationError`.""" + field = mock_instance(NavigationalEventField) + invalid_field = json.loads(field.json()) invalid_field["id"] = id diff --git a/tests/models/edx/navigational/test_statements.py b/tests/models/edx/navigational/test_statements.py index 7d512c672..3d7d235a9 100644 --- a/tests/models/edx/navigational/test_statements.py +++ b/tests/models/edx/navigational/test_statements.py @@ -17,7 +17,7 @@ from ralph.models.selector import ModelSelector from tests.fixtures.hypothesis_strategies import custom_builds, custom_given - +from tests.factories import mock_instance @pytest.mark.parametrize( "class_", diff --git a/tests/models/edx/open_response_assessment/test_events.py b/tests/models/edx/open_response_assessment/test_events.py index 614f2bc98..a0a50b6a1 100644 --- a/tests/models/edx/open_response_assessment/test_events.py +++ b/tests/models/edx/open_response_assessment/test_events.py @@ -14,7 +14,7 @@ ) from tests.fixtures.hypothesis_strategies import custom_given - +from tests.factories import mock_instance @custom_given(ORAGetPeerSubmissionEventField) def test_models_edx_ora_get_peer_submission_event_field_with_valid_values(field): diff --git a/tests/models/edx/test_base.py b/tests/models/edx/test_base.py index 8f3a65de8..fc3234cac 100644 --- a/tests/models/edx/test_base.py +++ b/tests/models/edx/test_base.py @@ -8,12 +8,15 @@ from ralph.models.edx.base import BaseEdxModel -from tests.fixtures.hypothesis_strategies import custom_given +# from tests.fixtures.hypothesis_strategies import custom_given +from tests.factories import mock_instance -@custom_given(BaseEdxModel) -def test_models_edx_base_edx_model_with_valid_statement(statement): +# @custom_given(BaseEdxModel) +def test_models_edx_base_edx_model_with_valid_statement(): """Test that a valid base `Edx` statement does not raise a `ValidationError`.""" + statement = mock_instance(BaseEdxModel) + assert len(statement.username) == 0 or (len(statement.username) in range(2, 31, 1)) assert ( re.match(r"^course-v1:.+\+.+\+.+$", statement.context.course_id) @@ -42,9 +45,10 @@ def test_models_edx_base_edx_model_with_valid_statement(statement): ), ], ) -@custom_given(BaseEdxModel) -def test_models_edx_base_edx_model_with_invalid_statement(course_id, error, statement): +# @custom_given(BaseEdxModel) +def test_models_edx_base_edx_model_with_invalid_statement(course_id, error): """Test that an invalid base `Edx` statement raises a `ValidationError`.""" + statement = mock_instance(BaseEdxModel) invalid_statement = json.loads(statement.json()) invalid_statement["context"]["course_id"] = course_id diff --git a/tests/models/edx/test_browser.py b/tests/models/edx/test_browser.py index 06b2a7bed..7257c731b 100644 --- a/tests/models/edx/test_browser.py +++ b/tests/models/edx/test_browser.py @@ -8,12 +8,14 @@ from ralph.models.edx.browser import BaseBrowserModel -from tests.fixtures.hypothesis_strategies import custom_given +# from tests.fixtures.hypothesis_strategies import custom_given +from tests.factories import mock_instance -@custom_given(BaseBrowserModel) -def test_models_edx_base_browser_model_with_valid_statement(statement): +# @custom_given(BaseBrowserModel) +def test_models_edx_base_browser_model_with_valid_statement(): """Test that a valid base browser statement does not raise a `ValidationError`.""" + statement = mock_instance(BaseBrowserModel) assert re.match(r"^[a-f0-9]{32}$", statement.session) or statement.session == "" @@ -28,11 +30,12 @@ def test_models_edx_base_browser_model_with_valid_statement(statement): ("abcdef0123456789_abcdef012345678", "string does not match regex"), ], ) -@custom_given(BaseBrowserModel) +# @custom_given(BaseBrowserModel) def test_models_edx_base_browser_model_with_invalid_statement( - session, error, statement + session, error ): """Test that an invalid base browser statement raises a `ValidationError`.""" + statement = mock_instance(BaseBrowserModel) invalid_statement = json.loads(statement.json()) invalid_statement["session"] = session diff --git a/tests/models/edx/test_enrollment.py b/tests/models/edx/test_enrollment.py index 53c1af26c..1f7e3f337 100644 --- a/tests/models/edx/test_enrollment.py +++ b/tests/models/edx/test_enrollment.py @@ -14,7 +14,8 @@ ) from ralph.models.selector import ModelSelector -from tests.fixtures.hypothesis_strategies import custom_builds, custom_given +# from tests.fixtures.hypothesis_strategies import custom_builds, custom_given +from tests.factories import mock_instance @pytest.mark.parametrize( @@ -27,66 +28,71 @@ UIEdxCourseEnrollmentUpgradeClicked, ], ) -@custom_given(st.data()) -def test_models_edx_edx_course_enrollment_selectors_with_valid_statements(class_, data): +def test_models_edx_edx_course_enrollment_selectors_with_valid_statements(class_): """Test given a valid course enrollment edX statement the `get_first_model` selector method should return the expected model. """ - statement = json.loads(data.draw(custom_builds(class_)).json()) + statement = json.loads(mock_instance(class_).json()) + # statement = json.loads(data.draw(custom_builds(class_)).json()) model = ModelSelector(module="ralph.models.edx").get_first_model(statement) assert model is class_ -@custom_given(EdxCourseEnrollmentActivated) +# @custom_given(EdxCourseEnrollmentActivated) def test_models_edx_edx_course_enrollment_activated_with_valid_statement( - statement, + #statement, ): """Test that a `edx.course.enrollment.activated` statement has the expected `event_type` and `name`. """ + statement = mock_instance(EdxCourseEnrollmentActivated) assert statement.event_type == "edx.course.enrollment.activated" assert statement.name == "edx.course.enrollment.activated" -@custom_given(EdxCourseEnrollmentDeactivated) +# @custom_given(EdxCourseEnrollmentDeactivated) def test_models_edx_edx_course_enrollment_deactivated_with_valid_statement( - statement, + #statement, ): """Test that a `edx.course.enrollment.deactivated` statement has the expected `event_type` and `name`. """ + statement = mock_instance(EdxCourseEnrollmentDeactivated) assert statement.event_type == "edx.course.enrollment.deactivated" assert statement.name == "edx.course.enrollment.deactivated" -@custom_given(EdxCourseEnrollmentModeChanged) +# @custom_given(EdxCourseEnrollmentModeChanged) def test_models_edx_edx_course_enrollment_mode_changed_with_valid_statement( - statement, + #statement, ): """Test that a `edx.course.enrollment.mode_changed` statement has the expected `event_type` and `name`. """ + statement = mock_instance(EdxCourseEnrollmentModeChanged) assert statement.event_type == "edx.course.enrollment.mode_changed" assert statement.name == "edx.course.enrollment.mode_changed" -@custom_given(UIEdxCourseEnrollmentUpgradeClicked) +# @custom_given(UIEdxCourseEnrollmentUpgradeClicked) def test_models_edx_ui_edx_course_enrollment_upgrade_clicked_with_valid_statement( - statement, + #statement, ): """Test that a `edx.course.enrollment.upgrade_clicked` statement has the expected `event_type` and `name`. """ + statement = mock_instance(UIEdxCourseEnrollmentUpgradeClicked) assert statement.event_type == "edx.course.enrollment.upgrade_clicked" assert statement.name == "edx.course.enrollment.upgrade_clicked" -@custom_given(EdxCourseEnrollmentUpgradeSucceeded) +# @custom_given(EdxCourseEnrollmentUpgradeSucceeded) def test_models_edx_edx_course_enrollment_upgrade_succeeded_with_valid_statement( - statement, + #statement, ): """Test that a `edx.course.enrollment.upgrade.succeeded` statement has the expected `event_type` and `name`. """ + statement = mock_instance(EdxCourseEnrollmentUpgradeSucceeded) assert statement.event_type == "edx.course.enrollment.upgrade.succeeded" assert statement.name == "edx.course.enrollment.upgrade.succeeded" diff --git a/tests/models/edx/test_server.py b/tests/models/edx/test_server.py index ef8dbded4..aaa2fd396 100644 --- a/tests/models/edx/test_server.py +++ b/tests/models/edx/test_server.py @@ -8,14 +8,15 @@ from ralph.models.edx.server import Server from ralph.models.selector import ModelSelector -from tests.fixtures.hypothesis_strategies import custom_given +# from tests.fixtures.hypothesis_strategies import custom_given +from tests.factories import mock_instance - -@custom_given(Server) -def test_model_selector_server_get_model_with_valid_event(event): +def test_model_selector_server_get_model_with_valid_event(): """Test given a server statement, the get_model method should return the corresponding model. """ + event = mock_instance(Server) + event = json.loads(event.json()) assert ModelSelector(module="ralph.models.edx").get_first_model(event) is Server diff --git a/tests/models/test_converter.py b/tests/models/test_converter.py index 2fcad9e82..0043be5e9 100644 --- a/tests/models/test_converter.py +++ b/tests/models/test_converter.py @@ -24,7 +24,8 @@ from ralph.models.edx.converters.xapi.base import BaseConversionSet from ralph.models.edx.navigational.statements import UIPageClose -from tests.fixtures.hypothesis_strategies import custom_given +# from tests.fixtures.hypothesis_strategies import custom_given +from tests.factories import mock_instance @pytest.mark.parametrize( @@ -328,16 +329,16 @@ def test_converter_convert_with_an_event_missing_a_conversion_set_raises_an_exce list(result) -@settings(suppress_health_check=(HealthCheck.function_scoped_fixture,)) @pytest.mark.parametrize("valid_uuid", ["ee241f8b-174f-5bdb-bae9-c09de5fe017f"]) @pytest.mark.parametrize("invalid_platform_url", ["", "not an URL"]) -@custom_given(UIPageClose) def test_converter_convert_with_invalid_arguments_raises_an_exception( - valid_uuid, invalid_platform_url, caplog, event + valid_uuid, invalid_platform_url, caplog ): """Test given invalid arguments causing the conversion to fail at the validation step, the convert method should raise a ValidationError. """ + event = mock_instance(UIPageClose) + event_str = event.json() result = Converter( platform_url=invalid_platform_url, uuid_namespace=valid_uuid @@ -350,11 +351,13 @@ def test_converter_convert_with_invalid_arguments_raises_an_exception( @pytest.mark.parametrize("ignore_errors", [True, False]) @pytest.mark.parametrize("fail_on_unknown", [True, False]) @pytest.mark.parametrize("valid_uuid", ["ee241f8b-174f-5bdb-bae9-c09de5fe017f"]) -@custom_given(UIPageClose) def test_converter_convert_with_valid_events( - ignore_errors, fail_on_unknown, valid_uuid, event + ignore_errors, fail_on_unknown, valid_uuid ): """Test given a valid event the convert method should yield it.""" + + event = mock_instance(UIPageClose) + event_str = event.json() result = Converter( platform_url="https://fun-mooc.fr", uuid_namespace=valid_uuid @@ -365,13 +368,13 @@ def test_converter_convert_with_valid_events( ) -@settings(suppress_health_check=(HealthCheck.function_scoped_fixture,)) -@custom_given(UIPageClose) @pytest.mark.parametrize("valid_uuid", ["ee241f8b-174f-5bdb-bae9-c09de5fe017f"]) -def test_converter_convert_counter(valid_uuid, caplog, event): +def test_converter_convert_counter(valid_uuid, caplog): """Test given multiple events the convert method should log the total and invalid events. """ + event = mock_instance(UIPageClose) + valid_event = event.json() invalid_event_1 = 1 invalid_event_2 = "" diff --git a/tests/models/test_validator.py b/tests/models/test_validator.py index 61a6c8fb0..89e35c640 100644 --- a/tests/models/test_validator.py +++ b/tests/models/test_validator.py @@ -14,8 +14,8 @@ from ralph.models.selector import ModelSelector from ralph.models.validator import Validator -from tests.fixtures.hypothesis_strategies import custom_given - +# from tests.fixtures.hypothesis_strategies import custom_given +from tests.factories import mock_instance def test_models_validator_validate_with_no_events(caplog): """Test given no events, the validate method does not write error messages.""" @@ -142,11 +142,13 @@ def test_models_validator_validate_with_invalid_page_close_event_raises_an_excep @pytest.mark.parametrize("ignore_errors", [True, False]) @pytest.mark.parametrize("fail_on_unknown", [True, False]) -@custom_given(UIPageClose) +# @custom_given(UIPageClose) def test_models_validator_validate_with_valid_events( - ignore_errors, fail_on_unknown, event + ignore_errors, fail_on_unknown ): """Test given a valid event the validate method should yield it.""" + event = mock_instance(UIPageClose) + event_str = event.json() event_dict = json.loads(event_str) validator = Validator(ModelSelector(module="ralph.models.edx")) @@ -154,12 +156,14 @@ def test_models_validator_validate_with_valid_events( assert json.loads(next(result)) == event_dict -@settings(suppress_health_check=(HealthCheck.function_scoped_fixture,)) -@custom_given(UIPageClose) -def test_models_validator_validate_counter(caplog, event): +# @settings(suppress_health_check=(HealthCheck.function_scoped_fixture,)) +# @custom_given(UIPageClose) +def test_models_validator_validate_counter(caplog): """Test given multiple events the validate method should log the total and invalid events. """ + event = mock_instance(UIPageClose) + valid_event = event.json() invalid_event_1 = 1 invalid_event_2 = "" @@ -176,11 +180,13 @@ def test_models_validator_validate_counter(caplog, event): ) in caplog.record_tuples -@custom_given(Server) -def test_models_validator_validate_typing_cleanup(event): +# @custom_given(Server) +def test_models_validator_validate_typing_cleanup(): """Test given a valid event with wrong field types, the validate method should fix them. """ + event = mock_instance(Server) + valid_event_str = event.json() valid_event = json.loads(valid_event_str) valid_event["host"] = "1" diff --git a/tests/models/xapi/base/test_statements.py b/tests/models/xapi/base/test_statements.py index 1f0c8cfa6..0edbbb6cf 100644 --- a/tests/models/xapi/base/test_statements.py +++ b/tests/models/xapi/base/test_statements.py @@ -28,7 +28,7 @@ # from tests.fixtures.hypothesis_strategies import custom_builds, custom_given -from tests.factories import mock_instance, ModelFactory +from tests.factories import mock_xapi_instance, ModelFactory @pytest.mark.parametrize( @@ -44,7 +44,7 @@ def test_models_xapi_base_statement_with_invalid_null_values(path, value): value is set to "null", an empty object, or has no value, except in an "extensions" property. """ - statement = mock_instance(BaseXapiStatement) + statement = mock_xapi_instance(BaseXapiStatement) statement = statement.dict(exclude_none=True) set_dict_value_from_path(statement, path.split("__"), value) @@ -71,7 +71,7 @@ def test_models_xapi_base_statement_with_valid_null_values(path, value): property. """ - statement = mock_instance(BaseXapiStatement, object=mock_instance(BaseXapiActivity)) + statement = mock_xapi_instance(BaseXapiStatement, object=mock_xapi_instance(BaseXapiActivity)) statement = statement.dict(exclude_none=True) @@ -90,11 +90,11 @@ def test_models_xapi_base_statement_with_valid_empty_array(path): that there is no correct answer. """ - statement = mock_instance( + statement = mock_xapi_instance( BaseXapiStatement, - object=mock_instance( + object=mock_xapi_instance( BaseXapiActivity, - definition=mock_instance(BaseXapiActivityInteractionDefinition), + definition=mock_xapi_instance(BaseXapiActivityInteractionDefinition), ), ) @@ -124,7 +124,7 @@ def test_models_xapi_base_statement_must_use_actor_verb_and_object(field): "object" property. """ - statement = mock_instance(BaseXapiStatement) + statement = mock_xapi_instance(BaseXapiStatement) statement = statement.dict(exclude_none=True) del statement["context"] # Necessary as context leads to another validation error @@ -152,8 +152,8 @@ def test_models_xapi_base_statement_with_invalid_data_types(path, value): An LRS rejects with error code 400 Bad Request a Statement which uses the wrong data type. """ - statement = mock_instance( - BaseXapiStatement, actor=mock_instance(BaseXapiAgentWithAccount) + statement = mock_xapi_instance( + BaseXapiStatement, actor=mock_xapi_instance(BaseXapiAgentWithAccount) ) statement = statement.dict(exclude_none=True) @@ -183,8 +183,8 @@ def test_models_xapi_base_statement_with_invalid_data_format(path, value): particular format (such as mailto IRI, UUID, or IRI) is required. (Empty strings are covered by XAPI-00001) """ - statement = mock_instance( - BaseXapiStatement, actor=mock_instance(BaseXapiAgentWithAccount) + statement = mock_xapi_instance( + BaseXapiStatement, actor=mock_xapi_instance(BaseXapiAgentWithAccount) ) statement = statement.dict(exclude_none=True) @@ -202,8 +202,8 @@ def test_models_xapi_base_statement_with_invalid_letter_cases(path, value): An LRS rejects with error code 400 Bad Request a Statement where the case of a key does not match the case specified in this specification. """ - statement = mock_instance( - BaseXapiStatement, actor=mock_instance(BaseXapiAgentWithAccount) + statement = mock_xapi_instance( + BaseXapiStatement, actor=mock_xapi_instance(BaseXapiAgentWithAccount) ) statement = statement.dict(exclude_none=True) @@ -222,7 +222,7 @@ def test_models_xapi_base_statement_should_not_accept_additional_properties(): An LRS rejects with error code 400 Bad Request a Statement where a key or value is not allowed by this specification. """ - statement = mock_instance(BaseXapiStatement) + statement = mock_xapi_instance(BaseXapiStatement) invalid_statement = statement.dict(exclude_none=True) invalid_statement["NEW_INVALID_FIELD"] = "some value" @@ -238,7 +238,7 @@ def test_models_xapi_base_statement_with_iri_without_scheme(path, value): An LRS rejects with error code 400 Bad Request a Statement containing IRL or IRI values without a scheme. """ - statement = mock_instance(BaseXapiStatement) + statement = mock_xapi_instance(BaseXapiStatement) statement = statement.dict(exclude_none=True) set_dict_value_from_path(statement, path.split("__"), value) @@ -261,7 +261,7 @@ def test_models_xapi_base_statement_with_invalid_extensions(path): An Extension "key" is an IRI. The LRS rejects with 400 a statement which has an extension key which is not a valid IRI, if an extension object is present. """ - statement = mock_instance(BaseXapiStatement, object=mock_instance(BaseXapiActivity)) + statement = mock_xapi_instance(BaseXapiStatement, object=mock_xapi_instance(BaseXapiActivity)) statement = statement.dict(exclude_none=True) set_dict_value_from_path(statement, path.split("__"), "") @@ -275,8 +275,8 @@ def test_models_xapi_base_statement_with_two_agent_types(path, value): An Agent MUST NOT include more than one Inverse Functional Identifier. """ - statement = mock_instance( - BaseXapiStatement, actor=mock_instance(BaseXapiAgentWithAccount) + statement = mock_xapi_instance( + BaseXapiStatement, actor=mock_xapi_instance(BaseXapiAgentWithAccount) ) statement = statement.dict(exclude_none=True) @@ -290,8 +290,8 @@ def test_models_xapi_base_statement_missing_member_property(): An Anonymous Group MUST include a "member" property listing constituent Agents. """ - statement = mock_instance( - BaseXapiStatement, actor=mock_instance(BaseXapiAnonymousGroup) + statement = mock_xapi_instance( + BaseXapiStatement, actor=mock_xapi_instance(BaseXapiAnonymousGroup) ) statement = statement.dict(exclude_none=True) @@ -326,11 +326,11 @@ def test_models_xapi_base_statement_with_invalid_group_objects(klass): BaseXapiIdentifiedGroupWithAccount, ] ) - statement = mock_instance(BaseXapiStatement, actor=mock_instance(actor_class)) + statement = mock_xapi_instance(BaseXapiStatement, actor=mock_xapi_instance(actor_class)) kwargs = {"exclude_none": True} statement = statement.dict(**kwargs) - statement["actor"]["member"] = [mock_instance(klass).dict(**kwargs)] # TODO: check that nothing was lost + statement["actor"]["member"] = [mock_xapi_instance(klass).dict(**kwargs)] # TODO: check that nothing was lost err = "actor -> member -> 0 -> objectType\n unexpected value; permitted: 'Agent'" with pytest.raises(ValidationError, match=err): BaseXapiStatement(**statement) @@ -342,8 +342,8 @@ def test_models_xapi_base_statement_with_two_group_identifiers(path, value): An Identified Group MUST include exactly one Inverse Functional Identifier. """ - statement = mock_instance( - BaseXapiStatement, actor=mock_instance(BaseXapiIdentifiedGroupWithAccount) + statement = mock_xapi_instance( + BaseXapiStatement, actor=mock_xapi_instance(BaseXapiIdentifiedGroupWithAccount) ) statement = statement.dict(exclude_none=True) @@ -367,8 +367,8 @@ def test_models_xapi_base_statement_with_sub_statement_ref(path, value): A SubStatement MUST NOT have the "id", "stored", "version" or "authority" properties. """ - statement = mock_instance( - BaseXapiStatement, object=mock_instance(BaseXapiSubStatement) + statement = mock_xapi_instance( + BaseXapiStatement, object=mock_xapi_instance(BaseXapiSubStatement) ) statement = statement.dict(exclude_none=True) @@ -391,11 +391,11 @@ def test_models_xapi_base_statement_with_invalid_interaction_object(value): An interaction component's id value SHOULD NOT have whitespace. Within an array of interaction components, all id values MUST be distinct. """ - statement = mock_instance( + statement = mock_xapi_instance( BaseXapiStatement, - object=mock_instance( + object=mock_xapi_instance( BaseXapiActivity, - definition=mock_instance(BaseXapiActivityInteractionDefinition), + definition=mock_xapi_instance(BaseXapiActivityInteractionDefinition), ), ) @@ -427,7 +427,7 @@ def test_models_xapi_base_statement_with_invalid_context_value(path, value): BaseXapiStatementRef, ] ) - statement = mock_instance(BaseXapiStatement, object=mock_instance(object_class)) + statement = mock_xapi_instance(BaseXapiStatement, object=mock_xapi_instance(object_class)) statement = statement.dict(exclude_none=True) @@ -444,7 +444,7 @@ def test_models_xapi_base_statement_with_invalid_context_activities(path): Every key in the contextActivities Object MUST be one of parent, grouping, category, or other. """ - statement = mock_instance(BaseXapiStatement) + statement = mock_xapi_instance(BaseXapiStatement) statement = statement.dict(exclude_none=True) set_dict_value_from_path(statement, path.split("."), {"id": "http://w3id.org/xapi"}) @@ -466,7 +466,7 @@ def test_models_xapi_base_statement_with_valid_context_activities(value): Every value in the contextActivities Object MUST be either a single Activity Object or an array of Activity Objects. """ - statement = mock_instance(BaseXapiStatement) + statement = mock_xapi_instance(BaseXapiStatement) statement = statement.dict(exclude_none=True) path = ["context", "contextActivities"] @@ -485,7 +485,7 @@ def test_models_xapi_base_statement_with_invalid_version(value): An LRS MUST reject all Statements with a version specified that does not start with 1.0. """ - statement = mock_instance(BaseXapiStatement) + statement = mock_xapi_instance(BaseXapiStatement) statement = statement.dict(exclude_none=True) set_dict_value_from_path(statement, ["version"], value) @@ -499,7 +499,7 @@ def test_models_xapi_base_statement_with_valid_version(): Statements returned by an LRS MUST retain the version they are accepted with. If they lack a version, the version MUST be set to 1.0.0. """ - statement = mock_instance(BaseXapiStatement) + statement = mock_xapi_instance(BaseXapiStatement) statement = statement.dict(exclude_none=True) set_dict_value_from_path(statement, ["version"], "1.0.3") @@ -519,7 +519,7 @@ def test_models_xapi_base_statement_should_consider_valid_all_defined_xapi_model # All specific xAPI models should inherit BaseXapiStatement assert issubclass(model, BaseXapiStatement) - statement = mock_instance(model) + statement = mock_xapi_instance(model) statement = statement.json(exclude_none=True, by_alias=True) # TODO: check that we are not losing info by mocking random model try: BaseXapiStatement(**json.loads(statement)) diff --git a/tests/test_cli.py b/tests/test_cli.py index 83dd6d3c4..6ab832f86 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -33,7 +33,7 @@ WS_TEST_HOST, WS_TEST_PORT, ) -# from tests.fixtures.hypothesis_strategies import custom_given + from tests.factories import mock_instance test_logger = logging.getLogger("ralph") @@ -482,12 +482,11 @@ def test_cli_validate_command_with_edx_format(): assert event_str in result.output -@hypothesis_settings(deadline=None) @pytest.mark.parametrize("valid_uuid", ["ee241f8b-174f-5bdb-bae9-c09de5fe017f"]) def test_cli_convert_command_from_edx_to_xapi_format(valid_uuid): """Test ralph convert command from edx to xapi format.""" event = mock_instance(UIPageClose) - + event_str = event.json() runner = CliRunner() command = f"-v ERROR convert -f edx -t xapi -u {valid_uuid} -p https://fun-mooc.fr" From 83c9f1717bdde0a6e8f226f3cf13cd48f700b56a Mon Sep 17 00:00:00 2001 From: lleeoo Date: Wed, 13 Dec 2023 12:10:08 +0100 Subject: [PATCH 15/19] wip --- .../edx/problem_interaction/fields/events.py | 50 ++++---- src/ralph/models/edx/video/fields/events.py | 7 +- src/ralph/models/xapi/video/results.py | 12 +- tests/factories.py | 7 +- .../models/edx/converters/xapi/test_server.py | 5 +- .../edx/navigational/test_statements.py | 40 +++--- .../open_response_assessment/test_events.py | 21 ++-- .../test_statements.py | 50 ++++---- .../edx/peer_instruction/test_events.py | 13 +- .../edx/peer_instruction/test_statements.py | 20 ++- .../edx/problem_interaction/test_events.py | 117 +++++++++--------- .../problem_interaction/test_statements.py | 72 ++++++----- .../edx/textbook_interaction/test_events.py | 20 +-- .../textbook_interaction/test_statements.py | 60 ++++----- tests/models/edx/video/test_events.py | 16 +-- tests/models/edx/video/test_statements.py | 42 +++---- tests/models/test_validator.py | 1 - tests/models/xapi/base/test_agents.py | 6 +- tests/models/xapi/base/test_groups.py | 7 +- tests/models/xapi/base/test_objects.py | 8 +- tests/models/xapi/base/test_results.py | 8 +- .../models/xapi/base/test_unnested_objects.py | 19 ++- .../xapi/concepts/test_activity_types.py | 10 +- tests/models/xapi/concepts/test_verbs.py | 10 +- tests/models/xapi/test_lms.py | 68 +++++----- tests/models/xapi/test_navigation.py | 18 ++- tests/models/xapi/test_video.py | 58 ++++----- tests/models/xapi/test_virtual_classroom.py | 80 ++++++------ 28 files changed, 402 insertions(+), 443 deletions(-) diff --git a/src/ralph/models/edx/problem_interaction/fields/events.py b/src/ralph/models/edx/problem_interaction/fields/events.py index 8ba3b0e16..a5d32530d 100644 --- a/src/ralph/models/edx/problem_interaction/fields/events.py +++ b/src/ralph/models/edx/problem_interaction/fields/events.py @@ -2,9 +2,9 @@ import sys from datetime import datetime -from typing import Dict, List, Optional, Union +from typing import Annotated, Dict, List, Optional, Union -from pydantic import constr +from pydantic import constr, Field from ...base import AbstractBaseEventField, BaseModelWithConfig @@ -62,7 +62,7 @@ class State(BaseModelWithConfig): """ correct_map: Dict[ - constr(regex=r"^[a-f0-9]{32}_[0-9]_[0-9]$"), + Annotated[str, Field(regex=r"^[a-f0-9]{32}_[0-9]_[0-9]$")], CorrectMap, ] done: Optional[bool] @@ -170,23 +170,23 @@ class ProblemCheckEventField(AbstractBaseEventField): """ answers: Dict[ - constr(regex=r"^[a-f0-9]{32}_[0-9]_[0-9]$"), + Annotated[str, Field(regex=r"^[a-f0-9]{32}_[0-9]_[0-9]$")], Union[List[str], str], ] attempts: int correct_map: Dict[ - constr(regex=r"^[a-f0-9]{32}_[0-9]_[0-9]$"), + Annotated[str, Field(regex=r"^[a-f0-9]{32}_[0-9]_[0-9]$")], CorrectMap, ] grade: int max_grade: int - problem_id: constr( + problem_id: Annotated[str, Field( regex=r"^block-v1:[^\/+]+(\/|\+)[^\/+]+(\/|\+)[^\/?]+" r"type@problem\+block@[a-f0-9]{32}$" - ) + )] state: State submission: Dict[ - constr(regex=r"^[a-f0-9]{32}_[0-9]_[0-9]$"), + Annotated[str, Field(regex=r"^[a-f0-9]{32}_[0-9]_[0-9]$")], SubmissionAnswerField, ] success: Union[Literal["correct"], Literal["incorrect"]] @@ -204,14 +204,14 @@ class ProblemCheckFailEventField(AbstractBaseEventField): """ answers: Dict[ - constr(regex=r"^[a-f0-9]{32}_[0-9]_[0-9]$"), + Annotated[str, Field(regex=r"^[a-f0-9]{32}_[0-9]_[0-9]$")], Union[List[str], str], ] failure: Union[Literal["closed"], Literal["unreset"]] - problem_id: constr( + problem_id: Annotated[str, Field( regex=r"^block-v1:[^\/+]+(\/|\+)[^\/+]+(\/|\+)[^\/?]+" r"type@problem\+block@[a-f0-9]{32}$" - ) + )] state: State @@ -235,10 +235,10 @@ class ProblemRescoreEventField(AbstractBaseEventField): new_total: int orig_score: int orig_total: int - problem_id: constr( + problem_id: Annotated[str, Field( regex=r"^block-v1:[^\/+]+(\/|\+)[^\/+]+(\/|\+)[^\/?]+" r"type@problem\+block@[a-f0-9]{32}$" - ) + )] state: State success: Union[Literal["correct"], Literal["incorrect"]] @@ -253,10 +253,10 @@ class ProblemRescoreFailEventField(AbstractBaseEventField): """ failure: Union[Literal["closed"], Literal["unreset"]] - problem_id: constr( + problem_id: Annotated[str, Field( regex=r"^block-v1:[^\/+]+(\/|\+)[^\/+]+(\/|\+)[^\/?]+" r"type@problem\+block@[a-f0-9]{32}$" - ) + )] state: State @@ -293,10 +293,10 @@ class ResetProblemEventField(AbstractBaseEventField): new_state: State old_state: State - problem_id: constr( + problem_id: Annotated[str, Field( regex=r"^block-v1:[^\/+]+(\/|\+)[^\/+]+(\/|\+)[^\/?]+" r"type@problem\+block@[a-f0-9]{32}$" - ) + )] class ResetProblemFailEventField(AbstractBaseEventField): @@ -310,10 +310,10 @@ class ResetProblemFailEventField(AbstractBaseEventField): failure: Union[Literal["closed"], Literal["not_done"]] old_state: State - problem_id: constr( + problem_id: Annotated[str, Field( regex=r"^block-v1:[^\/+]+(\/|\+)[^\/+]+(\/|\+)[^\/?]+" r"type@problem\+block@[a-f0-9]{32}$" - ) + )] class SaveProblemFailEventField(AbstractBaseEventField): @@ -329,10 +329,10 @@ class SaveProblemFailEventField(AbstractBaseEventField): answers: Dict[str, Union[int, str, list, dict]] failure: Union[Literal["closed"], Literal["done"]] - problem_id: constr( + problem_id: Annotated[str, Field( regex=r"^block-v1:[^\/+]+(\/|\+)[^\/+]+(\/|\+)[^\/?]+" r"type@problem\+block@[a-f0-9]{32}$" - ) + )] state: State @@ -347,10 +347,10 @@ class SaveProblemSuccessEventField(AbstractBaseEventField): """ answers: Dict[str, Union[int, str, list, dict]] - problem_id: constr( + problem_id: Annotated[str, Field( regex=r"^block-v1:[^\/+]+(\/|\+)[^\/+]+(\/|\+)[^\/?]+" r"type@problem\+block@[a-f0-9]{32}$" - ) + )] state: State @@ -361,7 +361,7 @@ class ShowAnswerEventField(AbstractBaseEventField): problem_id (str): Consists of the ID of the problem being shown. """ - problem_id: constr( + problem_id: Annotated[str, Field( regex=r"^block-v1:[^\/+]+(\/|\+)[^\/+]+(\/|\+)[^\/?]+" r"type@problem\+block@[a-f0-9]{32}$" - ) + )] \ No newline at end of file diff --git a/src/ralph/models/edx/video/fields/events.py b/src/ralph/models/edx/video/fields/events.py index 786a57957..0829a8f2f 100644 --- a/src/ralph/models/edx/video/fields/events.py +++ b/src/ralph/models/edx/video/fields/events.py @@ -2,6 +2,8 @@ import sys +from pydantic import NonNegativeFloat + from ...base import AbstractBaseEventField if sys.version_info >= (3, 8): @@ -48,7 +50,6 @@ class PauseVideoEventField(VideoBaseEventField): currentTime: float - class SeekVideoEventField(VideoBaseEventField): """Pydantic model for `seek_video`.`event` field. @@ -61,8 +62,8 @@ class SeekVideoEventField(VideoBaseEventField): within the video, either `onCaptionSeek` or `onSlideSeek` value. """ - new_time: float - old_time: float + new_time: NonNegativeFloat # TODO: Ask Quitterie if this is valid + old_time: NonNegativeFloat type: str diff --git a/src/ralph/models/xapi/video/results.py b/src/ralph/models/xapi/video/results.py index 359ef0b41..7c515ad53 100644 --- a/src/ralph/models/xapi/video/results.py +++ b/src/ralph/models/xapi/video/results.py @@ -33,7 +33,7 @@ class VideoResultExtensions(BaseExtensionModelWithConfig): time (float): Consists of the video time code when the event was emitted. """ - time: NonNegativeFloat = Field(alias=RESULT_EXTENSION_TIME, min=0) + time: NonNegativeFloat = Field(alias=RESULT_EXTENSION_TIME) playedSegments: Optional[str] = Field(alias=CONTEXT_EXTENSION_PLAYED_SEGMENTS) @@ -44,7 +44,7 @@ class VideoPausedResultExtensions(VideoResultExtensions): progress (float): Consists of the ratio of media consumed by the actor. """ - progress: Optional[NonNegativeFloat] = Field(alias=RESULT_EXTENSION_PROGRESS, min=0) + progress: Optional[NonNegativeFloat] = Field(alias=RESULT_EXTENSION_PROGRESS) class VideoSeekedResultExtensions(BaseExtensionModelWithConfig): @@ -57,8 +57,8 @@ class VideoSeekedResultExtensions(BaseExtensionModelWithConfig): object during a seek operation. """ - timeFrom: NonNegativeFloat = Field(alias=RESULT_EXTENSION_TIME_FROM, min=0) - timeTo: NonNegativeFloat = Field(alias=RESULT_EXTENSION_TIME_TO, min=0) + timeFrom: NonNegativeFloat = Field(alias=RESULT_EXTENSION_TIME_FROM) + timeTo: NonNegativeFloat = Field(alias=RESULT_EXTENSION_TIME_TO) class VideoCompletedResultExtensions(VideoResultExtensions): @@ -68,7 +68,7 @@ class VideoCompletedResultExtensions(VideoResultExtensions): progress (float): Consists of the percentage of media consumed by the actor. """ - progress: NonNegativeFloat = Field(alias=RESULT_EXTENSION_PROGRESS, min=0) + progress: NonNegativeFloat = Field(alias=RESULT_EXTENSION_PROGRESS) class VideoTerminatedResultExtensions(VideoResultExtensions): @@ -78,7 +78,7 @@ class VideoTerminatedResultExtensions(VideoResultExtensions): progress (float): Consists of the percentage of media consumed by the actor. """ - progress: NonNegativeFloat = Field(alias=RESULT_EXTENSION_PROGRESS, min=0) + progress: NonNegativeFloat = Field(alias=RESULT_EXTENSION_PROGRESS) class VideoEnableClosedCaptioningResultExtensions(VideoResultExtensions): diff --git a/tests/factories.py b/tests/factories.py index 44839d495..8d0da5ff2 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -3,6 +3,7 @@ from typing import Any, Callable from decimal import Decimal +from pydantic import NonNegativeFloat from polyfactory.factories.pydantic_factory import ( ModelFactory as PolyfactoryModelFactory, T, @@ -67,7 +68,7 @@ def prune(d: Any, exceptions: list = []): elif isinstance(d, list): d_list = [prune(v) for v in d] return [v for v in d_list if v] - if d: + if d not in [[], {}, ""]: return d return False @@ -179,13 +180,13 @@ class UISeqPrev(ModelFactory[UISeqPrev]): __model__ = UISeqPrev __set_as_default_factory_for_type__ = True - event = lambda: mock_xapi_instance(NavigationalEventField, old=1, new=0) + event = lambda: mock_instance(NavigationalEventField, old=1, new=0) class UISeqNext(ModelFactory[UISeqNext]): __model__ = UISeqNext __set_as_default_factory_for_type__ = True - event = lambda: mock_xapi_instance(NavigationalEventField, old=0, new=1) + event = lambda: mock_instance(NavigationalEventField, old=0, new=1) def mock_xapi_instance(klass, *args, **kwargs): diff --git a/tests/models/edx/converters/xapi/test_server.py b/tests/models/edx/converters/xapi/test_server.py index be113d938..294cd76da 100644 --- a/tests/models/edx/converters/xapi/test_server.py +++ b/tests/models/edx/converters/xapi/test_server.py @@ -10,7 +10,7 @@ from ralph.models.edx.converters.xapi.server import ServerEventToPageViewed from ralph.models.edx.server import Server -from tests.fixtures.hypothesis_ strategies import custom_given +# from tests.fixtures.hypothes is_ strategies import custom_given from tests.factories import mock_instance, mock_url @pytest.mark.parametrize("uuid_namespace", ["ee241f8b-174f-5bdb-bae9-c09de5fe017f"]) @@ -73,9 +73,10 @@ def test_models_edx_converters_xapi_server_server_event_to_page_viewed( @pytest.mark.parametrize("uuid_namespace", ["ee241f8b-174f-5bdb-bae9-c09de5fe017f"]) def test_models_edx_converters_xapi_server_server_event_to_page_viewed_with_anonymous_user( # noqa: E501 - uuid_namespace, event + uuid_namespace ): """Test that anonymous usernames are replaced with `anonymous`.""" + event = mock_instance(Server) platform_url = mock_url() event.context.user_id = "" diff --git a/tests/models/edx/navigational/test_statements.py b/tests/models/edx/navigational/test_statements.py index 3d7d235a9..07679aac8 100644 --- a/tests/models/edx/navigational/test_statements.py +++ b/tests/models/edx/navigational/test_statements.py @@ -16,7 +16,7 @@ ) from ralph.models.selector import ModelSelector -from tests.fixtures.hypothesis_strategies import custom_builds, custom_given +# from tests.fixtures.hypothesis_strategies import custom_builds, custom_given from tests.factories import mock_instance @pytest.mark.parametrize( @@ -28,21 +28,21 @@ UISeqPrev, ], ) -@custom_given(st.data()) -def test_models_edx_navigational_selectors_with_valid_statements(class_, data): +def test_models_edx_navigational_selectors_with_valid_statements(class_): """Test given a valid navigational edX statement the `get_first_model` selector method should return the expected model. """ - statement = json.loads(data.draw(custom_builds(class_)).json()) + statement = json.loads(mock_instance(class_).json()) model = ModelSelector(module="ralph.models.edx").get_first_model(statement) assert model is class_ -@custom_given(NavigationalEventField) -def test_fields_edx_navigational_events_event_field_with_valid_content(field): +def test_fields_edx_navigational_events_event_field_with_valid_content(): """Test that a valid `NavigationalEventField` does not raise a `ValidationError`. """ + field = mock_instance(NavigationalEventField) + assert re.match( ( r"^block-v1:[^\/+]+(\/|\+)[^\/+]+(\/|\+)[^\/?]+" @@ -79,9 +79,9 @@ def test_fields_edx_navigational_events_event_field_with_valid_content(field): ), ], ) -@custom_given(NavigationalEventField) -def test_fields_edx_navigational_events_event_field_with_invalid_content(id, field): +def test_fields_edx_navigational_events_event_field_with_invalid_content(id): """Test that an invalid `NavigationalEventField` raises a `ValidationError`.""" + field = mock_instance(NavigationalEventField) invalid_field = json.loads(field.json()) invalid_field["id"] = id @@ -89,34 +89,34 @@ def test_fields_edx_navigational_events_event_field_with_invalid_content(id, fie NavigationalEventField(**invalid_field) -@custom_given(UIPageClose) -def test_models_edx_ui_page_close_with_valid_statement(statement): +def test_models_edx_ui_page_close_with_valid_statement(): """Test that a `page_close` statement has the expected `event`, `event_type` and `name`. """ + statement = mock_instance(UIPageClose) assert statement.event == "{}" assert statement.event_type == "page_close" assert statement.name == "page_close" -@custom_given(UISeqGoto) -def test_models_edx_ui_seq_goto_with_valid_statement(statement): +def test_models_edx_ui_seq_goto_with_valid_statement(): """Test that a `seq_goto` statement has the expected `event_type` and `name`.""" + statement = mock_instance(UISeqGoto) assert statement.event_type == "seq_goto" assert statement.name == "seq_goto" -@custom_given(UISeqNext) -def test_models_edx_ui_seq_next_with_valid_statement(statement): +def test_models_edx_ui_seq_next_with_valid_statement(): """Test that a `seq_next` statement has the expected `event_type` and `name`.""" + statement = mock_instance(UISeqNext) assert statement.event_type == "seq_next" assert statement.name == "seq_next" @pytest.mark.parametrize("old,new", [("0", "10"), ("10", "0")]) -@custom_given(UISeqNext) -def test_models_edx_ui_seq_next_with_invalid_statement(old, new, event): +def test_models_edx_ui_seq_next_with_invalid_statement(old, new): """Test that an invalid `seq_next` event raises a ValidationError.""" + event = mock_instance(UISeqNext) invalid_event = json.loads(event.json()) invalid_event["event"]["old"] = old invalid_event["event"]["new"] = new @@ -128,17 +128,17 @@ def test_models_edx_ui_seq_next_with_invalid_statement(old, new, event): UISeqNext(**invalid_event) -@custom_given(UISeqPrev) -def test_models_edx_ui_seq_prev_with_valid_statement(statement): +def test_models_edx_ui_seq_prev_with_valid_statement(): """Test that a `seq_prev` statement has the expected `event_type` and `name`.""" + statement = mock_instance(UISeqPrev) assert statement.event_type == "seq_prev" assert statement.name == "seq_prev" @pytest.mark.parametrize("old,new", [("0", "10"), ("10", "0")]) -@custom_given(UISeqPrev) -def test_models_edx_ui_seq_prev_with_invalid_statement(old, new, event): +def test_models_edx_ui_seq_prev_with_invalid_statement(old, new): """Test that an invalid `seq_prev` event raises a ValidationError.""" + event = mock_instance(UISeqPrev) invalid_event = json.loads(event.json()) invalid_event["event"]["old"] = old invalid_event["event"]["new"] = new diff --git a/tests/models/edx/open_response_assessment/test_events.py b/tests/models/edx/open_response_assessment/test_events.py index a0a50b6a1..46b95c0ca 100644 --- a/tests/models/edx/open_response_assessment/test_events.py +++ b/tests/models/edx/open_response_assessment/test_events.py @@ -13,38 +13,37 @@ ORAGetSubmissionForStaffGradingEventField, ) -from tests.fixtures.hypothesis_strategies import custom_given +# from tests.fixtures.hypothesis_strategies import custom_given from tests.factories import mock_instance -@custom_given(ORAGetPeerSubmissionEventField) -def test_models_edx_ora_get_peer_submission_event_field_with_valid_values(field): +def test_models_edx_ora_get_peer_submission_event_field_with_valid_values(): """Test that a valid `ORAGetPeerSubmissionEventField` does not raise a `ValidationError`. """ + field = mock_instance(ORAGetPeerSubmissionEventField) assert re.match( r"^block-v1:.+\+.+\+.+type@openassessment+block@[a-f0-9]{32}$", field.item_id ) -@custom_given(ORAGetSubmissionForStaffGradingEventField) def test_models_edx_ora_get_submission_for_staff_grading_event_field_with_valid_values( - field, ): """Test that a valid `ORAGetSubmissionForStaffGradingEventField` does not raise a `ValidationError`. """ + field = mock_instance(ORAGetSubmissionForStaffGradingEventField) assert re.match( r"^block-v1:.+\+.+\+.+type@openassessment+block@[a-f0-9]{32}$", field.item_id ) -@custom_given(ORAAssessEventField) -def test_models_edx_ora_assess_event_field_with_valid_values(field): +def test_models_edx_ora_assess_event_field_with_valid_values(): """Test that a valid `ORAAssessEventField` does not raise a `ValidationError`. """ + field = mock_instance(ORAAssessEventField) assert field.score_type in {"PE", "SE", "ST"} @@ -53,9 +52,9 @@ def test_models_edx_ora_assess_event_field_with_valid_values(field): "score_type", ["pe", "se", "st", "SA", "PA", "22", "&T"], ) -@custom_given(ORAAssessEventField) -def test_models_edx_ora_assess_event_field_with_invalid_values(score_type, field): +def test_models_edx_ora_assess_event_field_with_invalid_values(score_type): """Test that invalid `ORAAssessEventField` raises a `ValidationError`.""" + field = mock_instance(ORAAssessEventField) invalid_field = json.loads(field.json()) invalid_field["score_type"] = score_type @@ -72,13 +71,13 @@ def test_models_edx_ora_assess_event_field_with_invalid_values(score_type, field "D0d4a647742943e3951b45d9db8a0ea1ff71ae36", ], ) -@custom_given(ORAAssessEventRubricField) def test_models_edx_ora_assess_event_rubric_field_with_invalid_problem_id_value( - content_hash, field + content_hash ): """Test that an invalid `problem_id` value in `ProblemCheckEventField` raises a `ValidationError`. """ + field = mock_instance(ORAAssessEventRubricField) invalid_field = json.loads(field.json()) invalid_field["content_hash"] = content_hash diff --git a/tests/models/edx/open_response_assessment/test_statements.py b/tests/models/edx/open_response_assessment/test_statements.py index dc03c7b74..d140d4921 100644 --- a/tests/models/edx/open_response_assessment/test_statements.py +++ b/tests/models/edx/open_response_assessment/test_statements.py @@ -19,8 +19,8 @@ ) from ralph.models.selector import ModelSelector -from tests.fixtures.hypothesis_strategies import custom_builds, custom_given - +# from tests.fixtures.hypothesis_strategies import custom_builds, custom_given +from tests.factories import mock_instance @pytest.mark.parametrize( "class_", @@ -37,34 +37,32 @@ ORAStudentTrainingAssessExample, ], ) -@custom_given(st.data()) -def test_models_edx_ora_selectors_with_valid_statements(class_, data): +def test_models_edx_ora_selectors_with_valid_statements(class_): """Test given a valid open response assessment edX statement the `get_first_model` selector method should return the expected model. """ - - statement = json.loads(data.draw(custom_builds(class_)).json()) + statement = json.loads(mock_instance(class_).json()) model = ModelSelector(module="ralph.models.edx").get_first_model(statement) assert model is class_ -@custom_given(ORAGetPeerSubmission) -def test_models_edx_ora_get_peer_submission_with_valid_statement(statement): +def test_models_edx_ora_get_peer_submission_with_valid_statement(): """Test that a `openassessmentblock.get_peer_submission` statement has the expected `event_type` and `page` fields. """ + statement = mock_instance(ORAGetPeerSubmission) assert statement.event_type == "openassessmentblock.get_peer_submission" assert statement.page == "x_module" -@custom_given(ORAGetSubmissionForStaffGrading) def test_models_edx_ora_get_submission_for_staff_grading_with_valid_statement( - statement, + ): """Test that a `openassessmentblock.get_submission_for_staff_grading` statement has the expected `event_type` and `page` fields. """ + statement = mock_instance(ORAGetSubmissionForStaffGrading) assert ( statement.event_type == "openassessmentblock.get_submission_for_staff_grading" @@ -72,81 +70,81 @@ def test_models_edx_ora_get_submission_for_staff_grading_with_valid_statement( assert statement.page == "x_module" -@custom_given(ORAPeerAssess) -def test_models_edx_ora_peer_assess_with_valid_statement(statement): +def test_models_edx_ora_peer_assess_with_valid_statement(): """Test that a `openassessmentblock.peer_assess` statement has the expected `event_type` and `page` fields. """ + statement = mock_instance(ORAPeerAssess) assert statement.event_type == "openassessmentblock.peer_assess" assert statement.page == "x_module" -@custom_given(ORASelfAssess) -def test_models_edx_ora_self_assess_with_valid_statement(statement): +def test_models_edx_ora_self_assess_with_valid_statement(): """Test that a `openassessmentblock.self_assess` statement has the expected `event_type` and `page` fields. """ + statement = mock_instance(ORASelfAssess) assert statement.event_type == "openassessmentblock.self_assess" assert statement.page == "x_module" -@custom_given(ORAStaffAssess) -def test_models_edx_ora_staff_assess_with_valid_statement(statement): +def test_models_edx_ora_staff_assess_with_valid_statement(): """Test that a `openassessmentblock.staff_assess` statement has the expected `event_type` and `page` fields. """ + statement = mock_instance(ORAStaffAssess) assert statement.event_type == "openassessmentblock.staff_assess" assert statement.page == "x_module" -@custom_given(ORASubmitFeedbackOnAssessments) -def test_models_edx_ora_submit_feedback_on_assessments_with_valid_statement(statement): +def test_models_edx_ora_submit_feedback_on_assessments_with_valid_statement(): """Test that a `openassessmentblock.submit_feedback_on_assessments` statement has the expected `event_type` and `page` fields. """ + statement = mock_instance(ORASubmitFeedbackOnAssessments) assert statement.event_type == "openassessmentblock.submit_feedback_on_assessments" assert statement.page == "x_module" -@custom_given(ORACreateSubmission) -def test_models_edx_ora_create_submission_with_valid_statement(statement): +def test_models_edx_ora_create_submission_with_valid_statement(): """Test that a `openassessmentblock.create_submission` statement has the expected `event_type` and `page` fields. """ + statement = mock_instance(ORACreateSubmission) assert statement.event_type == "openassessmentblock.create_submission" assert statement.page == "x_module" -@custom_given(ORASaveSubmission) -def test_models_edx_ora_save_submission_with_valid_statement(statement): +def test_models_edx_ora_save_submission_with_valid_statement(): """Test that a `openassessmentblock.save_submission` statement has the expected `event_type` and `page` fields. """ + statement = mock_instance(ORASaveSubmission) assert statement.event_type == "openassessmentblock.save_submission" assert statement.page == "x_module" -@custom_given(ORAStudentTrainingAssessExample) -def test_models_edx_ora_student_training_assess_example_with_valid_statement(statement): +def test_models_edx_ora_student_training_assess_example_with_valid_statement(): """Test that a `openassessment.student_training_assess_example` statement has the expected `event_type` and `page` fields. """ + statement = mock_instance(ORAStudentTrainingAssessExample) assert statement.event_type == "openassessment.student_training_assess_example" assert statement.page == "x_module" -@custom_given(ORAUploadFile) -def test_models_edx_ora_upload_file_example_with_valid_statement(statement): +def test_models_edx_ora_upload_file_example_with_valid_statement(): """Test that a `openassessment.upload_file` statement has the expected `event_type` and `page` fields. """ + statement = mock_instance(ORAUploadFile) assert statement.event_type == "openassessment.upload_file" assert statement.name == "openassessment.upload_file" diff --git a/tests/models/edx/peer_instruction/test_events.py b/tests/models/edx/peer_instruction/test_events.py index 8c9cbe0f3..25c0fbe50 100644 --- a/tests/models/edx/peer_instruction/test_events.py +++ b/tests/models/edx/peer_instruction/test_events.py @@ -7,22 +7,23 @@ from ralph.models.edx.peer_instruction.fields.events import PeerInstructionEventField -from tests.fixtures.hypothesis_strategies import custom_given +# from tests.fixtures.hypothesis_strategies import custom_given +from tests.factories import mock_instance - -@custom_given(PeerInstructionEventField) -def test_models_edx_peer_instruction_event_field_with_valid_field(field): +def test_models_edx_peer_instruction_event_field_with_valid_field(): """Test that a valid `PeerInstructionEventField` does not raise a `ValidationError`. """ + field = mock_instance(PeerInstructionEventField) assert len(field.rationale) <= 12500 -@custom_given(PeerInstructionEventField) -def test_models_edx_peer_instruction_event_field_with_invalid_rationale(field): +def test_models_edx_peer_instruction_event_field_with_invalid_rationale(): """Test that a valid `PeerInstructionEventField` does not raise a `ValidationError`. """ + field = mock_instance(PeerInstructionEventField) + invalid_field = json.loads(field.json()) invalid_field["rationale"] = "x" * 12501 with pytest.raises( diff --git a/tests/models/edx/peer_instruction/test_statements.py b/tests/models/edx/peer_instruction/test_statements.py index 3c2841573..c6b42e951 100644 --- a/tests/models/edx/peer_instruction/test_statements.py +++ b/tests/models/edx/peer_instruction/test_statements.py @@ -12,8 +12,8 @@ ) from ralph.models.selector import ModelSelector -from tests.fixtures.hypothesis_strategies import custom_builds, custom_given - +# from tests.fixtures.hypothesis_strategies import custom_builds, custom_given +from tests.factories import mock_instance @pytest.mark.parametrize( "class_", @@ -23,44 +23,42 @@ PeerInstructionRevisedSubmitted, ], ) -@custom_given(st.data()) -def test_models_edx_peer_instruction_selectors_with_valid_statements(class_, data): +def test_models_edx_peer_instruction_selectors_with_valid_statements(class_): """Test given a valid peer_instruction edX statement the `get_first_model` selector method should return the expected model. """ - statement = json.loads(data.draw(custom_builds(class_)).json()) + statement = json.loads(mock_instance(class_).json()) model = ModelSelector(module="ralph.models.edx").get_first_model(statement) assert model is class_ -@custom_given(PeerInstructionAccessed) def test_models_edx_peer_instruction_accessed_with_valid_statement( - statement, ): """Test that a `ubc.peer_instruction.accessed` statement has the expected `event_type`. """ + statement = mock_instance(PeerInstructionAccessed) assert statement.event_type == "ubc.peer_instruction.accessed" assert statement.name == "ubc.peer_instruction.accessed" -@custom_given(PeerInstructionOriginalSubmitted) def test_models_edx_peer_instruction_original_submitted_with_valid_statement( - statement, + ): """Test that a `ubc.peer_instruction.original_submitted` statement has the expected `event_type`. """ + statement = mock_instance(PeerInstructionOriginalSubmitted) assert statement.event_type == "ubc.peer_instruction.original_submitted" assert statement.name == "ubc.peer_instruction.original_submitted" -@custom_given(PeerInstructionRevisedSubmitted) def test_models_edx_peer_instruction_revised_submitted_with_valid_statement( - statement, + ): """Test that a `ubc.peer_instruction.revised_submitted` statement has the expected `event_type`. """ + statement = mock_instance(PeerInstructionRevisedSubmitted) assert statement.event_type == "ubc.peer_instruction.revised_submitted" assert statement.name == "ubc.peer_instruction.revised_submitted" diff --git a/tests/models/edx/problem_interaction/test_events.py b/tests/models/edx/problem_interaction/test_events.py index 1eef264fc..5ba73f8c2 100644 --- a/tests/models/edx/problem_interaction/test_events.py +++ b/tests/models/edx/problem_interaction/test_events.py @@ -19,22 +19,22 @@ SaveProblemSuccessEventField, ) -from tests.fixtures.hypothesis_strategies import custom_given +# from tests.fixtures.hypothesis_strategies import custom_given +from tests.factories import mock_instance - -@custom_given(CorrectMap) -def test_models_edx_correct_map_with_valid_content(subfield): +def test_models_edx_correct_map_with_valid_content(): """Test that a valid `CorrectMap` does not raise a `ValidationError`.""" + subfield = mock_instance(CorrectMap) assert subfield.correctness in ("correct", "incorrect") assert subfield.hintmode in ("on_request", "always", None) @pytest.mark.parametrize("correctness", ["corect", "incorect"]) -@custom_given(CorrectMap) -def test_models_edx_correct_map_with_invalid_correctness_value(correctness, subfield): +def test_models_edx_correct_map_with_invalid_correctness_value(correctness): """Test that an invalid `correctness` value in `CorrectMap` raises a `ValidationError`. """ + subfield = mock_instance(CorrectMap) invalid_subfield = json.loads(subfield.json()) invalid_subfield["correctness"] = correctness @@ -43,11 +43,11 @@ def test_models_edx_correct_map_with_invalid_correctness_value(correctness, subf @pytest.mark.parametrize("hintmode", ["onrequest", "alway"]) -@custom_given(CorrectMap) -def test_models_edx_correct_map_with_invalid_hintmode_value(hintmode, subfield): +def test_models_edx_correct_map_with_invalid_hintmode_value(hintmode): """Test that an invalid `hintmode` value in `CorrectMap` raises a `ValidationError`. """ + subfield = mock_instance(CorrectMap) invalid_subfield = json.loads(subfield.json()) invalid_subfield["hintmode"] = hintmode @@ -55,11 +55,11 @@ def test_models_edx_correct_map_with_invalid_hintmode_value(hintmode, subfield): CorrectMap(**invalid_subfield) -@custom_given(EdxProblemHintFeedbackDisplayedEventField) -def test_models_edx_problem_hint_feedback_displayed_event_field_with_valid_field(field): +def test_models_edx_problem_hint_feedback_displayed_event_field_with_valid_field(): """Test that a valid `EdxProblemHintFeedbackDisplayedEventField` does not raise a `ValidationError`. """ + field = mock_instance(EdxProblemHintFeedbackDisplayedEventField) assert field.question_type in ( "stringresponse", "choiceresponse", @@ -80,13 +80,14 @@ def test_models_edx_problem_hint_feedback_displayed_event_field_with_valid_field "optionrespons", ], ) -@custom_given(EdxProblemHintFeedbackDisplayedEventField) def test_models_edx_problem_hint_feedback_displayed_event_field_with_invalid_question_type_value( # noqa - question_type, field + question_type ): """Test that an invalid `question_type` value in `EdxProblemHintFeedbackDisplayedEventField` raises a `ValidationError`. """ + field = mock_instance(EdxProblemHintFeedbackDisplayedEventField) + invalid_field = json.loads(field.json()) invalid_field["question_type"] = question_type @@ -95,13 +96,13 @@ def test_models_edx_problem_hint_feedback_displayed_event_field_with_invalid_que @pytest.mark.parametrize("trigger_type", ["jingle", "compund"]) -@custom_given(EdxProblemHintFeedbackDisplayedEventField) def test_models_edx_problem_hint_feedback_displayed_event_field_with_invalid_trigger_type_value( # noqa - trigger_type, field + trigger_type ): """Test that an invalid `question_type` value in `EdxProblemHintFeedbackDisplayedEventField` raises a `ValidationError`. """ + field = mock_instance(EdxProblemHintFeedbackDisplayedEventField) invalid_field = json.loads(field.json()) invalid_field["trigger_type"] = trigger_type @@ -109,11 +110,11 @@ def test_models_edx_problem_hint_feedback_displayed_event_field_with_invalid_tri EdxProblemHintFeedbackDisplayedEventField(**invalid_field) -@custom_given(ProblemCheckEventField) -def test_models_edx_problem_check_event_field_with_valid_field(field): +def test_models_edx_problem_check_event_field_with_valid_field(): """Test that a valid `ProblemCheckEventField` does not raise a `ValidationError`. """ + field = mock_instance(ProblemCheckEventField) assert re.match( ( r"^block-v1:[^\/+]+(\/|\+)[^\/+]+(\/|\+)[^\/?]" @@ -151,13 +152,13 @@ def test_models_edx_problem_check_event_field_with_valid_field(field): ), ], ) -@custom_given(ProblemCheckEventField) def test_models_edx_problem_check_event_field_with_invalid_problem_id_value( - problem_id, field + problem_id ): """Test that an invalid `problem_id` value in `ProblemCheckEventField` raises a `ValidationError`. """ + field = mock_instance(ProblemCheckEventField) invalid_field = json.loads(field.json()) invalid_field["problem_id"] = problem_id @@ -168,13 +169,13 @@ def test_models_edx_problem_check_event_field_with_invalid_problem_id_value( @pytest.mark.parametrize("success", ["corect", "incorect"]) -@custom_given(ProblemCheckEventField) def test_models_edx_problem_check_event_field_with_invalid_success_value( - success, field + success ): """Test that an invalid `success` value in `ProblemCheckEventField` raises a `ValidationError`. """ + field = mock_instance(ProblemCheckEventField) invalid_field = json.loads(field.json()) invalid_field["success"] = success @@ -182,11 +183,11 @@ def test_models_edx_problem_check_event_field_with_invalid_success_value( ProblemCheckEventField(**invalid_field) -@custom_given(ProblemCheckFailEventField) -def test_models_edx_problem_check_fail_event_field_with_valid_field(field): +def test_models_edx_problem_check_fail_event_field_with_valid_field(): """Test that a valid `ProblemCheckFailEventField` does not raise a `ValidationError`. """ + field = mock_instance(ProblemCheckFailEventField) assert re.match( ( r"^block-v1:[^\/+]+(\/|\+)[^\/+]+(\/|\+)[^\/?]" @@ -224,13 +225,13 @@ def test_models_edx_problem_check_fail_event_field_with_valid_field(field): ), ], ) -@custom_given(ProblemCheckFailEventField) def test_models_edx_problem_check_fail_event_field_with_invalid_problem_id_value( - problem_id, field + problem_id ): """Test that an invalid `problem_id` value in `ProblemCheckFailEventField` raises a `ValidationError`. """ + field = mock_instance(ProblemCheckFailEventField) invalid_field = json.loads(field.json()) invalid_field["problem_id"] = problem_id @@ -241,13 +242,13 @@ def test_models_edx_problem_check_fail_event_field_with_invalid_problem_id_value @pytest.mark.parametrize("failure", ["close", "unresit"]) -@custom_given(ProblemCheckFailEventField) def test_models_edx_problem_check_fail_event_field_with_invalid_failure_value( - failure, field + failure ): """Test that an invalid `failure` value in `ProblemCheckFailEventField` raises a `ValidationError`. """ + field = mock_instance(ProblemCheckFailEventField) invalid_field = json.loads(field.json()) invalid_field["failure"] = failure @@ -255,11 +256,11 @@ def test_models_edx_problem_check_fail_event_field_with_invalid_failure_value( ProblemCheckFailEventField(**invalid_field) -@custom_given(ProblemRescoreEventField) -def test_models_edx_problem_rescore_event_field_with_valid_field(field): +def test_models_edx_problem_rescore_event_field_with_valid_field(): """Test that a valid `ProblemRescoreEventField` does not raise a `ValidationError`. """ + field = mock_instance(ProblemRescoreEventField) assert re.match( ( r"^block-v1:[^\/+]+(\/|\+)[^\/+]+(\/|\+)[^\/?]" @@ -297,13 +298,13 @@ def test_models_edx_problem_rescore_event_field_with_valid_field(field): ), ], ) -@custom_given(ProblemRescoreEventField) def test_models_edx_problem_rescore_event_field_with_invalid_problem_id_value( - problem_id, field + problem_id ): """Test that an invalid `problem_id` value in `ProblemRescoreEventField` raises a `ValidationError`. """ + field = mock_instance(ProblemRescoreEventField) invalid_field = json.loads(field.json()) invalid_field["problem_id"] = problem_id @@ -314,13 +315,13 @@ def test_models_edx_problem_rescore_event_field_with_invalid_problem_id_value( @pytest.mark.parametrize("success", ["corect", "incorect"]) -@custom_given(ProblemRescoreEventField) def test_models_edx_problem_rescore_event_field_with_invalid_success_value( - success, field + success ): """Test that an invalid `success` value in `ProblemRescoreEventField` raises a `ValidationError`. """ + field = mock_instance(ProblemRescoreEventField) invalid_field = json.loads(field.json()) invalid_field["success"] = success @@ -328,11 +329,11 @@ def test_models_edx_problem_rescore_event_field_with_invalid_success_value( ProblemRescoreEventField(**invalid_field) -@custom_given(ProblemRescoreFailEventField) -def test_models_edx_problem_rescore_fail_event_field_with_valid_field(field): +def test_models_edx_problem_rescore_fail_event_field_with_valid_field(): """Test that a valid `ProblemRescoreFailEventField` does not raise a `ValidationError`. """ + field = mock_instance(ProblemRescoreFailEventField) assert re.match( ( r"^block-v1:[^\/+]+(\/|\+)[^\/+]+(\/|\+)[^\/?]" @@ -370,13 +371,13 @@ def test_models_edx_problem_rescore_fail_event_field_with_valid_field(field): ), ], ) -@custom_given(ProblemRescoreFailEventField) def test_models_edx_problem_rescore_fail_event_field_with_invalid_problem_id_value( - problem_id, field + problem_id ): """Test that an invalid `problem_id` value in `ProblemRescoreFailEventField` raises a `ValidationError`. """ + field = mock_instance(ProblemRescoreFailEventField) invalid_field = json.loads(field.json()) invalid_field["problem_id"] = problem_id @@ -387,13 +388,13 @@ def test_models_edx_problem_rescore_fail_event_field_with_invalid_problem_id_val @pytest.mark.parametrize("failure", ["close", "unresit"]) -@custom_given(ProblemRescoreFailEventField) def test_models_edx_problem_rescore_fail_event_field_with_invalid_failure_value( - failure, field + failure ): """Test that an invalid `failure` value in `ProblemRescoreFailEventField` raises a `ValidationError`. """ + field = mock_instance(ProblemRescoreFailEventField) invalid_field = json.loads(field.json()) invalid_field["failure"] = failure @@ -401,11 +402,11 @@ def test_models_edx_problem_rescore_fail_event_field_with_invalid_failure_value( ProblemRescoreFailEventField(**invalid_field) -@custom_given(ResetProblemEventField) -def test_models_edx_reset_problem_event_field_with_valid_field(field): +def test_models_edx_reset_problem_event_field_with_valid_field(): """Test that a valid `ResetProblemEventField` does not raise a `ValidationError`. """ + field = mock_instance(ResetProblemEventField) assert re.match( ( r"^block-v1:[^\/+]+(\/|\+)[^\/+]+(\/|\+)[^\/?]" @@ -442,13 +443,13 @@ def test_models_edx_reset_problem_event_field_with_valid_field(field): ), ], ) -@custom_given(ResetProblemEventField) def test_models_edx_reset_problem_event_field_with_invalid_problem_id_value( - problem_id, field + problem_id ): """Test that an invalid `problem_id` value in `ResetProblemEventField` raises a `ValidationError`. """ + field = mock_instance(ResetProblemEventField) invalid_field = json.loads(field.json()) invalid_field["problem_id"] = problem_id @@ -458,11 +459,11 @@ def test_models_edx_reset_problem_event_field_with_invalid_problem_id_value( ResetProblemEventField(**invalid_field) -@custom_given(ResetProblemFailEventField) -def test_models_edx_reset_problem_fail_event_field_with_valid_field(field): +def test_models_edx_reset_problem_fail_event_field_with_valid_field(): """Test that a valid `ResetProblemFailEventField` does not raise a `ValidationError`. """ + field = mock_instance(ResetProblemFailEventField) assert re.match( ( r"^block-v1:[^\/+]+(\/|\+)[^\/+]+(\/|\+)[^\/?]" @@ -500,13 +501,13 @@ def test_models_edx_reset_problem_fail_event_field_with_valid_field(field): ), ], ) -@custom_given(ResetProblemFailEventField) def test_models_edx_reset_problem_fail_event_field_with_invalid_problem_id_value( - problem_id, field + problem_id ): """Test that an invalid `problem_id` value in `ResetProblemFailEventField` raises a `ValidationError`. """ + field = mock_instance(ResetProblemFailEventField) invalid_field = json.loads(field.json()) invalid_field["problem_id"] = problem_id @@ -517,13 +518,13 @@ def test_models_edx_reset_problem_fail_event_field_with_invalid_problem_id_value @pytest.mark.parametrize("failure", ["close", "not_close"]) -@custom_given(ResetProblemFailEventField) def test_models_edx_reset_problem_fail_event_field_with_invalid_failure_value( - failure, field + failure ): """Test that an invalid `failure` value in `ResetProblemFailEventField` raises a `ValidationError`. """ + field = mock_instance(ResetProblemFailEventField) invalid_field = json.loads(field.json()) invalid_field["failure"] = failure @@ -531,11 +532,11 @@ def test_models_edx_reset_problem_fail_event_field_with_invalid_failure_value( ResetProblemFailEventField(**invalid_field) -@custom_given(SaveProblemFailEventField) -def test_models_edx_save_problem_fail_event_field_with_valid_field(field): +def test_models_edx_save_problem_fail_event_field_with_valid_field(): """Test that a valid `SaveProblemFailEventField` does not raise a `ValidationError`. """ + field = mock_instance(SaveProblemFailEventField) assert re.match( ( r"^block-v1:[^\/+]+(\/|\+)[^\/+]+(\/|\+)[^\/?]" @@ -573,13 +574,13 @@ def test_models_edx_save_problem_fail_event_field_with_valid_field(field): ), ], ) -@custom_given(SaveProblemFailEventField) def test_models_edx_save_problem_fail_event_field_with_invalid_problem_id_value( - problem_id, field + problem_id ): """Test that an invalid `problem_id` value in `SaveProblemFailEventField` raises a `ValidationError`. """ + field = mock_instance(SaveProblemFailEventField) invalid_field = json.loads(field.json()) invalid_field["problem_id"] = problem_id @@ -590,13 +591,13 @@ def test_models_edx_save_problem_fail_event_field_with_invalid_problem_id_value( @pytest.mark.parametrize("failure", ["close", "doned"]) -@custom_given(SaveProblemFailEventField) def test_models_edx_save_problem_fail_event_field_with_invalid_failure_value( - failure, field + failure ): """Test that an invalid `failure` value in `SaveProblemFailEventField` raises a `ValidationError`. """ + field = mock_instance(SaveProblemFailEventField) invalid_field = json.loads(field.json()) invalid_field["failure"] = failure @@ -604,11 +605,11 @@ def test_models_edx_save_problem_fail_event_field_with_invalid_failure_value( SaveProblemFailEventField(**invalid_field) -@custom_given(SaveProblemSuccessEventField) -def test_models_edx_save_problem_success_event_field_with_valid_field(field): +def test_models_edx_save_problem_success_event_field_with_valid_field(): """Test that a valid `SaveProblemFailEventField` does not raise a `ValidationError`. """ + field = mock_instance(SaveProblemSuccessEventField) assert re.match( ( r"^block-v1:[^\/+]+(\/|\+)[^\/+]+(\/|\+)[^\/?]" @@ -645,13 +646,13 @@ def test_models_edx_save_problem_success_event_field_with_valid_field(field): ), ], ) -@custom_given(SaveProblemSuccessEventField) def test_models_edx_save_problem_success_event_field_with_invalid_problem_id_value( - problem_id, field + problem_id ): """Test that an invalid `problem_id` value in `SaveProblemSuccessEventField` raises a `ValidationError`. """ + field = mock_instance(SaveProblemSuccessEventField) invalid_field = json.loads(field.json()) invalid_field["problem_id"] = problem_id diff --git a/tests/models/edx/problem_interaction/test_statements.py b/tests/models/edx/problem_interaction/test_statements.py index 0dbb53e19..c3573a040 100644 --- a/tests/models/edx/problem_interaction/test_statements.py +++ b/tests/models/edx/problem_interaction/test_statements.py @@ -25,8 +25,8 @@ ) from ralph.models.selector import ModelSelector -from tests.fixtures.hypothesis_strategies import custom_builds, custom_given - +# from tests.fixtures.hypothesis_strategies import custom_builds, custom_given +from tests.factories import mock_instance @pytest.mark.parametrize( "class_", @@ -49,159 +49,157 @@ UIProblemShow, ], ) -@custom_given(st.data()) def test_models_edx_edx_problem_interaction_selectors_with_valid_statements( - class_, data + class_ ): """Test given a valid problem interaction edX statement the `get_first_model` selector method should return the expected model. """ - statement = json.loads(data.draw(custom_builds(class_)).json()) + statement = json.loads(mock_instance(class_).json()) model = ModelSelector(module="ralph.models.edx").get_first_model(statement) assert model is class_ -@custom_given(EdxProblemHintDemandhintDisplayed) def test_models_edx_edx_problem_hint_demandhint_displayed_with_valid_statement( - statement, ): """Test that a `edx.problem.hint.demandhint_displayed` statement has the expected `event_type` and `page`. """ + statement = mock_instance(EdxProblemHintDemandhintDisplayed) assert statement.event_type == "edx.problem.hint.demandhint_displayed" assert statement.page == "x_module" -@custom_given(EdxProblemHintFeedbackDisplayed) -def test_models_edx_edx_problem_hint_feedback_displayed_with_valid_statement(statement): +def test_models_edx_edx_problem_hint_feedback_displayed_with_valid_statement(): """Test that a `edx.problem.hint.feedback_displayed` statement has the expected `event_type` and `page`. """ + statement = mock_instance(EdxProblemHintFeedbackDisplayed) assert statement.event_type == "edx.problem.hint.feedback_displayed" assert statement.page == "x_module" -@custom_given(UIProblemCheck) -def test_models_edx_ui_problem_check_with_valid_statement(statement): +def test_models_edx_ui_problem_check_with_valid_statement(): """Test that a `problem_check` browser statement has the expected `event_type` and `name`. """ + statement = mock_instance(UIProblemCheck) assert statement.event_type == "problem_check" assert statement.name == "problem_check" -@custom_given(ProblemCheck) -def test_models_edx_problem_check_with_valid_statement(statement): +def test_models_edx_problem_check_with_valid_statement(): """Test that a `problem_check` server statement has the expected `event_type` and `page`. """ + statement = mock_instance(ProblemCheck) assert statement.event_type == "problem_check" assert statement.page == "x_module" -@custom_given(ProblemCheckFail) -def test_models_edx_problem_check_fail_with_valid_statement(statement): +def test_models_edx_problem_check_fail_with_valid_statement(): """Test that a `problem_check_fail` server statement has the expected `event_type` and `page`. """ + statement = mock_instance(ProblemCheckFail) assert statement.event_type == "problem_check_fail" assert statement.page == "x_module" -@custom_given(UIProblemGraded) -def test_models_edx_ui_problem_graded_with_valid_statement(statement): +def test_models_edx_ui_problem_graded_with_valid_statement(): """Test that a `problem_graded` browser statement has the expected `event_type` and `name`. """ + statement = mock_instance(UIProblemGraded) assert statement.event_type == "problem_graded" assert statement.name == "problem_graded" -@custom_given(ProblemRescore) -def test_models_edx_problem_rescore_with_valid_statement(statement): +def test_models_edx_problem_rescore_with_valid_statement(): """Test that a `problem_rescore` server statement has the expected `event_type` and `page`. """ + statement = mock_instance(ProblemRescore) assert statement.event_type == "problem_rescore" assert statement.page == "x_module" -@custom_given(ProblemRescoreFail) -def test_models_edx_problem_rescore_fail_with_valid_statement(statement): +def test_models_edx_problem_rescore_fail_with_valid_statement(): """Test that a `problem_rescore` server statement has the expected `event_type` and `page`. """ + statement = mock_instance(ProblemRescoreFail) assert statement.event_type == "problem_rescore_fail" assert statement.page == "x_module" -@custom_given(UIProblemReset) -def test_models_edx_ui_problem_reset_with_valid_statement(statement): +def test_models_edx_ui_problem_reset_with_valid_statement(): """Test that a `problem_reset` browser statement has the expected `event_type` and `name`. """ + statement = mock_instance(UIProblemReset) assert statement.event_type == "problem_reset" assert statement.name == "problem_reset" -@custom_given(UIProblemSave) -def test_models_edx_ui_problem_save_with_valid_statement(statement): +def test_models_edx_ui_problem_save_with_valid_statement(): """Test that a `problem_save` browser statement has the expected `event_type` and `name`. """ + statement = mock_instance(UIProblemSave) assert statement.event_type == "problem_save" assert statement.name == "problem_save" -@custom_given(UIProblemShow) -def test_models_edx_ui_problem_show_with_valid_statement(statement): +def test_models_edx_ui_problem_show_with_valid_statement(): """Test that a `problem_show` browser statement has the expected `event_type` and `name`. """ + statement = mock_instance(UIProblemShow) assert statement.event_type == "problem_show" assert statement.name == "problem_show" -@custom_given(ResetProblem) -def test_models_edx_reset_problem_with_valid_statement(statement): +def test_models_edx_reset_problem_with_valid_statement(): """Test that a `reset_problem` server statement has the expected `event_type` and `page`. """ + statement = mock_instance(ResetProblem) assert statement.event_type == "reset_problem" assert statement.page == "x_module" -@custom_given(ResetProblemFail) -def test_models_edx_reset_problem_fail_with_valid_statement(statement): +def test_models_edx_reset_problem_fail_with_valid_statement(): """Test that a `reset_problem_fail` server statement has the expected `event_type` and `page`. """ + statement = mock_instance(ResetProblemFail) assert statement.event_type == "reset_problem_fail" assert statement.page == "x_module" -@custom_given(SaveProblemFail) -def test_models_edx_save_problem_fail_with_valid_statement(statement): +def test_models_edx_save_problem_fail_with_valid_statement(): """Test that a `save_problem_fail` server statement has the expected `event_type` and `page`. """ + statement = mock_instance(SaveProblemFail) assert statement.event_type == "save_problem_fail" assert statement.page == "x_module" -@custom_given(SaveProblemSuccess) -def test_models_edx_save_problem_success_with_valid_statement(statement): +def test_models_edx_save_problem_success_with_valid_statement(): """Test that a `save_problem_success` server statement has the expected `event_type` and `page`. """ + statement = mock_instance(SaveProblemSuccess) assert statement.event_type == "save_problem_success" assert statement.page == "x_module" -@custom_given(ShowAnswer) -def test_models_edx_show_answer_with_valid_statement(statement): +def test_models_edx_show_answer_with_valid_statement(): """Test that a `showanswer` server statement has the expected `event_type` and `page`. """ + statement = mock_instance(ShowAnswer) assert statement.event_type == "showanswer" assert statement.page == "x_module" diff --git a/tests/models/edx/textbook_interaction/test_events.py b/tests/models/edx/textbook_interaction/test_events.py index 024fff7ac..f94c90729 100644 --- a/tests/models/edx/textbook_interaction/test_events.py +++ b/tests/models/edx/textbook_interaction/test_events.py @@ -11,14 +11,14 @@ TextbookPdfChapterNavigatedEventField, ) -from tests.fixtures.hypothesis_strategies import custom_given +# from tests.fixtures.hypothesis_strategies import custom_given +from tests.factories import mock_instance - -@custom_given(TextbookInteractionBaseEventField) -def test_fields_edx_textbook_interaction_base_event_field_with_valid_content(field): +def test_fields_edx_textbook_interaction_base_event_field_with_valid_content(): """Test that a valid `TextbookInteractionBaseEventField` does not raise a `ValidationError`. """ + field = mock_instance(TextbookInteractionBaseEventField) assert re.match( r"^\/asset-v1:[^\/+]+(\/|\+)[^\/+]+(\/|\+)[^\/?]+type@asset\+block.+$", @@ -61,13 +61,13 @@ def test_fields_edx_textbook_interaction_base_event_field_with_valid_content(fie ), ), ) -@custom_given(TextbookInteractionBaseEventField) def test_fields_edx_textbook_interaction_base_event_field_with_invalid_content( - chapter, field + chapter ): """Test that an invalid `TextbookInteractionBaseEventField` raises a `ValidationError`. """ + field = mock_instance(TextbookInteractionBaseEventField) invalid_field = json.loads(field.json()) invalid_field["chapter"] = chapter @@ -76,13 +76,13 @@ def test_fields_edx_textbook_interaction_base_event_field_with_invalid_content( TextbookInteractionBaseEventField(**invalid_field) -@custom_given(TextbookPdfChapterNavigatedEventField) def test_fields_edx_textbook_pdf_chapter_navigated_event_field_with_valid_content( - field, + ): """Test that a valid `TextbookPdfChapterNavigatedEventField` does not raise a `ValidationError`. """ + field = mock_instance(TextbookPdfChapterNavigatedEventField) assert re.match( (r"^\/asset-v1:[^\/+]+(\/|\+)[^\/+]+(\/|\+)[^\/?]+type@asset\+block.+$"), @@ -121,13 +121,13 @@ def test_fields_edx_textbook_pdf_chapter_navigated_event_field_with_valid_conten ), ), ) -@custom_given(TextbookPdfChapterNavigatedEventField) def test_fields_edx_textbook_pdf_chapter_navigated_event_field_with_invalid_content( - chapter, field + chapter ): """Test that an invalid `TextbookPdfChapterNavigatedEventField` raises a `ValidationError`. """ + field = mock_instance(TextbookPdfChapterNavigatedEventField) invalid_field = json.loads(field.json()) invalid_field["chapter"] = chapter diff --git a/tests/models/edx/textbook_interaction/test_statements.py b/tests/models/edx/textbook_interaction/test_statements.py index 1218d74ce..be22264fd 100644 --- a/tests/models/edx/textbook_interaction/test_statements.py +++ b/tests/models/edx/textbook_interaction/test_statements.py @@ -23,8 +23,8 @@ ) from ralph.models.selector import ModelSelector -from tests.fixtures.hypothesis_strategies import custom_builds, custom_given - +# from tests.fixtures.hypothesis_strategies import custom_builds, custom_given +from tests.factories import mock_instance @pytest.mark.parametrize( "class_", @@ -45,153 +45,147 @@ UITextbookPdfZoomMenuChanged, ], ) -@custom_given(st.data()) def test_models_edx_ui_textbook_interaction_selectors_with_valid_statements( - class_, data + class_ ): """Test given a valid textbook interaction edX statement the `get_first_model` selector method should return the expected model. """ - statement = json.loads(data.draw(custom_builds(class_)).json()) + statement = json.loads(mock_instance(class_).json()) model = ModelSelector(module="ralph.models.edx").get_first_model(statement) assert model is class_ -@custom_given(UIBook) -def test_models_edx_ui_book_with_valid_statement(statement): +def test_models_edx_ui_book_with_valid_statement(): """Test that a `book` statement has the expected `event_type` and `name`.""" + statement = mock_instance(UIBook) assert statement.event_type == "book" assert statement.name == "book" -@custom_given(UITextbookPdfThumbnailsToggled) -def test_models_edx_ui_textbook_pdf_thumbnails_toggled_with_valid_statement(statement): +def test_models_edx_ui_textbook_pdf_thumbnails_toggled_with_valid_statement(): """Test that a `textbook.pdf.thumbnails.toggled` statement has the expected `event_type` and `name`. """ + statement = mock_instance(UITextbookPdfThumbnailsToggled) assert statement.event_type == "textbook.pdf.thumbnails.toggled" assert statement.name == "textbook.pdf.thumbnails.toggled" -@custom_given(UITextbookPdfThumbnailNavigated) def test_models_edx_ui_textbook_pdf_thumbnail_navigated_with_valid_statement( - statement, ): """Test that a `textbook.pdf.thumbnail.navigated` statement has the expected `event_type` and `name`. """ + statement = mock_instance(UITextbookPdfThumbnailNavigated) assert statement.event_type == "textbook.pdf.thumbnail.navigated" assert statement.name == "textbook.pdf.thumbnail.navigated" -@custom_given(UITextbookPdfOutlineToggled) def test_models_edx_ui_textbook_pdf_outline_toggled_with_valid_statement( - statement, ): """Test that a `textbook.pdf.outline.toggled` statement has the expected `event_type` and `name`. """ + statement = mock_instance(UITextbookPdfOutlineToggled) assert statement.event_type == "textbook.pdf.outline.toggled" assert statement.name == "textbook.pdf.outline.toggled" -@custom_given(UITextbookPdfChapterNavigated) def test_models_edx_ui_textbook_pdf_chapter_navigated_with_valid_statement( - statement, ): """Test that a `textbook.pdf.chapter.navigated` statement has the expected `event_type` and `name`. """ + statement = mock_instance(UITextbookPdfChapterNavigated) assert statement.event_type == "textbook.pdf.chapter.navigated" assert statement.name == "textbook.pdf.chapter.navigated" -@custom_given(UITextbookPdfPageNavigated) def test_models_edx_ui_textbook_pdf_page_navigated_with_valid_statement( - statement, ): """Test that a `textbook.pdf.page.navigated` statement has the expected `event_type` and `name`. """ + statement = mock_instance(UITextbookPdfPageNavigated) assert statement.event_type == "textbook.pdf.page.navigated" assert statement.name == "textbook.pdf.page.navigated" -@custom_given(UITextbookPdfZoomButtonsChanged) def test_models_edx_ui_textbook_pdf_zoom_buttons_changed_with_valid_statement( - statement, ): """Test that a `textbook.pdf.zoom.buttons.changed` statement has the expected `event_type` and `name`. """ + statement = mock_instance(UITextbookPdfZoomButtonsChanged) assert statement.event_type == "textbook.pdf.zoom.buttons.changed" assert statement.name == "textbook.pdf.zoom.buttons.changed" -@custom_given(UITextbookPdfZoomMenuChanged) -def test_models_edx_ui_textbook_pdf_zoom_menu_changed_with_valid_statement(statement): +def test_models_edx_ui_textbook_pdf_zoom_menu_changed_with_valid_statement(): """Test that a `textbook.pdf.zoom.menu.changed` has the expected `event_type` and `name`. """ + statement = mock_instance(UITextbookPdfZoomMenuChanged) assert statement.event_type == "textbook.pdf.zoom.menu.changed" assert statement.name == "textbook.pdf.zoom.menu.changed" -@custom_given(UITextbookPdfDisplayScaled) -def test_models_edx_ui_textbook_pdf_display_scaled_with_valid_statement(statement): +def test_models_edx_ui_textbook_pdf_display_scaled_with_valid_statement(): """Test that a `textbook.pdf.display.scaled` statement has the expected `event_type` and `name`. """ + statement = mock_instance(UITextbookPdfDisplayScaled) assert statement.event_type == "textbook.pdf.display.scaled" assert statement.name == "textbook.pdf.display.scaled" -@custom_given(UITextbookPdfPageScrolled) -def test_models_edx_ui_textbook_pdf_page_scrolled_with_valid_statement(statement): +def test_models_edx_ui_textbook_pdf_page_scrolled_with_valid_statement(): """Test that a `textbook.pdf.page.scrolled` statement has the expected `event_type` and `name`. """ + statement = mock_instance(UITextbookPdfPageScrolled) assert statement.event_type == "textbook.pdf.page.scrolled" assert statement.name == "textbook.pdf.page.scrolled" -@custom_given(UITextbookPdfSearchExecuted) -def test_models_edx_ui_textbook_pdf_search_executed_with_valid_statement(statement): +def test_models_edx_ui_textbook_pdf_search_executed_with_valid_statement(): """Test that a `textbook.pdf.search.executed` statement has the expected `event_type` and `name`. """ + statement = mock_instance(UITextbookPdfSearchExecuted) assert statement.event_type == "textbook.pdf.search.executed" assert statement.name == "textbook.pdf.search.executed" -@custom_given(UITextbookPdfSearchNavigatedNext) def test_models_edx_ui_textbook_pdf_search_navigated_next_with_valid_statement( - statement, + ): """Test that a `textbook.pdf.search.navigatednext` statement has the expected `event_type` and `name`. """ + statement = mock_instance(UITextbookPdfSearchNavigatedNext) assert statement.event_type == "textbook.pdf.search.navigatednext" assert statement.name == "textbook.pdf.search.navigatednext" -@custom_given(UITextbookPdfSearchHighlightToggled) def test_models_edx_ui_textbook_pdf_search_highlight_toggled_with_valid_statement( - statement, + ): """Test that a `textbook.pdf.search.highlight.toggled` statement has the expected `event_type` and `name`. """ + statement = mock_instance(UITextbookPdfSearchHighlightToggled) assert statement.event_type == "textbook.pdf.search.highlight.toggled" assert statement.name == "textbook.pdf.search.highlight.toggled" -@custom_given(UITextbookPdfSearchCaseSensitivityToggled) def test_models_edx_ui_textbook_pdf_search_case_sensitivity_toggled_with_valid_statement( # noqa - statement, + ): """Test that a `textbook.pdf.searchcasesensitivity.toggled` statement has the expected `event_type` and `name`. """ + statement = mock_instance(UITextbookPdfSearchCaseSensitivityToggled) assert statement.event_type == "textbook.pdf.searchcasesensitivity.toggled" assert statement.name == "textbook.pdf.searchcasesensitivity.toggled" diff --git a/tests/models/edx/video/test_events.py b/tests/models/edx/video/test_events.py index 166589e72..5d980c83b 100644 --- a/tests/models/edx/video/test_events.py +++ b/tests/models/edx/video/test_events.py @@ -7,14 +7,14 @@ from ralph.models.edx.video.fields.events import SpeedChangeVideoEventField -from tests.fixtures.hypothesis_strategies import custom_given +# from tests.fixtures.hypothesis_strategies import custom_given +from tests.factories import mock_instance - -@custom_given(SpeedChangeVideoEventField) -def test_models_edx_speed_change_video_event_field_with_valid_field(field): +def test_models_edx_speed_change_video_event_field_with_valid_field(): """Test that a valid `SpeedChangeVideoEventField` does not raise a `ValidationError`. """ + field = mock_instance(SpeedChangeVideoEventField) assert field.old_speed in ["0.75", "1.0", "1.25", "1.50", "2.0"] assert field.new_speed in ["0.75", "1.0", "1.25", "1.50", "2.0"] @@ -23,13 +23,13 @@ def test_models_edx_speed_change_video_event_field_with_valid_field(field): "old_speed", ["0,75", "1", "-1.0", "1.30"], ) -@custom_given(SpeedChangeVideoEventField) def test_models_edx_speed_change_video_event_field_with_invalid_old_speed_value( - old_speed, field + old_speed ): """Test that an invalid `old_speed` value in `SpeedChangeVideoEventField` raises a `ValidationError`. """ + field = mock_instance(SpeedChangeVideoEventField) invalid_field = json.loads(field.json()) invalid_field["old_speed"] = old_speed @@ -41,13 +41,13 @@ def test_models_edx_speed_change_video_event_field_with_invalid_old_speed_value( "new_speed", ["0,75", "1", "-1.0", "1.30"], ) -@custom_given(SpeedChangeVideoEventField) def test_models_edx_speed_change_video_event_field_with_invalid_new_speed_value( - new_speed, field + new_speed ): """Test that an invalid `new_speed` value in `SpeedChangeVideoEventField` raises a `ValidationError`. """ + field = mock_instance(SpeedChangeVideoEventField) invalid_field = json.loads(field.json()) invalid_field["new_speed"] = new_speed diff --git a/tests/models/edx/video/test_statements.py b/tests/models/edx/video/test_statements.py index e2dec9269..39689cad6 100644 --- a/tests/models/edx/video/test_statements.py +++ b/tests/models/edx/video/test_statements.py @@ -3,7 +3,6 @@ import json import pytest -from hypothesis import strategies as st from ralph.models.edx.video.statements import ( UIHideTranscript, @@ -19,8 +18,8 @@ ) from ralph.models.selector import ModelSelector -from tests.fixtures.hypothesis_strategies import custom_builds, custom_given - +# from tests.fixtures.hypothesis_strategies import custom_builds, custom_given +from tests.factories import mock_instance @pytest.mark.parametrize( "class_", @@ -37,98 +36,89 @@ UIVideoShowCCMenu, ], ) -@custom_given(st.data()) -def test_models_edx_video_selectors_with_valid_statements(class_, data): +def test_models_edx_video_selectors_with_valid_statements(class_): """Test given a valid video edX statement the `get_first_model` selector method should return the expected model. """ - statement = json.loads(data.draw(custom_builds(class_)).json()) + statement = json.loads(mock_instance(class_).json()) model = ModelSelector(module="ralph.models.edx").get_first_model(statement) assert model is class_ -@custom_given(UIPlayVideo) def test_models_edx_ui_play_video_with_valid_statement( - statement, + ): + statement = mock_instance(UIPlayVideo) """Test that a `play_video` statement has the expected `event_type`.""" assert statement.event_type == "play_video" -@custom_given(UIPauseVideo) def test_models_edx_ui_pause_video_with_valid_statement( - statement, + ): + statement = mock_instance(UIPauseVideo) """Test that a `pause_video` statement has the expected `event_type`.""" assert statement.event_type == "pause_video" -@custom_given(UILoadVideo) def test_models_edx_ui_load_video_with_valid_statement( - statement, ): + statement = mock_instance(UILoadVideo) """Test that a `load_video` statement has the expected `event_type` and `name`.""" assert statement.event_type == "load_video" assert statement.name in {"load_video", "edx.video.loaded"} -@custom_given(UISeekVideo) def test_models_edx_ui_seek_video_with_valid_statement( - statement, ): + statement = mock_instance(UISeekVideo) """Test that a `seek_video` statement has the expected `event_type`.""" assert statement.event_type == "seek_video" -@custom_given(UIStopVideo) def test_models_edx_ui_stop_video_with_valid_statement( - statement, ): + statement = mock_instance(UIStopVideo) """Test that a `stop_video` statement has the expected `event_type`.""" assert statement.event_type == "stop_video" -@custom_given(UIHideTranscript) def test_models_edx_ui_hide_transcript_with_valid_statement( - statement, ): """Test that a `hide_transcript` statement has the expected `event_type` and `name`. """ + statement = mock_instance(UIHideTranscript) assert statement.event_type == "hide_transcript" assert statement.name in {"hide_transcript", "edx.video.transcript.hidden"} -@custom_given(UIShowTranscript) def test_models_edx_ui_show_transcript_with_valid_statement( - statement, ): """Test that a `show_transcript` statement has the expected `event_type` and `name. """ + statement = mock_instance(UIShowTranscript) assert statement.event_type == "show_transcript" assert statement.name in {"show_transcript", "edx.video.transcript.shown"} -@custom_given(UISpeedChangeVideo) def test_models_edx_ui_speed_change_video_with_valid_statement( - statement, ): """Test that a `speed_change_video` statement has the expected `event_type`.""" + statement = mock_instance(UISpeedChangeVideo) assert statement.event_type == "speed_change_video" -@custom_given(UIVideoHideCCMenu) def test_models_edx_ui_vide_hide_cc_menu_with_valid_statement( - statement, ): """Test that a `video_hide_cc_menu` statement has the expected `event_type`.""" + statement = mock_instance(UIVideoHideCCMenu) assert statement.event_type == "video_hide_cc_menu" -@custom_given(UIVideoShowCCMenu) def test_models_edx_ui_video_show_cc_menu_with_valid_statement( - statement, ): """Test that a `video_show_cc_menu` statement has the expected `event_type`.""" + statement = mock_instance(UIVideoShowCCMenu) assert statement.event_type == "video_show_cc_menu" diff --git a/tests/models/test_validator.py b/tests/models/test_validator.py index 89e35c640..976da564d 100644 --- a/tests/models/test_validator.py +++ b/tests/models/test_validator.py @@ -156,7 +156,6 @@ def test_models_validator_validate_with_valid_events( assert json.loads(next(result)) == event_dict -# @settings(suppress_health_check=(HealthCheck.function_scoped_fixture,)) # @custom_given(UIPageClose) def test_models_validator_validate_counter(caplog): """Test given multiple events the validate method diff --git a/tests/models/xapi/base/test_agents.py b/tests/models/xapi/base/test_agents.py index 91685ddbe..56b4e248a 100644 --- a/tests/models/xapi/base/test_agents.py +++ b/tests/models/xapi/base/test_agents.py @@ -10,14 +10,14 @@ # from tests.fixtures.hypothesis_strategies import custom_given -from tests.factories import mock_instance +from tests.factories import mock_xapi_instance def test_models_xapi_base_agent_with_mbox_sha1_sum_ifi_with_valid_field(): """Test a valid BaseXapiAgentWithMboxSha1Sum has the expected `mbox_sha1sum` regex. """ - field = mock_instance(BaseXapiAgentWithMboxSha1Sum) + field = mock_xapi_instance(BaseXapiAgentWithMboxSha1Sum) assert re.match(r"^[0-9a-f]{40}$", field.mbox_sha1sum) @@ -37,7 +37,7 @@ def test_models_xapi_base_agent_with_mbox_sha1_sum_ifi_with_invalid_field( BaseXapiAgentWithMboxSha1Sum raises a `ValidationError`. """ - field = mock_instance(BaseXapiAgentWithMboxSha1Sum) + field = mock_xapi_instance(BaseXapiAgentWithMboxSha1Sum) invalid_field = json.loads(field.json()) invalid_field["mbox_sha1sum"] = mbox_sha1sum diff --git a/tests/models/xapi/base/test_groups.py b/tests/models/xapi/base/test_groups.py index c3ad162c1..6ae2edfa2 100644 --- a/tests/models/xapi/base/test_groups.py +++ b/tests/models/xapi/base/test_groups.py @@ -2,15 +2,14 @@ from ralph.models.xapi.base.groups import BaseXapiGroupCommonProperties -from tests.fixtures.hypothesis_strategies import custom_given +# from tests.fixtures.hypothesis_strategies import custom_given +from tests.factories import mock_xapi_instance - -@custom_given(BaseXapiGroupCommonProperties) def test_models_xapi_base_groups_group_common_properties_with_valid_field( - field, ): """Test a valid BaseXapiGroupCommonProperties has the expected `objectType` value. """ + field = mock_xapi_instance(BaseXapiGroupCommonProperties) assert field.objectType == "Group" diff --git a/tests/models/xapi/base/test_objects.py b/tests/models/xapi/base/test_objects.py index 8129c48a5..1a1b833a6 100644 --- a/tests/models/xapi/base/test_objects.py +++ b/tests/models/xapi/base/test_objects.py @@ -2,11 +2,11 @@ from ralph.models.xapi.base.objects import BaseXapiSubStatement -from tests.fixtures.hypothesis_strategies import custom_given +# from tests.fixtures.hypothesis_strategies import custom_given +from tests.factories import mock_xapi_instance - -@custom_given(BaseXapiSubStatement) -def test_models_xapi_object_base_sub_statement_type_with_valid_field(field): +def test_models_xapi_object_base_sub_statement_type_with_valid_field(): """Test a valid BaseXapiSubStatement has the expected `objectType` value.""" + field = mock_xapi_instance(BaseXapiSubStatement) assert field.objectType == "SubStatement" diff --git a/tests/models/xapi/base/test_results.py b/tests/models/xapi/base/test_results.py index 61164f411..249cdc907 100644 --- a/tests/models/xapi/base/test_results.py +++ b/tests/models/xapi/base/test_results.py @@ -7,8 +7,8 @@ from ralph.models.xapi.base.results import BaseXapiResultScore -from tests.fixtures.hypothesis_strategies import custom_given - +# from tests.fixtures.hypothesis_strategies import custom_given +from tests.factories import mock_xapi_instance @pytest.mark.parametrize( "raw_value, min_value, max_value, error_msg", @@ -18,13 +18,13 @@ (12, 5, 10, "raw cannot be greater than max"), ], ) -@custom_given(BaseXapiResultScore) def test_models_xapi_base_result_score_with_invalid_raw_min_max_relation( - raw_value, min_value, max_value, error_msg, field + raw_value, min_value, max_value, error_msg ): """Test invalids `raw`,`min`,`max` relation in BaseXapiResultScore raises ValidationError. """ + field = mock_xapi_instance(BaseXapiResultScore) invalid_field = json.loads(field.json()) invalid_field["raw"] = raw_value diff --git a/tests/models/xapi/base/test_unnested_objects.py b/tests/models/xapi/base/test_unnested_objects.py index a5bc5a24f..edd9152b6 100644 --- a/tests/models/xapi/base/test_unnested_objects.py +++ b/tests/models/xapi/base/test_unnested_objects.py @@ -12,22 +12,20 @@ BaseXapiStatementRef, ) -from tests.fixtures.hypothesis_strategies import custom_given +# from tests.fixtures.hypothesis_strategies import custom_given +from tests.factories import mock_xapi_instance -@custom_given(BaseXapiStatementRef) -def test_models_xapi_base_object_statement_ref_type_with_valid_field(field): +def test_models_xapi_base_object_statement_ref_type_with_valid_field(): """Test a valid BaseXapiStatementRef has the expected `objectType` value.""" - + field = mock_xapi_instance(BaseXapiStatementRef) assert field.objectType == "StatementRef" -@custom_given(BaseXapiInteractionComponent) def test_models_xapi_base_object_interaction_component_with_valid_field( - field, ): """Test a valid BaseXapiInteractionComponent has the expected `id` regex.""" - + field = mock_xapi_instance(BaseXapiInteractionComponent) assert re.match(r"^[^\s]+$", field.id) @@ -35,13 +33,13 @@ def test_models_xapi_base_object_interaction_component_with_valid_field( "id_value", [" test_id", "\ntest"], ) -@custom_given(BaseXapiInteractionComponent) def test_models_xapi_base_object_interaction_component_with_invalid_field( - id_value, field + id_value ): """Test an invalid `id` property in BaseXapiInteractionComponent raises a `ValidationError`. """ + field = mock_xapi_instance(BaseXapiInteractionComponent) invalid_property = json.loads(field.json()) invalid_property["id"] = id_value @@ -50,13 +48,12 @@ def test_models_xapi_base_object_interaction_component_with_invalid_field( BaseXapiInteractionComponent(**invalid_property) -@custom_given(BaseXapiActivityInteractionDefinition) def test_models_xapi_base_object_activity_type_interaction_definition_with_valid_field( - field, ): """Test a valid BaseXapiActivityInteractionDefinition has the expected `objectType` value. """ + field = mock_xapi_instance(BaseXapiActivityInteractionDefinition) assert field.interactionType in ( "true-false", diff --git a/tests/models/xapi/concepts/test_activity_types.py b/tests/models/xapi/concepts/test_activity_types.py index e0d535084..05b38f3ae 100644 --- a/tests/models/xapi/concepts/test_activity_types.py +++ b/tests/models/xapi/concepts/test_activity_types.py @@ -26,10 +26,9 @@ VirtualClassroomActivity, ) -from tests.fixtures.hypothesis_strategies import custom_builds, custom_given +# from tests.fixtures.hypothesis_strategies import custom_builds, custom_given +from tests.factories import mock_xapi_instance - -@settings(deadline=None) @pytest.mark.parametrize( "class_, definition_type", [ @@ -50,12 +49,11 @@ (DocumentActivity, "http://id.tincanapi.com/activitytype/document"), ], ) -@custom_given(st.data()) def test_models_xapi_concept_activity_types_with_valid_field( - class_, definition_type, data + class_, definition_type ): """Test that a valid xAPI activity has the expected the `definition`.`type` value. """ - field = json.loads(data.draw(custom_builds(class_)).json()) + field = json.loads(mock_xapi_instance(class_).json()) assert field["definition"]["type"] == definition_type diff --git a/tests/models/xapi/concepts/test_verbs.py b/tests/models/xapi/concepts/test_verbs.py index a69a4a097..7cc947608 100644 --- a/tests/models/xapi/concepts/test_verbs.py +++ b/tests/models/xapi/concepts/test_verbs.py @@ -42,10 +42,9 @@ UnsharedScreenVerb, ) -from tests.fixtures.hypothesis_strategies import custom_builds, custom_given +# from tests.fixtures.hypothesis_strategies import custom_builds, custom_given +from tests.factories import mock_xapi_instance - -@settings(deadline=None) @pytest.mark.parametrize( "class_, verb_id", [ @@ -89,8 +88,7 @@ ), ], ) -@custom_given(st.data()) -def test_models_xapi_concept_verbs_with_valid_field(class_, verb_id, data): +def test_models_xapi_concept_verbs_with_valid_field(class_, verb_id): """Test that a valid xAPI verb has the expected the `id` value.""" - field = json.loads(data.draw(custom_builds(class_)).json()) + field = json.loads(mock_xapi_instance(class_).json()) assert field["id"] == verb_id diff --git a/tests/models/xapi/test_lms.py b/tests/models/xapi/test_lms.py index 82e9efe87..b4073f70c 100644 --- a/tests/models/xapi/test_lms.py +++ b/tests/models/xapi/test_lms.py @@ -24,10 +24,9 @@ LMSUploadedVideo, ) -from tests.fixtures.hypothesis_strategies import custom_builds, custom_given +# from tests.fixtures.hypothesis_strategies import custom_builds, custom_given +from tests.factories import mock_xapi_instance - -@settings(deadline=None) @pytest.mark.parametrize( "class_", [ @@ -45,21 +44,20 @@ LMSUploadedAudio, ], ) -@custom_given(st.data()) -def test_models_xapi_lms_selectors_with_valid_statements(class_, data): +def test_models_xapi_lms_selectors_with_valid_statements(class_): """Test given a valid LMS xAPI statement the `get_first_model` selector method should return the expected model. """ - statement = json.loads(data.draw(custom_builds(class_)).json()) + statement = json.loads(mock_xapi_instance(class_).json()) model = ModelSelector(module="ralph.models.xapi").get_first_model(statement) assert model is class_ -@custom_given(LMSRegisteredCourse) -def test_models_xapi_lms_registered_course_with_valid_statement(statement): +def test_models_xapi_lms_registered_course_with_valid_statement(): """Test that a valid registered to a course statement has the expected `verb`.`id` and `object`.`definition`.`type` property values. """ + statement = mock_xapi_instance(LMSRegisteredCourse) assert statement.verb.id == "http://adlnet.gov/expapi/verbs/registered" assert ( @@ -67,11 +65,11 @@ def test_models_xapi_lms_registered_course_with_valid_statement(statement): ) -@custom_given(LMSUnregisteredCourse) -def test_models_xapi_lms_unregistered_course_with_valid_statement(statement): +def test_models_xapi_lms_unregistered_course_with_valid_statement(): """Test that a valid unregistered to a course statement has the expected `verb`.`id` and `object`.`definition`.`type` property values. """ + statement = mock_xapi_instance(LMSUnregisteredCourse) assert statement.verb.id == "http://id.tincanapi.com/verb/unregistered" assert ( @@ -79,11 +77,11 @@ def test_models_xapi_lms_unregistered_course_with_valid_statement(statement): ) -@custom_given(LMSAccessedPage) -def test_models_xapi_lms_accessed_page_with_valid_statement(statement): +def test_models_xapi_lms_accessed_page_with_valid_statement(): """Test that a valid accessed a page statement has the expected `verb`.`id` and `object`.`definition`.`type` property values. """ + statement = mock_xapi_instance(LMSAccessedPage) assert statement.verb.id == "https://w3id.org/xapi/netc/verbs/accessed" assert ( @@ -92,41 +90,41 @@ def test_models_xapi_lms_accessed_page_with_valid_statement(statement): ) -@custom_given(LMSAccessedFile) -def test_models_xapi_lms_accessed_file_with_valid_statement(statement): +def test_models_xapi_lms_accessed_file_with_valid_statement(): """Test that a valid accessed a file statement has the expected `verb`.`id` and `object`.`definition`.`type` property values. """ + statement = mock_xapi_instance(LMSAccessedFile) assert statement.verb.id == "https://w3id.org/xapi/netc/verbs/accessed" assert statement.object.definition.type == "http://activitystrea.ms/file" -@custom_given(LMSUploadedFile) -def test_models_xapi_lms_uploaded_file_with_valid_statement(statement): +def test_models_xapi_lms_uploaded_file_with_valid_statement(): """Test that a valid uploaded a file statement has the expected `verb`.`id` and `object`.`definition`.`type` property values. """ + statement = mock_xapi_instance(LMSUploadedFile) assert statement.verb.id == "https://w3id.org/xapi/netc/verbs/uploaded" assert statement.object.definition.type == "http://activitystrea.ms/file" -@custom_given(LMSDownloadedFile) -def test_models_xapi_lms_downloaded_file_with_valid_statement(statement): +def test_models_xapi_lms_downloaded_file_with_valid_statement(): """Test that a valid downloaded a file statement has the expected `verb`.`id` and `object`.`definition`.`type` property values. """ + statement = mock_xapi_instance(LMSDownloadedFile) assert statement.verb.id == "http://id.tincanapi.com/verb/downloaded" assert statement.object.definition.type == "http://activitystrea.ms/file" -@custom_given(LMSDownloadedVideo) -def test_models_xapi_lms_downloaded_video_with_valid_statement(statement): +def test_models_xapi_lms_downloaded_video_with_valid_statement(): """Test that a valid downloaded a video statement has the expected `verb`.`id` and `object`.`definition`.`type` property values. """ + statement = mock_xapi_instance(LMSDownloadedVideo) assert statement.verb.id == "http://id.tincanapi.com/verb/downloaded" assert ( @@ -135,11 +133,11 @@ def test_models_xapi_lms_downloaded_video_with_valid_statement(statement): ) -@custom_given(LMSUploadedVideo) -def test_models_xapi_lms_uploaded_video_with_valid_statement(statement): +def test_models_xapi_lms_uploaded_video_with_valid_statement(): """Test that a valid uploaded a video statement has the expected `verb`.`id` and `object`.`definition`.`type` property values. """ + statement = mock_xapi_instance(LMSUploadedVideo) assert statement.verb.id == "https://w3id.org/xapi/netc/verbs/uploaded" assert ( @@ -148,11 +146,11 @@ def test_models_xapi_lms_uploaded_video_with_valid_statement(statement): ) -@custom_given(LMSDownloadedDocument) -def test_models_xapi_lms_downloaded_document_with_valid_statement(statement): +def test_models_xapi_lms_downloaded_document_with_valid_statement(): """Test that a valid downloaded a document statement has the expected `verb`.`id` and `object`.`definition`.`type` property values. """ + statement = mock_xapi_instance(LMSDownloadedDocument) assert statement.verb.id == "http://id.tincanapi.com/verb/downloaded" assert ( @@ -161,11 +159,11 @@ def test_models_xapi_lms_downloaded_document_with_valid_statement(statement): ) -@custom_given(LMSUploadedDocument) -def test_models_xapi_lms_uploaded_document_with_valid_statement(statement): +def test_models_xapi_lms_uploaded_document_with_valid_statement(): """Test that a valid uploaded a document statement has the expected `verb`.`id` and `object`.`definition`.`type` property values. """ + statement = mock_xapi_instance(LMSUploadedDocument) assert statement.verb.id == "https://w3id.org/xapi/netc/verbs/uploaded" assert ( @@ -174,11 +172,11 @@ def test_models_xapi_lms_uploaded_document_with_valid_statement(statement): ) -@custom_given(LMSDownloadedAudio) -def test_models_xapi_lms_downloaded_audio_with_valid_statement(statement): +def test_models_xapi_lms_downloaded_audio_with_valid_statement(): """Test that a valid downloaded an audio statement has the expected `verb`.`id` and `object`.`definition`.`type` property values. """ + statement = mock_xapi_instance(LMSDownloadedAudio) assert statement.verb.id == "http://id.tincanapi.com/verb/downloaded" assert ( @@ -187,11 +185,11 @@ def test_models_xapi_lms_downloaded_audio_with_valid_statement(statement): ) -@custom_given(LMSUploadedAudio) -def test_models_xapi_lms_uploaded_audio_with_valid_statement(statement): +def test_models_xapi_lms_uploaded_audio_with_valid_statement(): """Test that a valid uploaded an audio statement has the expected `verb`.`id` and `object`.`definition`.`type` property values. """ + statement = mock_xapi_instance(LMSUploadedAudio) assert statement.verb.id == "https://w3id.org/xapi/netc/verbs/uploaded" assert ( @@ -200,7 +198,6 @@ def test_models_xapi_lms_uploaded_audio_with_valid_statement(statement): ) -@settings(deadline=None) @pytest.mark.parametrize( "category", [ @@ -213,13 +210,13 @@ def test_models_xapi_lms_uploaded_audio_with_valid_statement(statement): [{"id": "https://foo.bar"}, {"id": "https://w3id.org/xapi/lms"}], ], ) -@custom_given(LMSContextContextActivities) def test_models_xapi_lms_context_context_activities_with_valid_category( - category, context_activities + category ): """Test that a valid `LMSContextContextActivities` should not raise a `ValidationError`. """ + context_activities = mock_xapi_instance(LMSContextContextActivities) activities = json.loads(context_activities.json(exclude_none=True, by_alias=True)) activities["category"] = category try: @@ -230,7 +227,6 @@ def test_models_xapi_lms_context_context_activities_with_valid_category( ) -@settings(deadline=None) @pytest.mark.parametrize( "category", [ @@ -243,13 +239,13 @@ def test_models_xapi_lms_context_context_activities_with_valid_category( [{"id": "https://foo.bar"}, {"id": "https://w3id.org/xapi/not-lms"}], ], ) -@custom_given(LMSContextContextActivities) def test_models_xapi_lms_context_context_activities_with_invalid_category( - category, context_activities + category ): """Test that an invalid `LMSContextContextActivities` should raise a `ValidationError`. """ + context_activities = mock_xapi_instance(LMSContextContextActivities) activities = json.loads(context_activities.json(exclude_none=True, by_alias=True)) activities["category"] = category msg = ( diff --git a/tests/models/xapi/test_navigation.py b/tests/models/xapi/test_navigation.py index 2a0293a81..e31698457 100644 --- a/tests/models/xapi/test_navigation.py +++ b/tests/models/xapi/test_navigation.py @@ -9,34 +9,32 @@ from ralph.models.selector import ModelSelector from ralph.models.xapi.navigation.statements import PageTerminated, PageViewed -from tests.fixtures.hypothesis_strategies import custom_builds, custom_given +# from tests.fixtures.hypothesis_strategies import custom_builds, custom_given +from tests.factories import mock_xapi_instance - -@settings(deadline=None) @pytest.mark.parametrize("class_", [PageTerminated, PageViewed]) -@custom_given(st.data()) -def test_models_xapi_navigation_selectors_with_valid_statements(class_, data): +def test_models_xapi_navigation_selectors_with_valid_statements(class_): """Test given a valid navigation xAPI statement the `get_first_model` selector method should return the expected model. """ - statement = json.loads(data.draw(custom_builds(class_)).json()) + statement = json.loads(mock_xapi_instance(class_).json()) model = ModelSelector(module="ralph.models.xapi").get_first_model(statement) assert model is class_ -@custom_given(PageTerminated) -def test_models_xapi_navigation_page_terminated_with_valid_statement(statement): +def test_models_xapi_navigation_page_terminated_with_valid_statement(): """Test that a valid page_terminated statement has the expected `verb`.`id` and `object`.`definition`.`type` property values. """ + statement = mock_xapi_instance(PageTerminated) assert statement.verb.id == "http://adlnet.gov/expapi/verbs/terminated" assert statement.object.definition.type == "http://activitystrea.ms/schema/1.0/page" -@custom_given(PageViewed) -def test_models_xapi_page_viewed_with_valid_statement(statement): +def test_models_xapi_page_viewed_with_valid_statement(): """Test that a valid page_viewed statement has the expected `verb`.`id` and `object`.`definition`.`type` property values. """ + statement = mock_xapi_instance(PageViewed) assert statement.verb.id == "http://id.tincanapi.com/verb/viewed" assert statement.object.definition.type == "http://activitystrea.ms/schema/1.0/page" diff --git a/tests/models/xapi/test_video.py b/tests/models/xapi/test_video.py index 07fdfefed..44951399a 100644 --- a/tests/models/xapi/test_video.py +++ b/tests/models/xapi/test_video.py @@ -22,9 +22,9 @@ VideoVolumeChangeInteraction, ) -from tests.fixtures.hypothesis_strategies import custom_builds, custom_given +# from tests.fixtures.hypothesis_strategies import custom_builds, custom_given +from tests.factories import mock_xapi_instance -from tests.factories import mock_instance @pytest.mark.parametrize( "class_", @@ -41,12 +41,11 @@ def test_models_xapi_video_selectors_with_valid_statements(class_): """Test given a valid video xAPI statement the `get_first_model` selector method should return the expected model. """ - statement = json.loads(mock_instance(class_).json()) + statement = json.loads(mock_xapi_instance(class_).json()) model = ModelSelector(module="ralph.models.xapi").get_first_model(statement) assert model is class_ -@settings(deadline=None) @pytest.mark.parametrize( "class_", [ @@ -55,14 +54,13 @@ def test_models_xapi_video_selectors_with_valid_statements(class_): VideoScreenChangeInteraction, ], ) -@custom_given(st.data()) -def test_models_xapi_video_interaction_validator_with_valid_statements(class_, data): +def test_models_xapi_video_interaction_validator_with_valid_statements(class_): """Test given a valid video interaction xAPI statement the `get_first_valid_model` validator method should return the expected model. """ statement = json.loads( - data.draw(custom_builds(class_)).json(exclude_none=True, by_alias=True) + mock_xapi_instance(class_).json(exclude_none=True, by_alias=True) ) model = Validator(ModelSelector(module="ralph.models.xapi")).get_first_valid_model( @@ -72,11 +70,11 @@ def test_models_xapi_video_interaction_validator_with_valid_statements(class_, d assert isinstance(model, class_) -@custom_given(VideoInitialized) -def test_models_xapi_video_initialized_with_valid_statement(statement): +def test_models_xapi_video_initialized_with_valid_statement(): """Test that a valid video initialized statement has the expected `verb`.`id` and `object`.`definition`.`type` property values. """ + statement = mock_xapi_instance(VideoInitialized) assert statement.verb.id == "http://adlnet.gov/expapi/verbs/initialized" assert ( @@ -85,11 +83,11 @@ def test_models_xapi_video_initialized_with_valid_statement(statement): ) -@custom_given(VideoPlayed) -def test_models_xapi_video_played_with_valid_statement(statement): +def test_models_xapi_video_played_with_valid_statement(): """Test that a valid video played statement has the expected `verb`.`id` and `object`.`definition`.`type` property values. """ + statement = mock_xapi_instance(VideoPlayed) assert statement.verb.id == "https://w3id.org/xapi/video/verbs/played" assert ( @@ -98,11 +96,11 @@ def test_models_xapi_video_played_with_valid_statement(statement): ) -@custom_given(VideoPaused) -def test_models_xapi_video_paused_with_valid_statement(statement): +def test_models_xapi_video_paused_with_valid_statement(): """Test that a video paused statement has the expected `verb`.`id` and `object`.`definition`.`type` property values. """ + statement = mock_xapi_instance(VideoPaused) assert statement.verb.id == "https://w3id.org/xapi/video/verbs/paused" assert ( @@ -111,11 +109,11 @@ def test_models_xapi_video_paused_with_valid_statement(statement): ) -@custom_given(VideoSeeked) -def test_models_xapi_video_seeked_with_valid_statement(statement): +def test_models_xapi_video_seeked_with_valid_statement(): """Test that a video seeked statement has the expected `verb`.`id` and `object`.`definition`.`type` property values. """ + statement = mock_xapi_instance(VideoSeeked) assert statement.verb.id == "https://w3id.org/xapi/video/verbs/seeked" assert ( @@ -124,11 +122,11 @@ def test_models_xapi_video_seeked_with_valid_statement(statement): ) -@custom_given(VideoCompleted) -def test_models_xapi_video_completed_with_valid_statement(statement): +def test_models_xapi_video_completed_with_valid_statement(): """Test that a video completed statement has the expected `verb`.`id` and `object`.`definition`.`type` property values. """ + statement = mock_xapi_instance(VideoCompleted) assert statement.verb.id == "http://adlnet.gov/expapi/verbs/completed" assert ( @@ -137,11 +135,11 @@ def test_models_xapi_video_completed_with_valid_statement(statement): ) -@custom_given(VideoTerminated) -def test_models_xapi_video_terminated_with_valid_statement(statement): +def test_models_xapi_video_terminated_with_valid_statement(): """Test that a video terminated statement has the expected `verb`.`id` and `object`.`definition`.`type` property values. """ + statement = mock_xapi_instance(VideoTerminated) assert statement.verb.id == "http://adlnet.gov/expapi/verbs/terminated" assert ( @@ -150,11 +148,11 @@ def test_models_xapi_video_terminated_with_valid_statement(statement): ) -@custom_given(VideoEnableClosedCaptioning) -def test_models_xapi_video_enable_closed_captioning_with_valid_statement(statement): +def test_models_xapi_video_enable_closed_captioning_with_valid_statement(): """Test that a video enable closed captioning statement has the expected `verb`.`id` and `object`.`definition`.`type` property values. """ + statement = mock_xapi_instance(VideoEnableClosedCaptioning) assert statement.verb.id == "http://adlnet.gov/expapi/verbs/interacted" assert ( @@ -163,11 +161,11 @@ def test_models_xapi_video_enable_closed_captioning_with_valid_statement(stateme ) -@custom_given(VideoVolumeChangeInteraction) -def test_models_xapi_video_volume_change_interaction_with_valid_statement(statement): +def test_models_xapi_video_volume_change_interaction_with_valid_statement(): """Test that a video volume change interaction statement has the expected `verb`.`id` and `object`.`definition`.`type` property values. """ + statement = mock_xapi_instance(VideoVolumeChangeInteraction) assert statement.verb.id == "http://adlnet.gov/expapi/verbs/interacted" assert ( @@ -176,11 +174,11 @@ def test_models_xapi_video_volume_change_interaction_with_valid_statement(statem ) -@custom_given(VideoScreenChangeInteraction) -def test_models_xapi_video_screen_change_interaction_with_valid_statement(statement): +def test_models_xapi_video_screen_change_interaction_with_valid_statement(): """Test that a video screen change interaction statement has the expected `verb`.`id` and `object`.`definition`.`type` property values. """ + statement = mock_xapi_instance(VideoScreenChangeInteraction) assert statement.verb.id == "http://adlnet.gov/expapi/verbs/interacted" assert ( @@ -189,7 +187,6 @@ def test_models_xapi_video_screen_change_interaction_with_valid_statement(statem ) -@settings(deadline=None) @pytest.mark.parametrize( "category", [ @@ -202,13 +199,13 @@ def test_models_xapi_video_screen_change_interaction_with_valid_statement(statem [{"id": "https://foo.bar"}, {"id": "https://w3id.org/xapi/video"}], ], ) -@custom_given(VideoContextContextActivities) def test_models_xapi_video_context_activities_with_valid_category( - category, context_activities + category ): """Test that a valid `VideoContextContextActivities` should not raise a `ValidationError`. """ + context_activities = mock_xapi_instance(VideoContextContextActivities) activities = json.loads(context_activities.json(exclude_none=True, by_alias=True)) activities["category"] = category try: @@ -219,7 +216,6 @@ def test_models_xapi_video_context_activities_with_valid_category( ) -@settings(deadline=None) @pytest.mark.parametrize( "category", [ @@ -232,13 +228,13 @@ def test_models_xapi_video_context_activities_with_valid_category( [{"id": "https://foo.bar"}, {"id": "https://w3id.org/xapi/not-video"}], ], ) -@custom_given(VideoContextContextActivities) def test_models_xapi_video_context_activities_with_invalid_category( - category, context_activities + category ): """Test that an invalid `VideoContextContextActivities` should raise a `ValidationError`. """ + context_activities = mock_xapi_instance(VideoContextContextActivities) activities = json.loads(context_activities.json(exclude_none=True, by_alias=True)) activities["category"] = category msg = ( diff --git a/tests/models/xapi/test_virtual_classroom.py b/tests/models/xapi/test_virtual_classroom.py index 8f5108b06..1665a1c0f 100644 --- a/tests/models/xapi/test_virtual_classroom.py +++ b/tests/models/xapi/test_virtual_classroom.py @@ -29,10 +29,9 @@ VirtualClassroomUnsharedScreen, ) -from tests.fixtures.hypothesis_strategies import custom_builds, custom_given -from tests.factories import mock_instance +# from tests.fixtures.hypothesis_strategies import custom_builds, custom_given +from tests.factories import mock_xapi_instance -@settings(deadline=None) @pytest.mark.parametrize( "class_", [ @@ -53,21 +52,20 @@ VirtualClassroomStoppedCamera, ], ) -@custom_given(st.data()) -def test_models_xapi_virtual_classroom_selectors_with_valid_statements(class_, data): +def test_models_xapi_virtual_classroom_selectors_with_valid_statements(class_): """Test given a valid virtual classroom xAPI statement the `get_first_model` selector method should return the expected model. """ - statement = json.loads(mock_instance(class_).json()) + statement = json.loads(mock_xapi_instance(class_).json()) model = ModelSelector(module="ralph.models.xapi").get_first_model(statement) assert model is class_ -@custom_given(VirtualClassroomInitialized) -def test_models_xapi_virtual_classroom_initialized_with_valid_statement(statement): +def test_models_xapi_virtual_classroom_initialized_with_valid_statement(): """Test that a valid virtual classroom initialized statement has the expected `verb`.`id` and `object`.`definition`.`type` property values. """ + statement = mock_xapi_instance(VirtualClassroomInitialized) assert statement.verb.id == "http://adlnet.gov/expapi/verbs/initialized" assert ( statement.object.definition.type @@ -75,11 +73,11 @@ def test_models_xapi_virtual_classroom_initialized_with_valid_statement(statemen ) -@custom_given(VirtualClassroomJoined) -def test_models_xapi_virtual_classroom_joined_with_valid_statement(statement): +def test_models_xapi_virtual_classroom_joined_with_valid_statement(): """Test that a virtual classroom joined statement has the expected `verb`.`id` and `object`.`definition`.`type` property values. """ + statement = mock_xapi_instance(VirtualClassroomJoined) assert statement.verb.id == "http://activitystrea.ms/join" assert ( statement.object.definition.type @@ -87,11 +85,11 @@ def test_models_xapi_virtual_classroom_joined_with_valid_statement(statement): ) -@custom_given(VirtualClassroomLeft) -def test_models_xapi_virtual_classroom_left_with_valid_statement(statement): +def test_models_xapi_virtual_classroom_left_with_valid_statement(): """Test that a virtual classroom left statement has the expected `verb`.`id` and `object`.`definition`.`type` property values. """ + statement = mock_xapi_instance(VirtualClassroomLeft) assert statement.verb.id == "http://activitystrea.ms/leave" assert ( statement.object.definition.type @@ -99,11 +97,11 @@ def test_models_xapi_virtual_classroom_left_with_valid_statement(statement): ) -@custom_given(VirtualClassroomTerminated) -def test_models_xapi_virtual_classroom_terminated_with_valid_statement(statement): +def test_models_xapi_virtual_classroom_terminated_with_valid_statement(): """Test that a virtual classroom terminated statement has the expected `verb`.`id` and `object`.`definition`.`type` property values. """ + statement = mock_xapi_instance(VirtualClassroomTerminated) assert statement.verb.id == "http://adlnet.gov/expapi/verbs/terminated" assert ( statement.object.definition.type @@ -111,11 +109,11 @@ def test_models_xapi_virtual_classroom_terminated_with_valid_statement(statement ) -@custom_given(VirtualClassroomMuted) -def test_models_xapi_virtual_classroom_muted_with_valid_statement(statement): +def test_models_xapi_virtual_classroom_muted_with_valid_statement(): """Test that a virtual classroom muted statement has the expected `verb`.`id` and `object`.`definition`.`type` property values. """ + statement = mock_xapi_instance(VirtualClassroomMuted) assert statement.verb.id == "https://w3id.org/xapi/virtual-classroom/verbs/muted" assert ( statement.object.definition.type @@ -123,11 +121,11 @@ def test_models_xapi_virtual_classroom_muted_with_valid_statement(statement): ) -@custom_given(VirtualClassroomUnmuted) -def test_models_xapi_virtual_classroom_unmuted_with_valid_statement(statement): +def test_models_xapi_virtual_classroom_unmuted_with_valid_statement(): """Test that a virtual classroom unmuted statement has the expected `verb`.`id` and `object`.`definition`.`type` property values. """ + statement = mock_xapi_instance(VirtualClassroomUnmuted) assert statement.verb.id == "https://w3id.org/xapi/virtual-classroom/verbs/unmuted" assert ( statement.object.definition.type @@ -135,11 +133,11 @@ def test_models_xapi_virtual_classroom_unmuted_with_valid_statement(statement): ) -@custom_given(VirtualClassroomSharedScreen) -def test_models_xapi_virtual_classroom_shared_screen_with_valid_statement(statement): +def test_models_xapi_virtual_classroom_shared_screen_with_valid_statement(): """Test that a virtual classroom shared screen statement has the expected `verb`.`id` and `object`.`definition`.`type` property values. """ + statement = mock_xapi_instance(VirtualClassroomSharedScreen) assert ( statement.verb.id == "https://w3id.org/xapi/virtual-classroom/verbs/shared-screen" @@ -150,11 +148,11 @@ def test_models_xapi_virtual_classroom_shared_screen_with_valid_statement(statem ) -@custom_given(VirtualClassroomUnsharedScreen) -def test_models_xapi_virtual_classroom_unshared_screen_with_valid_statement(statement): +def test_models_xapi_virtual_classroom_unshared_screen_with_valid_statement(): """Test that a virtual classroom unshared screen statement has the expected `verb`.`id` and `object`.`definition`.`type` property values. """ + statement = mock_xapi_instance(VirtualClassroomUnsharedScreen) assert ( statement.verb.id == "https://w3id.org/xapi/virtual-classroom/verbs/unshared-screen" @@ -165,11 +163,11 @@ def test_models_xapi_virtual_classroom_unshared_screen_with_valid_statement(stat ) -@custom_given(VirtualClassroomStartedCamera) -def test_models_xapi_virtual_classroom_started_camera_with_valid_statement(statement): +def test_models_xapi_virtual_classroom_started_camera_with_valid_statement(): """Test that a virtual classroom started camera statement has the expected `verb`.`id` and `object`.`definition`.`type` property values. """ + statement = mock_xapi_instance(VirtualClassroomStartedCamera) assert ( statement.verb.id == "https://w3id.org/xapi/virtual-classroom/verbs/started-camera" @@ -180,11 +178,11 @@ def test_models_xapi_virtual_classroom_started_camera_with_valid_statement(state ) -@custom_given(VirtualClassroomStoppedCamera) -def test_models_xapi_virtual_classroom_stopped_camera_with_valid_statement(statement): +def test_models_xapi_virtual_classroom_stopped_camera_with_valid_statement(): """Test that a virtual classroom stopped camera statement has the expected `verb`.`id` and `object`.`definition`.`type` property values. """ + statement = mock_xapi_instance(VirtualClassroomStoppedCamera) assert ( statement.verb.id == "https://w3id.org/xapi/virtual-classroom/verbs/stopped-camera" @@ -195,11 +193,11 @@ def test_models_xapi_virtual_classroom_stopped_camera_with_valid_statement(state ) -@custom_given(VirtualClassroomRaisedHand) -def test_models_xapi_virtual_classroom_raised_hand_with_valid_statement(statement): +def test_models_xapi_virtual_classroom_raised_hand_with_valid_statement(): """Test that a virtual classroom raised hand statement has the expected `verb`.`id` and `object`.`definition`.`type` property values. """ + statement = mock_xapi_instance(VirtualClassroomRaisedHand) assert ( statement.verb.id == "https://w3id.org/xapi/virtual-classroom/verbs/raised-hand" ) @@ -209,11 +207,11 @@ def test_models_xapi_virtual_classroom_raised_hand_with_valid_statement(statemen ) -@custom_given(VirtualClassroomLoweredHand) -def test_models_xapi_virtual_classroom_lowered_hand_with_valid_statement(statement): +def test_models_xapi_virtual_classroom_lowered_hand_with_valid_statement(): """Test that a virtual classroom lowered hand statement has the expected `verb`.`id` and `object`.`definition`.`type` property values. """ + statement = mock_xapi_instance(VirtualClassroomLoweredHand) assert ( statement.verb.id == "https://w3id.org/xapi/virtual-classroom/verbs/lowered-hand" @@ -224,11 +222,11 @@ def test_models_xapi_virtual_classroom_lowered_hand_with_valid_statement(stateme ) -@custom_given(VirtualClassroomStartedPoll) -def test_models_xapi_virtual_classroom_started_poll_with_valid_statement(statement): +def test_models_xapi_virtual_classroom_started_poll_with_valid_statement(): """Test that a virtual classroom started poll statement has the expected `verb`.`id` and `object`.`definition`.`type` property values. """ + statement = mock_xapi_instance(VirtualClassroomStartedPoll) assert statement.verb.id == "http://adlnet.gov/expapi/verbs/asked" assert ( statement.object.definition.type @@ -236,11 +234,11 @@ def test_models_xapi_virtual_classroom_started_poll_with_valid_statement(stateme ) -@custom_given(VirtualClassroomAnsweredPoll) -def test_models_xapi_virtual_classroom_answered_poll_with_valid_statement(statement): +def test_models_xapi_virtual_classroom_answered_poll_with_valid_statement(): """Test that a virtual classroom answered poll statement has the expected `verb`.`id` and `object`.`definition`.`type` property values. """ + statement = mock_xapi_instance(VirtualClassroomAnsweredPoll) assert statement.verb.id == "http://adlnet.gov/expapi/verbs/answered" assert ( statement.object.definition.type @@ -248,10 +246,10 @@ def test_models_xapi_virtual_classroom_answered_poll_with_valid_statement(statem ) -@custom_given(VirtualClassroomPostedPublicMessage) def test_models_xapi_virtual_classroom_posted_public_message_with_valid_statement( - statement, + ): + statement = mock_xapi_instance(VirtualClassroomPostedPublicMessage) """Test that a virtual classroom posted public message statement has the expected `verb`.`id` and `object`.`definition`.`type` property values. """ @@ -262,7 +260,6 @@ def test_models_xapi_virtual_classroom_posted_public_message_with_valid_statemen ) -@settings(deadline=None) @pytest.mark.parametrize( "category", [ @@ -275,13 +272,13 @@ def test_models_xapi_virtual_classroom_posted_public_message_with_valid_statemen [{"id": "https://foo.bar"}, {"id": "https://w3id.org/xapi/virtual-classroom"}], ], ) -@custom_given(VirtualClassroomContextContextActivities) def test_models_xapi_virtual_classroom_context_activities_with_valid_category( - category, context_activities + category ): """Test that a valid `VirtualClassroomContextContextActivities` should not raise a `ValidationError`. """ + context_activities = mock_xapi_instance(VirtualClassroomContextContextActivities) activities = json.loads(context_activities.json(exclude_none=True, by_alias=True)) activities["category"] = category try: @@ -293,7 +290,6 @@ def test_models_xapi_virtual_classroom_context_activities_with_valid_category( ) -@settings(deadline=None) @pytest.mark.parametrize( "category", [ @@ -309,13 +305,13 @@ def test_models_xapi_virtual_classroom_context_activities_with_valid_category( ], ], ) -@custom_given(VirtualClassroomContextContextActivities) def test_models_xapi_virtual_classroom_context_activities_with_invalid_category( - category, context_activities + category ): """Test that an invalid `VirtualClassroomContextContextActivities` should raise a `ValidationError`. """ + context_activities = mock_xapi_instance(VirtualClassroomContextContextActivities) activities = json.loads(context_activities.json(exclude_none=True, by_alias=True)) activities["category"] = category msg = ( From 4f9a69b08c836ab9fb9f080fc006958f11431250 Mon Sep 17 00:00:00 2001 From: lleeoo Date: Wed, 13 Dec 2023 12:14:32 +0100 Subject: [PATCH 16/19] lint wip --- src/ralph/api/models.py | 1 - src/ralph/conf.py | 5 +- .../edx/problem_interaction/fields/events.py | 101 +++++++++++------- src/ralph/models/edx/video/fields/events.py | 3 +- src/ralph/models/xapi/base/agents.py | 3 +- src/ralph/models/xapi/base/common.py | 12 +-- src/ralph/models/xapi/base/contexts.py | 4 +- src/ralph/models/xapi/base/ifi.py | 4 +- src/ralph/models/xapi/base/results.py | 4 +- src/ralph/models/xapi/base/statements.py | 5 +- .../models/xapi/base/unnested_objects.py | 6 +- .../models/xapi/virtual_classroom/contexts.py | 1 - .../models/xapi/virtual_classroom/results.py | 4 +- tests/api/test_forwarding.py | 21 ++-- tests/factories.py | 95 ++++++++-------- tests/fixtures/hypothesis_configuration.py | 3 +- tests/fixtures/hypothesis_strategies.py | 8 +- .../edx/converters/xapi/test_enrollment.py | 6 +- .../edx/converters/xapi/test_navigational.py | 6 +- .../models/edx/converters/xapi/test_server.py | 10 +- .../models/edx/converters/xapi/test_video.py | 20 ++-- tests/models/edx/navigational/test_events.py | 1 + .../edx/navigational/test_statements.py | 2 +- .../open_response_assessment/test_events.py | 6 +- .../test_statements.py | 6 +- .../edx/peer_instruction/test_events.py | 1 + .../edx/peer_instruction/test_statements.py | 13 +-- .../edx/problem_interaction/test_events.py | 47 +++----- .../problem_interaction/test_statements.py | 9 +- tests/models/edx/test_browser.py | 4 +- tests/models/edx/test_enrollment.py | 11 +- tests/models/edx/test_server.py | 1 + .../edx/textbook_interaction/test_events.py | 11 +- .../textbook_interaction/test_statements.py | 33 ++---- tests/models/edx/video/test_events.py | 5 +- tests/models/edx/video/test_statements.py | 33 ++---- tests/models/test_converter.py | 3 +- tests/models/test_validator.py | 8 +- tests/models/xapi/base/test_agents.py | 7 +- tests/models/xapi/base/test_groups.py | 4 +- tests/models/xapi/base/test_objects.py | 1 + tests/models/xapi/base/test_results.py | 1 + tests/models/xapi/base/test_statements.py | 33 ++++-- .../models/xapi/base/test_unnested_objects.py | 10 +- .../xapi/concepts/test_activity_types.py | 7 +- tests/models/xapi/concepts/test_verbs.py | 3 +- tests/models/xapi/test_lms.py | 11 +- tests/models/xapi/test_navigation.py | 3 +- tests/models/xapi/test_video.py | 10 +- tests/models/xapi/test_virtual_classroom.py | 13 +-- tests/test_cli.py | 4 +- 51 files changed, 283 insertions(+), 340 deletions(-) diff --git a/src/ralph/api/models.py b/src/ralph/api/models.py index c60f77e08..94a8ee3d1 100644 --- a/src/ralph/api/models.py +++ b/src/ralph/api/models.py @@ -4,7 +4,6 @@ validation. """ from typing import Optional, Union - from uuid import UUID from pydantic import AnyUrl, BaseModel, Extra diff --git a/src/ralph/conf.py b/src/ralph/conf.py index ed64dc0ad..61e3f0307 100644 --- a/src/ralph/conf.py +++ b/src/ralph/conf.py @@ -11,11 +11,10 @@ AnyUrl, BaseModel, BaseSettings, - constr, Extra, Field, + constr, root_validator, - StrictStr, ) from ralph.exceptions import ConfigurationException @@ -140,7 +139,7 @@ class ParserSettings(BaseModel): class XapiForwardingConfigurationSettings(BaseModel): """Pydantic model for xAPI forwarding configuration item.""" - # class Config: # noqa: D106 # TODO: done + # class Config: # TODO: done # min_anystr_length = 1 url: AnyUrl diff --git a/src/ralph/models/edx/problem_interaction/fields/events.py b/src/ralph/models/edx/problem_interaction/fields/events.py index a5d32530d..54d2abf49 100644 --- a/src/ralph/models/edx/problem_interaction/fields/events.py +++ b/src/ralph/models/edx/problem_interaction/fields/events.py @@ -4,7 +4,7 @@ from datetime import datetime from typing import Annotated, Dict, List, Optional, Union -from pydantic import constr, Field +from pydantic import Field from ...base import AbstractBaseEventField, BaseModelWithConfig @@ -180,10 +180,13 @@ class ProblemCheckEventField(AbstractBaseEventField): ] grade: int max_grade: int - problem_id: Annotated[str, Field( - regex=r"^block-v1:[^\/+]+(\/|\+)[^\/+]+(\/|\+)[^\/?]+" - r"type@problem\+block@[a-f0-9]{32}$" - )] + problem_id: Annotated[ + str, + Field( + regex=r"^block-v1:[^\/+]+(\/|\+)[^\/+]+(\/|\+)[^\/?]+" + r"type@problem\+block@[a-f0-9]{32}$" + ), + ] state: State submission: Dict[ Annotated[str, Field(regex=r"^[a-f0-9]{32}_[0-9]_[0-9]$")], @@ -208,10 +211,13 @@ class ProblemCheckFailEventField(AbstractBaseEventField): Union[List[str], str], ] failure: Union[Literal["closed"], Literal["unreset"]] - problem_id: Annotated[str, Field( - regex=r"^block-v1:[^\/+]+(\/|\+)[^\/+]+(\/|\+)[^\/?]+" - r"type@problem\+block@[a-f0-9]{32}$" - )] + problem_id: Annotated[ + str, + Field( + regex=r"^block-v1:[^\/+]+(\/|\+)[^\/+]+(\/|\+)[^\/?]+" + r"type@problem\+block@[a-f0-9]{32}$" + ), + ] state: State @@ -235,10 +241,13 @@ class ProblemRescoreEventField(AbstractBaseEventField): new_total: int orig_score: int orig_total: int - problem_id: Annotated[str, Field( - regex=r"^block-v1:[^\/+]+(\/|\+)[^\/+]+(\/|\+)[^\/?]+" - r"type@problem\+block@[a-f0-9]{32}$" - )] + problem_id: Annotated[ + str, + Field( + regex=r"^block-v1:[^\/+]+(\/|\+)[^\/+]+(\/|\+)[^\/?]+" + r"type@problem\+block@[a-f0-9]{32}$" + ), + ] state: State success: Union[Literal["correct"], Literal["incorrect"]] @@ -253,10 +262,13 @@ class ProblemRescoreFailEventField(AbstractBaseEventField): """ failure: Union[Literal["closed"], Literal["unreset"]] - problem_id: Annotated[str, Field( - regex=r"^block-v1:[^\/+]+(\/|\+)[^\/+]+(\/|\+)[^\/?]+" - r"type@problem\+block@[a-f0-9]{32}$" - )] + problem_id: Annotated[ + str, + Field( + regex=r"^block-v1:[^\/+]+(\/|\+)[^\/+]+(\/|\+)[^\/?]+" + r"type@problem\+block@[a-f0-9]{32}$" + ), + ] state: State @@ -293,10 +305,13 @@ class ResetProblemEventField(AbstractBaseEventField): new_state: State old_state: State - problem_id: Annotated[str, Field( - regex=r"^block-v1:[^\/+]+(\/|\+)[^\/+]+(\/|\+)[^\/?]+" - r"type@problem\+block@[a-f0-9]{32}$" - )] + problem_id: Annotated[ + str, + Field( + regex=r"^block-v1:[^\/+]+(\/|\+)[^\/+]+(\/|\+)[^\/?]+" + r"type@problem\+block@[a-f0-9]{32}$" + ), + ] class ResetProblemFailEventField(AbstractBaseEventField): @@ -310,10 +325,13 @@ class ResetProblemFailEventField(AbstractBaseEventField): failure: Union[Literal["closed"], Literal["not_done"]] old_state: State - problem_id: Annotated[str, Field( - regex=r"^block-v1:[^\/+]+(\/|\+)[^\/+]+(\/|\+)[^\/?]+" - r"type@problem\+block@[a-f0-9]{32}$" - )] + problem_id: Annotated[ + str, + Field( + regex=r"^block-v1:[^\/+]+(\/|\+)[^\/+]+(\/|\+)[^\/?]+" + r"type@problem\+block@[a-f0-9]{32}$" + ), + ] class SaveProblemFailEventField(AbstractBaseEventField): @@ -329,10 +347,13 @@ class SaveProblemFailEventField(AbstractBaseEventField): answers: Dict[str, Union[int, str, list, dict]] failure: Union[Literal["closed"], Literal["done"]] - problem_id: Annotated[str, Field( - regex=r"^block-v1:[^\/+]+(\/|\+)[^\/+]+(\/|\+)[^\/?]+" - r"type@problem\+block@[a-f0-9]{32}$" - )] + problem_id: Annotated[ + str, + Field( + regex=r"^block-v1:[^\/+]+(\/|\+)[^\/+]+(\/|\+)[^\/?]+" + r"type@problem\+block@[a-f0-9]{32}$" + ), + ] state: State @@ -347,10 +368,13 @@ class SaveProblemSuccessEventField(AbstractBaseEventField): """ answers: Dict[str, Union[int, str, list, dict]] - problem_id: Annotated[str, Field( - regex=r"^block-v1:[^\/+]+(\/|\+)[^\/+]+(\/|\+)[^\/?]+" - r"type@problem\+block@[a-f0-9]{32}$" - )] + problem_id: Annotated[ + str, + Field( + regex=r"^block-v1:[^\/+]+(\/|\+)[^\/+]+(\/|\+)[^\/?]+" + r"type@problem\+block@[a-f0-9]{32}$" + ), + ] state: State @@ -361,7 +385,10 @@ class ShowAnswerEventField(AbstractBaseEventField): problem_id (str): Consists of the ID of the problem being shown. """ - problem_id: Annotated[str, Field( - regex=r"^block-v1:[^\/+]+(\/|\+)[^\/+]+(\/|\+)[^\/?]+" - r"type@problem\+block@[a-f0-9]{32}$" - )] \ No newline at end of file + problem_id: Annotated[ + str, + Field( + regex=r"^block-v1:[^\/+]+(\/|\+)[^\/+]+(\/|\+)[^\/?]+" + r"type@problem\+block@[a-f0-9]{32}$" + ), + ] diff --git a/src/ralph/models/edx/video/fields/events.py b/src/ralph/models/edx/video/fields/events.py index 0829a8f2f..d8517c973 100644 --- a/src/ralph/models/edx/video/fields/events.py +++ b/src/ralph/models/edx/video/fields/events.py @@ -50,6 +50,7 @@ class PauseVideoEventField(VideoBaseEventField): currentTime: float + class SeekVideoEventField(VideoBaseEventField): """Pydantic model for `seek_video`.`event` field. @@ -62,7 +63,7 @@ class SeekVideoEventField(VideoBaseEventField): within the video, either `onCaptionSeek` or `onSlideSeek` value. """ - new_time: NonNegativeFloat # TODO: Ask Quitterie if this is valid + new_time: NonNegativeFloat # TODO: Ask Quitterie if this is valid old_time: NonNegativeFloat type: str diff --git a/src/ralph/models/xapi/base/agents.py b/src/ralph/models/xapi/base/agents.py index 139ef4d93..5d2945db9 100644 --- a/src/ralph/models/xapi/base/agents.py +++ b/src/ralph/models/xapi/base/agents.py @@ -4,8 +4,9 @@ from abc import ABC from typing import Optional, Union -from ralph.conf import NonEmptyStr, NonEmptyStrictStr +from ralph.conf import NonEmptyStrictStr from ralph.models.xapi.config import BaseModelWithConfig + from .common import IRI from .ifi import ( BaseXapiAccountIFI, diff --git a/src/ralph/models/xapi/base/common.py b/src/ralph/models/xapi/base/common.py index d3e2cd8e5..738d39a31 100644 --- a/src/ralph/models/xapi/base/common.py +++ b/src/ralph/models/xapi/base/common.py @@ -1,16 +1,13 @@ """Common for xAPI base definitions.""" -from typing import Dict, Generator, Type +from typing import Annotated, Dict, Generator, Type from langcodes import tag_is_valid -from pydantic import StrictStr, validate_email +from pydantic import Field from rfc3987 import parse from ralph.conf import NonEmptyStr, NonEmptyStrictStr, NonEmptyStrictStrPatch -from typing import Annotated -from pydantic import Field -from pydantic import BaseModel, root_validator class IRI(NonEmptyStrictStrPatch): """Pydantic custom data type validating RFC 3987 IRIs.""" @@ -24,6 +21,7 @@ def validate(iri: str) -> Type["IRI"]: yield validate + class LanguageTag(NonEmptyStr): """Pydantic custom data type validating RFC 5646 Language tags.""" @@ -38,7 +36,7 @@ def validate(tag: str) -> Type["LanguageTag"]: yield validate -LanguageMap = Dict[LanguageTag, NonEmptyStrictStr] # TODO: change back to strictstr +LanguageMap = Dict[LanguageTag, NonEmptyStrictStr] # TODO: change back to strictstr email_pattern = r"(^mailto:[-!#-'*+/-9=?A-Z^-~]+(\.[-!#-'*+/-9=?A-Z^-~]+)*|\"([]!#-[^-~ \t]|(\\[\t -~]))+\")@([-!#-'*+/-9=?A-Z^-~]+(\.[-!#-'*+/-9=?A-Z^-~]+)*|\[[\t -Z^-~]*])" @@ -48,7 +46,7 @@ def validate(tag: str) -> Type["LanguageTag"]: # """Pydantic custom data type validating `mailto:email` format.""" # @classmethod -# def __get_validators__(cls) -> Generator: # noqa: D105 +# def __get_validators__(cls) -> Generator: # def validate(mailto: str) -> Type["MailtoEmail"]: # """Check whether the provided value follows the `mailto:email` format.""" # if not mailto.startswith("mailto:"): diff --git a/src/ralph/models/xapi/base/contexts.py b/src/ralph/models/xapi/base/contexts.py index 6eb3d0fd6..0fb825a95 100644 --- a/src/ralph/models/xapi/base/contexts.py +++ b/src/ralph/models/xapi/base/contexts.py @@ -3,14 +3,14 @@ from typing import Dict, List, Optional, Union from uuid import UUID +from ralph.conf import NonEmptyStrictStr + from ..config import BaseModelWithConfig from .agents import BaseXapiAgent from .common import IRI, LanguageTag from .groups import BaseXapiGroup from .unnested_objects import BaseXapiActivity, BaseXapiStatementRef -from ralph.conf import NonEmptyStrictStr - class BaseXapiContextContextActivities(BaseModelWithConfig): """Pydantic model for context `contextActivities` property. diff --git a/src/ralph/models/xapi/base/ifi.py b/src/ralph/models/xapi/base/ifi.py index 1ba3c5a40..e7f326710 100644 --- a/src/ralph/models/xapi/base/ifi.py +++ b/src/ralph/models/xapi/base/ifi.py @@ -2,11 +2,11 @@ from pydantic import AnyUrl, constr +from ralph.conf import NonEmptyStrictStr + from ..config import BaseModelWithConfig from .common import IRI, MailtoEmail -from ralph.conf import NonEmptyStrictStr - class BaseXapiAccount(BaseModelWithConfig): """Pydantic model for IFI `account` property. diff --git a/src/ralph/models/xapi/base/results.py b/src/ralph/models/xapi/base/results.py index 908656bb2..42da00c48 100644 --- a/src/ralph/models/xapi/base/results.py +++ b/src/ralph/models/xapi/base/results.py @@ -6,11 +6,11 @@ from pydantic import StrictBool, conint, root_validator +from ralph.conf import NonEmptyStrictStr + from ..config import BaseModelWithConfig from .common import IRI -from ralph.conf import NonEmptyStrictStr - class BaseXapiResultScore(BaseModelWithConfig): """Pydantic model for result `score` property. diff --git a/src/ralph/models/xapi/base/statements.py b/src/ralph/models/xapi/base/statements.py index 4ba229065..a86b31134 100644 --- a/src/ralph/models/xapi/base/statements.py +++ b/src/ralph/models/xapi/base/statements.py @@ -4,7 +4,7 @@ from typing import Any, List, Optional, Union from uuid import UUID -from pydantic import constr, root_validator, BaseModel +from pydantic import constr, root_validator from ..config import BaseModelWithConfig from .agents import BaseXapiAgent @@ -16,9 +16,6 @@ from .verbs import BaseXapiVerb -from pprint import pprint # TODO: remove - - class BaseXapiStatement(BaseModelWithConfig): """Pydantic model for base xAPI statements. diff --git a/src/ralph/models/xapi/base/unnested_objects.py b/src/ralph/models/xapi/base/unnested_objects.py index 3b5f2c59d..be2d3401b 100644 --- a/src/ralph/models/xapi/base/unnested_objects.py +++ b/src/ralph/models/xapi/base/unnested_objects.py @@ -1,12 +1,12 @@ """Base xAPI `Object` definitions (1).""" import sys -from typing import Annotated, Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional, Union from uuid import UUID -from pydantic import AnyUrl, Field, StrictStr, constr, validator +from pydantic import AnyUrl, constr, validator -from ralph.conf import NonEmptyStr, NonEmptyStrictStr +from ralph.conf import NonEmptyStrictStr from ..config import BaseModelWithConfig from .common import IRI, LanguageMap diff --git a/src/ralph/models/xapi/virtual_classroom/contexts.py b/src/ralph/models/xapi/virtual_classroom/contexts.py index 0bde82170..e34fbf443 100644 --- a/src/ralph/models/xapi/virtual_classroom/contexts.py +++ b/src/ralph/models/xapi/virtual_classroom/contexts.py @@ -33,7 +33,6 @@ class VirtualClassroomProfileActivity(ProfileActivity): ] = "https://w3id.org/xapi/virtual-classroom" - class VirtualClassroomContextContextActivities(BaseXapiContextContextActivities): """Pydantic model for virtual classroom `context`.`contextActivities` property. diff --git a/src/ralph/models/xapi/virtual_classroom/results.py b/src/ralph/models/xapi/virtual_classroom/results.py index ee6d0eaee..d1a438bf7 100644 --- a/src/ralph/models/xapi/virtual_classroom/results.py +++ b/src/ralph/models/xapi/virtual_classroom/results.py @@ -1,9 +1,9 @@ """Virtual classroom xAPI events result fields definitions.""" -from ..base.results import BaseXapiResult - from ralph.conf import NonEmptyStrictStr +from ..base.results import BaseXapiResult + class VirtualClassroomAnsweredPollResult(BaseXapiResult): """Pydantic model for virtual classroom answered poll `result` property. diff --git a/tests/api/test_forwarding.py b/tests/api/test_forwarding.py index 68ff57bfb..947057f9f 100644 --- a/tests/api/test_forwarding.py +++ b/tests/api/test_forwarding.py @@ -5,9 +5,6 @@ import pytest from httpx import RequestError -from hypothesis import HealthCheck -from hypothesis import settings as hypothesis_settings -from hypothesis import strategies as st from pydantic import ValidationError from ralph.api.forwarding import forward_xapi_statements, get_active_xapi_forwardings @@ -17,7 +14,9 @@ from tests.factories import mock_instance -def test_api_forwarding_with_valid_configuration(monkeypatch, ): +def test_api_forwarding_with_valid_configuration( + monkeypatch, +): """Test the settings, given a valid forwarding configuration, should not raise an exception. """ @@ -85,8 +84,12 @@ def test_api_forwarding_get_active_xapi_forwardings_with_inactive_forwardings( configurations are inactive and return a list containing only active forwardings. """ - active_forwarding = mock_instance(XapiForwardingConfigurationSettings, is_active=True) - inactive_forwarding = mock_instance(XapiForwardingConfigurationSettings, is_active=False) + active_forwarding = mock_instance( + XapiForwardingConfigurationSettings, is_active=True + ) + inactive_forwarding = mock_instance( + XapiForwardingConfigurationSettings, is_active=False + ) active_forwarding_json = active_forwarding.json() inactive_forwarding_json = inactive_forwarding.json() @@ -133,9 +136,9 @@ async def test_api_forwarding_forward_xapi_statements_with_successful_request( count if the request was successful. """ - forwarding = mock_instance(XapiForwardingConfigurationSettings, - max_retries=1, - is_active=True) + forwarding = mock_instance( + XapiForwardingConfigurationSettings, max_retries=1, is_active=True + ) class MockSuccessfulResponse: """Dummy Successful Response.""" diff --git a/tests/factories.py b/tests/factories.py index 8d0da5ff2..b093abb03 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -1,69 +1,53 @@ """Mock model generation for testing.""" -from typing import Any, Callable from decimal import Decimal +from typing import Any, Callable -from pydantic import NonNegativeFloat +from polyfactory.factories.base import BaseFactory from polyfactory.factories.pydantic_factory import ( ModelFactory as PolyfactoryModelFactory, +) +from polyfactory.factories.pydantic_factory import ( T, ) from polyfactory.fields import Ignore +from pydantic import BaseModel from ralph.models.edx.navigational.fields.events import NavigationalEventField from ralph.models.edx.navigational.statements import UISeqNext, UISeqPrev -from ralph.models.xapi.base.common import MailtoEmail, IRI -from ralph.models.xapi import VirtualClassroomAnsweredPoll - -from ralph.models.xapi.concepts.activity_types.virtual_classroom import VirtualClassroomActivity -from ralph.models.xapi.base.contexts import BaseXapiContext -from ralph.models.xapi.base.results import BaseXapiResultScore -from ralph.models.xapi.base.common import LanguageTag -from ralph.models.xapi.base.contexts import BaseXapiContextContextActivities -from ralph.models.xapi.base.unnested_objects import ( - BaseXapiActivityInteractionDefinition, +from ralph.models.xapi.base.common import IRI, LanguageTag +from ralph.models.xapi.base.contexts import ( + BaseXapiContext, ) - +from ralph.models.xapi.base.results import BaseXapiResultScore from ralph.models.xapi.lms.contexts import ( LMSContextContextActivities, LMSProfileActivity, ) - from ralph.models.xapi.video.contexts import ( VideoContextContextActivities, VideoProfileActivity, ) - -from ralph.models.xapi.virtual_classroom.contexts import VirtualClassroomStartedPollContextActivities, VirtualClassroomPostedPublicMessageContextActivities - from ralph.models.xapi.virtual_classroom.contexts import ( - VirtualClassroomContextContextActivities, - VirtualClassroomProfileActivity, VirtualClassroomAnsweredPollContextActivities, + VirtualClassroomPostedPublicMessageContextActivities, + VirtualClassroomProfileActivity, + VirtualClassroomStartedPollContextActivities, ) -from pydantic import BaseModel -from polyfactory.factories.base import BaseFactory - -from pprint import pprint - def prune(d: Any, exceptions: list = []): """Remove all empty leaves from a dict, except fo those in `exceptions`.""" # TODO: add test ? - + if isinstance(d, BaseModel): d = d.dict() if isinstance(d, dict): d_dict_not_exceptions = { k: prune(v) for k, v in d.items() if k not in exceptions - } - d_dict_not_exceptions = { - k: v for k, v in d.items() if v - } - d_dict_exceptions = { - k: v for k, v in d.items() if k in exceptions } + d_dict_not_exceptions = {k: v for k, v in d.items() if v} + d_dict_exceptions = {k: v for k, v in d.items() if k in exceptions} return d_dict_not_exceptions | d_dict_exceptions elif isinstance(d, list): d_list = [prune(v) for v in d] @@ -76,7 +60,7 @@ def prune(d: Any, exceptions: list = []): class ModelFactory(PolyfactoryModelFactory[T]): __allow_none_optionals__ = False __is_base_factory__ = True - __random_seed__ = 6 # TODO: remove this + __random_seed__ = 6 # TODO: remove this @classmethod def get_provider_map(cls) -> dict[Any, Callable[[], Any]]: @@ -87,7 +71,7 @@ def get_provider_map(cls) -> dict[Any, Callable[[], Any]]: @classmethod def _get_or_create_factory(cls, model: type): - #print("Cls:", model) + # print("Cls:", model) created_factory = super()._get_or_create_factory(model) created_factory.get_provider_map = cls.get_provider_map created_factory._get_or_create_factory = cls._get_or_create_factory @@ -116,6 +100,7 @@ class BaseXapiResultScoreFactory(ModelFactory[BaseXapiResultScore]): def myfunc(): raise Exception("WHAT ARE YOU EVEN DOING") + # TODO: put back ? # class BaseXapiContextContextActivitiesFactory( # ModelFactory[BaseXapiContextContextActivities] @@ -137,34 +122,45 @@ class BaseXapiContextFactory(ModelFactory[BaseXapiContext]): # lambda: BaseXapiContextContextActivitiesFactory.build() or Ignore() # ) -class LMSContextContextActivitiesFactory(ModelFactory[LMSContextContextActivities]): + +class LMSContextContextActivitiesFactory(ModelFactory[LMSContextContextActivities]): __model__ = LMSContextContextActivities __set_as_default_factory_for_type__ = True - - category = lambda: mock_xapi_instance(LMSProfileActivity) # TODO: Uncomment + + category = lambda: mock_xapi_instance(LMSProfileActivity) # TODO: Uncomment + class VideoContextContextActivitiesFactory(ModelFactory[VideoContextContextActivities]): __model__ = VideoContextContextActivities __set_as_default_factory_for_type__ = True - - category = lambda: mock_xapi_instance(VideoProfileActivity) -class VirtualClassroomStartedPollContextActivitiesFactory(ModelFactory[VirtualClassroomStartedPollContextActivities]): + category = lambda: mock_xapi_instance(VideoProfileActivity) + + +class VirtualClassroomStartedPollContextActivitiesFactory( + ModelFactory[VirtualClassroomStartedPollContextActivities] +): __model__ = VirtualClassroomStartedPollContextActivities __set_as_default_factory_for_type__ = True - + category = lambda: mock_xapi_instance(VirtualClassroomProfileActivity) -class VirtualClassroomAnsweredPollContextActivitiesFactory(ModelFactory[VirtualClassroomAnsweredPollContextActivities]): + +class VirtualClassroomAnsweredPollContextActivitiesFactory( + ModelFactory[VirtualClassroomAnsweredPollContextActivities] +): __model__ = VirtualClassroomAnsweredPollContextActivities __set_as_default_factory_for_type__ = True - + category = lambda: mock_xapi_instance(VirtualClassroomProfileActivity) -class VirtualClassroomPostedPublicMessageContextActivitiesFactory(ModelFactory[VirtualClassroomPostedPublicMessageContextActivities]): + +class VirtualClassroomPostedPublicMessageContextActivitiesFactory( + ModelFactory[VirtualClassroomPostedPublicMessageContextActivities] +): __model__ = VirtualClassroomPostedPublicMessageContextActivities __set_as_default_factory_for_type__ = True - + category = lambda: mock_xapi_instance(VirtualClassroomProfileActivity) @@ -172,8 +168,7 @@ class VirtualClassroomPostedPublicMessageContextActivitiesFactory(ModelFactory[V # __model__ = VirtualClassroomAnsweredPollContextActivities # category = lambda: mock_xapi_instance(VirtualClassroomProfileActivity) -# parent = lambda: mock_xapi_instance(VirtualClassroomActivity) # TODO: Remove this. Check that this is valid - +# parent = lambda: mock_xapi_instance(VirtualClassroomActivity) # TODO: Remove this. Check that this is valid class UISeqPrev(ModelFactory[UISeqPrev]): @@ -182,6 +177,7 @@ class UISeqPrev(ModelFactory[UISeqPrev]): event = lambda: mock_instance(NavigationalEventField, old=1, new=0) + class UISeqNext(ModelFactory[UISeqNext]): __model__ = UISeqNext __set_as_default_factory_for_type__ = True @@ -194,8 +190,10 @@ def mock_xapi_instance(klass, *args, **kwargs): # Avoid redifining custom factories if klass not in BaseFactory._factory_type_mapping: + class KlassFactory(ModelFactory[klass]): __model__ = klass + else: KlassFactory = BaseFactory._factory_type_mapping[klass] @@ -205,13 +203,16 @@ class KlassFactory(ModelFactory[klass]): return klass(**kwargs) + def mock_instance(klass, *args, **kwargs): """Generate a mock instance of a given class (`klass`).""" # Avoid redifining custom factories if klass not in BaseFactory._factory_type_mapping: + class KlassFactory(ModelFactory[klass]): __model__ = klass + else: KlassFactory = BaseFactory._factory_type_mapping[klass] @@ -221,4 +222,4 @@ class KlassFactory(ModelFactory[klass]): def mock_url(): - return ModelFactory.__faker__.url() \ No newline at end of file + return ModelFactory.__faker__.url() diff --git a/tests/fixtures/hypothesis_configuration.py b/tests/fixtures/hypothesis_configuration.py index 1924be0e3..65c4020c5 100644 --- a/tests/fixtures/hypothesis_configuration.py +++ b/tests/fixtures/hypothesis_configuration.py @@ -1,12 +1,11 @@ """Hypothesis fixture configuration.""" -import operator from hypothesis import provisional, settings from hypothesis import strategies as st from pydantic import AnyHttpUrl, AnyUrl, StrictStr -from ralph.models.xapi.base.common import IRI, LanguageTag, MailtoEmail +from ralph.models.xapi.base.common import IRI, LanguageTag settings.register_profile("development", max_examples=1) settings.load_profile("development") diff --git a/tests/fixtures/hypothesis_strategies.py b/tests/fixtures/hypothesis_strategies.py index 56f43f8dc..c201c60dc 100644 --- a/tests/fixtures/hypothesis_strategies.py +++ b/tests/fixtures/hypothesis_strategies.py @@ -3,8 +3,7 @@ # import random # from typing import Union -from hypothesis import given -from hypothesis import strategies as st + # from pydantic import BaseModel # from ralph.models.edx.navigational.fields.events import NavigationalEventField @@ -101,12 +100,9 @@ # strategies.append(custom_builds(arg) if is_base_model(arg) else arg) # return given(*strategies, **kwargs) -from ralph.models.xapi.base.statements import BaseXapiStatement -from pydantic import BaseModel -from tests.factories import mock_instance, mock_xapi_instance # def custom_given(model: BaseModel, **kwargs): - + # if issubclass(model, BaseXapiStatement): # func = mock_xapi_instance # else: diff --git a/tests/models/edx/converters/xapi/test_enrollment.py b/tests/models/edx/converters/xapi/test_enrollment.py index 105f4d542..87578b79b 100644 --- a/tests/models/edx/converters/xapi/test_enrollment.py +++ b/tests/models/edx/converters/xapi/test_enrollment.py @@ -4,7 +4,6 @@ from uuid import UUID, uuid5 import pytest -from hypothesis import provisional from ralph.models.converter import convert_dict_event from ralph.models.edx.converters.xapi.enrollment import ( @@ -19,10 +18,11 @@ # from tests.fixtures.hypothesis_strategies import custom_given from tests.factories import mock_instance, mock_url + # @custom_given(EdxCourseEnrollmentActivated, provisional.urls()) @pytest.mark.parametrize("uuid_namespace", ["ee241f8b-174f-5bdb-bae9-c09de5fe017f"]) def test_models_edx_converters_xapi_enrollment_edx_course_enrollment_activated_to_lms_registered_course( # noqa: E501 - uuid_namespace#, event, platform_url + uuid_namespace, # , event, platform_url ): """Test that converting with `EdxCourseEnrollmentActivatedToLMSRegisteredCourse` returns the expected xAPI statement. @@ -71,7 +71,7 @@ def test_models_edx_converters_xapi_enrollment_edx_course_enrollment_activated_t # @custom_given(EdxCourseEnrollmentDeactivated, provisional.urls()) @pytest.mark.parametrize("uuid_namespace", ["ee241f8b-174f-5bdb-bae9-c09de5fe017f"]) def test_models_edx_converters_xapi_enrollment_edx_course_enrollment_deactivated_to_lms_unregistered_course( # noqa: E501 - uuid_namespace + uuid_namespace, ): """Test that converting with `EdxCourseEnrollmentDeactivatedToLMSUnregisteredCourse` returns the expected xAPI diff --git a/tests/models/edx/converters/xapi/test_navigational.py b/tests/models/edx/converters/xapi/test_navigational.py index 7e4c9f532..e9f0660e2 100644 --- a/tests/models/edx/converters/xapi/test_navigational.py +++ b/tests/models/edx/converters/xapi/test_navigational.py @@ -4,7 +4,6 @@ from uuid import UUID, uuid5 import pytest -from hypothesis import provisional from ralph.models.converter import convert_dict_event from ralph.models.edx.converters.xapi.navigational import UIPageCloseToPageTerminated @@ -13,16 +12,17 @@ # from tests.fixtures.hypothesis_strategies import custom_given from tests.factories import mock_instance, mock_url + @pytest.mark.parametrize("uuid_namespace", ["ee241f8b-174f-5bdb-bae9-c09de5fe017f"]) def test_models_edx_converters_xapi_navigational_ui_page_close_to_page_terminated( - uuid_namespace + uuid_namespace, ): """Test that converting with UIPageCloseToPageTerminated returns the expected xAPI statement. """ event = mock_instance(UIPageClose) platform_url = mock_url() - assert platform_url is not None # TODO: remove this + assert platform_url is not None # TODO: remove this event.context.user_id = "1" event_str = event.json() diff --git a/tests/models/edx/converters/xapi/test_server.py b/tests/models/edx/converters/xapi/test_server.py index 294cd76da..8a1bd215d 100644 --- a/tests/models/edx/converters/xapi/test_server.py +++ b/tests/models/edx/converters/xapi/test_server.py @@ -4,7 +4,6 @@ from uuid import UUID, uuid5 import pytest -from hypothesis import provisional, settings from ralph.models.converter import convert_dict_event, convert_str_event from ralph.models.edx.converters.xapi.server import ServerEventToPageViewed @@ -13,9 +12,10 @@ # from tests.fixtures.hypothes is_ strategies import custom_given from tests.factories import mock_instance, mock_url + @pytest.mark.parametrize("uuid_namespace", ["ee241f8b-174f-5bdb-bae9-c09de5fe017f"]) def test_models_edx_converters_xapi_server_server_event_to_page_viewed_constant_uuid( - uuid_namespace + uuid_namespace, ): """Test that `ServerEventToPageViewed.convert` returns a JSON string with a constant UUID. @@ -35,9 +35,7 @@ def test_models_edx_converters_xapi_server_server_event_to_page_viewed_constant_ @pytest.mark.parametrize("uuid_namespace", ["ee241f8b-174f-5bdb-bae9-c09de5fe017f"]) -def test_models_edx_converters_xapi_server_server_event_to_page_viewed( - uuid_namespace -): +def test_models_edx_converters_xapi_server_server_event_to_page_viewed(uuid_namespace): """Test that converting with `ServerEventToPageViewed` returns the expected xAPI statement. """ @@ -73,7 +71,7 @@ def test_models_edx_converters_xapi_server_server_event_to_page_viewed( @pytest.mark.parametrize("uuid_namespace", ["ee241f8b-174f-5bdb-bae9-c09de5fe017f"]) def test_models_edx_converters_xapi_server_server_event_to_page_viewed_with_anonymous_user( # noqa: E501 - uuid_namespace + uuid_namespace, ): """Test that anonymous usernames are replaced with `anonymous`.""" event = mock_instance(Server) diff --git a/tests/models/edx/converters/xapi/test_video.py b/tests/models/edx/converters/xapi/test_video.py index fa36971b6..63dcf498d 100644 --- a/tests/models/edx/converters/xapi/test_video.py +++ b/tests/models/edx/converters/xapi/test_video.py @@ -4,7 +4,6 @@ from uuid import UUID, uuid5 import pytest -from hypothesis import provisional from ralph.models.converter import convert_dict_event from ralph.models.edx.converters.xapi.video import ( @@ -25,9 +24,10 @@ # from tests.fixtures.hypothesis_strategies import custom_given from tests.factories import mock_instance, mock_url + @pytest.mark.parametrize("uuid_namespace", ["ee241f8b-174f-5bdb-bae9-c09de5fe017f"]) def test_models_edx_converters_xapi_video_ui_load_video_to_video_initialized( - uuid_namespace + uuid_namespace, ): """Test that converting with `UILoadVideoToVideoInitialized` returns the expected xAPI statement. @@ -83,16 +83,13 @@ def test_models_edx_converters_xapi_video_ui_load_video_to_video_initialized( @pytest.mark.parametrize("uuid_namespace", ["ee241f8b-174f-5bdb-bae9-c09de5fe017f"]) -def test_models_edx_converters_xapi_video_ui_play_video_to_video_played( - uuid_namespace -): +def test_models_edx_converters_xapi_video_ui_play_video_to_video_played(uuid_namespace): """Test that converting with `UIPlayVideoToVideoPlayed` returns the expected xAPI statement. """ event = mock_instance(UIPlayVideo) platform_url = mock_url() - event.context.user_id = "1" event.session = "af45a0e650c4a4fdb0bcde75a1e4b694" session_uuid = "af45a0e6-50c4-a4fd-b0bc-de75a1e4b694" @@ -146,7 +143,7 @@ def test_models_edx_converters_xapi_video_ui_play_video_to_video_played( @pytest.mark.parametrize("uuid_namespace", ["ee241f8b-174f-5bdb-bae9-c09de5fe017f"]) def test_models_edx_converters_xapi_video_ui_pause_video_to_video_paused( - uuid_namespace + uuid_namespace, ): """Test that converting with `UIPauseVideoToVideoPaused` returns the expected xAPI statement. @@ -208,7 +205,7 @@ def test_models_edx_converters_xapi_video_ui_pause_video_to_video_paused( @pytest.mark.parametrize("uuid_namespace", ["ee241f8b-174f-5bdb-bae9-c09de5fe017f"]) def test_models_edx_converters_xapi_video_ui_stop_video_to_video_terminated( - uuid_namespace + uuid_namespace, ): """Test that converting with `UIStopVideoToVideoTerminated` returns the expected xAPI statement. @@ -216,7 +213,6 @@ def test_models_edx_converters_xapi_video_ui_stop_video_to_video_terminated( event = mock_instance(UIStopVideo) platform_url = mock_url() - event.context.user_id = "1" event.session = "af45a0e650c4a4fdb0bcde75a1e4b694" session_uuid = "af45a0e6-50c4-a4fd-b0bc-de75a1e4b694" @@ -269,17 +265,15 @@ def test_models_edx_converters_xapi_video_ui_stop_video_to_video_terminated( "version": "1.0.0", } + @pytest.mark.parametrize("uuid_namespace", ["ee241f8b-174f-5bdb-bae9-c09de5fe017f"]) -def test_models_edx_converters_xapi_video_ui_seek_video_to_video_seeked( - uuid_namespace -): +def test_models_edx_converters_xapi_video_ui_seek_video_to_video_seeked(uuid_namespace): """Test that converting with `UISeekVideoToVideoSeeked` returns the expected xAPI statement. """ event = mock_instance(UISeekVideo) platform_url = mock_url() - event.context.user_id = "1" event.session = "af45a0e650c4a4fdb0bcde75a1e4b694" session_uuid = "af45a0e6-50c4-a4fd-b0bc-de75a1e4b694" diff --git a/tests/models/edx/navigational/test_events.py b/tests/models/edx/navigational/test_events.py index 30680af35..57dc3db01 100644 --- a/tests/models/edx/navigational/test_events.py +++ b/tests/models/edx/navigational/test_events.py @@ -11,6 +11,7 @@ # from tests.fixtures.hypothesis_strategies import custom_given from tests.factories import mock_instance + def test_fields_edx_navigational_events_event_field_with_valid_content(): """Test that a valid `NavigationalEventField` does not raise a `ValidationError`. diff --git a/tests/models/edx/navigational/test_statements.py b/tests/models/edx/navigational/test_statements.py index 07679aac8..16694b3e9 100644 --- a/tests/models/edx/navigational/test_statements.py +++ b/tests/models/edx/navigational/test_statements.py @@ -4,7 +4,6 @@ import re import pytest -from hypothesis import strategies as st from pydantic.error_wrappers import ValidationError from ralph.models.edx.navigational.fields.events import NavigationalEventField @@ -19,6 +18,7 @@ # from tests.fixtures.hypothesis_strategies import custom_builds, custom_given from tests.factories import mock_instance + @pytest.mark.parametrize( "class_", [ diff --git a/tests/models/edx/open_response_assessment/test_events.py b/tests/models/edx/open_response_assessment/test_events.py index 46b95c0ca..dbd14ee92 100644 --- a/tests/models/edx/open_response_assessment/test_events.py +++ b/tests/models/edx/open_response_assessment/test_events.py @@ -16,6 +16,7 @@ # from tests.fixtures.hypothesis_strategies import custom_given from tests.factories import mock_instance + def test_models_edx_ora_get_peer_submission_event_field_with_valid_values(): """Test that a valid `ORAGetPeerSubmissionEventField` does not raise a `ValidationError`. @@ -27,8 +28,7 @@ def test_models_edx_ora_get_peer_submission_event_field_with_valid_values(): ) -def test_models_edx_ora_get_submission_for_staff_grading_event_field_with_valid_values( -): +def test_models_edx_ora_get_submission_for_staff_grading_event_field_with_valid_values(): """Test that a valid `ORAGetSubmissionForStaffGradingEventField` does not raise a `ValidationError`. """ @@ -72,7 +72,7 @@ def test_models_edx_ora_assess_event_field_with_invalid_values(score_type): ], ) def test_models_edx_ora_assess_event_rubric_field_with_invalid_problem_id_value( - content_hash + content_hash, ): """Test that an invalid `problem_id` value in `ProblemCheckEventField` raises a `ValidationError`. diff --git a/tests/models/edx/open_response_assessment/test_statements.py b/tests/models/edx/open_response_assessment/test_statements.py index d140d4921..8e6a5a7b6 100644 --- a/tests/models/edx/open_response_assessment/test_statements.py +++ b/tests/models/edx/open_response_assessment/test_statements.py @@ -3,7 +3,6 @@ import json import pytest -from hypothesis import strategies as st from ralph.models.edx.open_response_assessment.statements import ( ORACreateSubmission, @@ -22,6 +21,7 @@ # from tests.fixtures.hypothesis_strategies import custom_builds, custom_given from tests.factories import mock_instance + @pytest.mark.parametrize( "class_", [ @@ -56,9 +56,7 @@ def test_models_edx_ora_get_peer_submission_with_valid_statement(): assert statement.page == "x_module" -def test_models_edx_ora_get_submission_for_staff_grading_with_valid_statement( - -): +def test_models_edx_ora_get_submission_for_staff_grading_with_valid_statement(): """Test that a `openassessmentblock.get_submission_for_staff_grading` statement has the expected `event_type` and `page` fields. """ diff --git a/tests/models/edx/peer_instruction/test_events.py b/tests/models/edx/peer_instruction/test_events.py index 25c0fbe50..2b44339f3 100644 --- a/tests/models/edx/peer_instruction/test_events.py +++ b/tests/models/edx/peer_instruction/test_events.py @@ -10,6 +10,7 @@ # from tests.fixtures.hypothesis_strategies import custom_given from tests.factories import mock_instance + def test_models_edx_peer_instruction_event_field_with_valid_field(): """Test that a valid `PeerInstructionEventField` does not raise a `ValidationError`. diff --git a/tests/models/edx/peer_instruction/test_statements.py b/tests/models/edx/peer_instruction/test_statements.py index c6b42e951..2fca67dfd 100644 --- a/tests/models/edx/peer_instruction/test_statements.py +++ b/tests/models/edx/peer_instruction/test_statements.py @@ -3,7 +3,6 @@ import json import pytest -from hypothesis import strategies as st from ralph.models.edx.peer_instruction.statements import ( PeerInstructionAccessed, @@ -15,6 +14,7 @@ # from tests.fixtures.hypothesis_strategies import custom_builds, custom_given from tests.factories import mock_instance + @pytest.mark.parametrize( "class_", [ @@ -32,8 +32,7 @@ def test_models_edx_peer_instruction_selectors_with_valid_statements(class_): assert model is class_ -def test_models_edx_peer_instruction_accessed_with_valid_statement( -): +def test_models_edx_peer_instruction_accessed_with_valid_statement(): """Test that a `ubc.peer_instruction.accessed` statement has the expected `event_type`. """ @@ -42,9 +41,7 @@ def test_models_edx_peer_instruction_accessed_with_valid_statement( assert statement.name == "ubc.peer_instruction.accessed" -def test_models_edx_peer_instruction_original_submitted_with_valid_statement( - -): +def test_models_edx_peer_instruction_original_submitted_with_valid_statement(): """Test that a `ubc.peer_instruction.original_submitted` statement has the expected `event_type`. """ @@ -53,9 +50,7 @@ def test_models_edx_peer_instruction_original_submitted_with_valid_statement( assert statement.name == "ubc.peer_instruction.original_submitted" -def test_models_edx_peer_instruction_revised_submitted_with_valid_statement( - -): +def test_models_edx_peer_instruction_revised_submitted_with_valid_statement(): """Test that a `ubc.peer_instruction.revised_submitted` statement has the expected `event_type`. """ diff --git a/tests/models/edx/problem_interaction/test_events.py b/tests/models/edx/problem_interaction/test_events.py index 5ba73f8c2..71f937c6b 100644 --- a/tests/models/edx/problem_interaction/test_events.py +++ b/tests/models/edx/problem_interaction/test_events.py @@ -22,6 +22,7 @@ # from tests.fixtures.hypothesis_strategies import custom_given from tests.factories import mock_instance + def test_models_edx_correct_map_with_valid_content(): """Test that a valid `CorrectMap` does not raise a `ValidationError`.""" subfield = mock_instance(CorrectMap) @@ -81,7 +82,7 @@ def test_models_edx_problem_hint_feedback_displayed_event_field_with_valid_field ], ) def test_models_edx_problem_hint_feedback_displayed_event_field_with_invalid_question_type_value( # noqa - question_type + question_type, ): """Test that an invalid `question_type` value in `EdxProblemHintFeedbackDisplayedEventField` raises a `ValidationError`. @@ -97,7 +98,7 @@ def test_models_edx_problem_hint_feedback_displayed_event_field_with_invalid_que @pytest.mark.parametrize("trigger_type", ["jingle", "compund"]) def test_models_edx_problem_hint_feedback_displayed_event_field_with_invalid_trigger_type_value( # noqa - trigger_type + trigger_type, ): """Test that an invalid `question_type` value in `EdxProblemHintFeedbackDisplayedEventField` raises a `ValidationError`. @@ -152,9 +153,7 @@ def test_models_edx_problem_check_event_field_with_valid_field(): ), ], ) -def test_models_edx_problem_check_event_field_with_invalid_problem_id_value( - problem_id -): +def test_models_edx_problem_check_event_field_with_invalid_problem_id_value(problem_id): """Test that an invalid `problem_id` value in `ProblemCheckEventField` raises a `ValidationError`. """ @@ -169,9 +168,7 @@ def test_models_edx_problem_check_event_field_with_invalid_problem_id_value( @pytest.mark.parametrize("success", ["corect", "incorect"]) -def test_models_edx_problem_check_event_field_with_invalid_success_value( - success -): +def test_models_edx_problem_check_event_field_with_invalid_success_value(success): """Test that an invalid `success` value in `ProblemCheckEventField` raises a `ValidationError`. """ @@ -226,7 +223,7 @@ def test_models_edx_problem_check_fail_event_field_with_valid_field(): ], ) def test_models_edx_problem_check_fail_event_field_with_invalid_problem_id_value( - problem_id + problem_id, ): """Test that an invalid `problem_id` value in `ProblemCheckFailEventField` raises a `ValidationError`. @@ -242,9 +239,7 @@ def test_models_edx_problem_check_fail_event_field_with_invalid_problem_id_value @pytest.mark.parametrize("failure", ["close", "unresit"]) -def test_models_edx_problem_check_fail_event_field_with_invalid_failure_value( - failure -): +def test_models_edx_problem_check_fail_event_field_with_invalid_failure_value(failure): """Test that an invalid `failure` value in `ProblemCheckFailEventField` raises a `ValidationError`. """ @@ -299,7 +294,7 @@ def test_models_edx_problem_rescore_event_field_with_valid_field(): ], ) def test_models_edx_problem_rescore_event_field_with_invalid_problem_id_value( - problem_id + problem_id, ): """Test that an invalid `problem_id` value in `ProblemRescoreEventField` raises a `ValidationError`. @@ -315,9 +310,7 @@ def test_models_edx_problem_rescore_event_field_with_invalid_problem_id_value( @pytest.mark.parametrize("success", ["corect", "incorect"]) -def test_models_edx_problem_rescore_event_field_with_invalid_success_value( - success -): +def test_models_edx_problem_rescore_event_field_with_invalid_success_value(success): """Test that an invalid `success` value in `ProblemRescoreEventField` raises a `ValidationError`. """ @@ -372,7 +365,7 @@ def test_models_edx_problem_rescore_fail_event_field_with_valid_field(): ], ) def test_models_edx_problem_rescore_fail_event_field_with_invalid_problem_id_value( - problem_id + problem_id, ): """Test that an invalid `problem_id` value in `ProblemRescoreFailEventField` raises a `ValidationError`. @@ -389,7 +382,7 @@ def test_models_edx_problem_rescore_fail_event_field_with_invalid_problem_id_val @pytest.mark.parametrize("failure", ["close", "unresit"]) def test_models_edx_problem_rescore_fail_event_field_with_invalid_failure_value( - failure + failure, ): """Test that an invalid `failure` value in `ProblemRescoreFailEventField` raises a `ValidationError`. @@ -443,9 +436,7 @@ def test_models_edx_reset_problem_event_field_with_valid_field(): ), ], ) -def test_models_edx_reset_problem_event_field_with_invalid_problem_id_value( - problem_id -): +def test_models_edx_reset_problem_event_field_with_invalid_problem_id_value(problem_id): """Test that an invalid `problem_id` value in `ResetProblemEventField` raises a `ValidationError`. """ @@ -502,7 +493,7 @@ def test_models_edx_reset_problem_fail_event_field_with_valid_field(): ], ) def test_models_edx_reset_problem_fail_event_field_with_invalid_problem_id_value( - problem_id + problem_id, ): """Test that an invalid `problem_id` value in `ResetProblemFailEventField` raises a `ValidationError`. @@ -518,9 +509,7 @@ def test_models_edx_reset_problem_fail_event_field_with_invalid_problem_id_value @pytest.mark.parametrize("failure", ["close", "not_close"]) -def test_models_edx_reset_problem_fail_event_field_with_invalid_failure_value( - failure -): +def test_models_edx_reset_problem_fail_event_field_with_invalid_failure_value(failure): """Test that an invalid `failure` value in `ResetProblemFailEventField` raises a `ValidationError`. """ @@ -575,7 +564,7 @@ def test_models_edx_save_problem_fail_event_field_with_valid_field(): ], ) def test_models_edx_save_problem_fail_event_field_with_invalid_problem_id_value( - problem_id + problem_id, ): """Test that an invalid `problem_id` value in `SaveProblemFailEventField` raises a `ValidationError`. @@ -591,9 +580,7 @@ def test_models_edx_save_problem_fail_event_field_with_invalid_problem_id_value( @pytest.mark.parametrize("failure", ["close", "doned"]) -def test_models_edx_save_problem_fail_event_field_with_invalid_failure_value( - failure -): +def test_models_edx_save_problem_fail_event_field_with_invalid_failure_value(failure): """Test that an invalid `failure` value in `SaveProblemFailEventField` raises a `ValidationError`. """ @@ -647,7 +634,7 @@ def test_models_edx_save_problem_success_event_field_with_valid_field(): ], ) def test_models_edx_save_problem_success_event_field_with_invalid_problem_id_value( - problem_id + problem_id, ): """Test that an invalid `problem_id` value in `SaveProblemSuccessEventField` raises a `ValidationError`. diff --git a/tests/models/edx/problem_interaction/test_statements.py b/tests/models/edx/problem_interaction/test_statements.py index c3573a040..fdd69767e 100644 --- a/tests/models/edx/problem_interaction/test_statements.py +++ b/tests/models/edx/problem_interaction/test_statements.py @@ -3,7 +3,6 @@ import json import pytest -from hypothesis import strategies as st from ralph.models.edx.problem_interaction.statements import ( EdxProblemHintDemandhintDisplayed, @@ -28,6 +27,7 @@ # from tests.fixtures.hypothesis_strategies import custom_builds, custom_given from tests.factories import mock_instance + @pytest.mark.parametrize( "class_", [ @@ -49,9 +49,7 @@ UIProblemShow, ], ) -def test_models_edx_edx_problem_interaction_selectors_with_valid_statements( - class_ -): +def test_models_edx_edx_problem_interaction_selectors_with_valid_statements(class_): """Test given a valid problem interaction edX statement the `get_first_model` selector method should return the expected model. """ @@ -60,8 +58,7 @@ def test_models_edx_edx_problem_interaction_selectors_with_valid_statements( assert model is class_ -def test_models_edx_edx_problem_hint_demandhint_displayed_with_valid_statement( -): +def test_models_edx_edx_problem_hint_demandhint_displayed_with_valid_statement(): """Test that a `edx.problem.hint.demandhint_displayed` statement has the expected `event_type` and `page`. """ diff --git a/tests/models/edx/test_browser.py b/tests/models/edx/test_browser.py index 7257c731b..e34bb4daf 100644 --- a/tests/models/edx/test_browser.py +++ b/tests/models/edx/test_browser.py @@ -31,9 +31,7 @@ def test_models_edx_base_browser_model_with_valid_statement(): ], ) # @custom_given(BaseBrowserModel) -def test_models_edx_base_browser_model_with_invalid_statement( - session, error -): +def test_models_edx_base_browser_model_with_invalid_statement(session, error): """Test that an invalid base browser statement raises a `ValidationError`.""" statement = mock_instance(BaseBrowserModel) invalid_statement = json.loads(statement.json()) diff --git a/tests/models/edx/test_enrollment.py b/tests/models/edx/test_enrollment.py index 1f7e3f337..8a688e73d 100644 --- a/tests/models/edx/test_enrollment.py +++ b/tests/models/edx/test_enrollment.py @@ -3,7 +3,6 @@ import json import pytest -from hypothesis import strategies as st from ralph.models.edx.enrollment.statements import ( EdxCourseEnrollmentActivated, @@ -40,7 +39,7 @@ def test_models_edx_edx_course_enrollment_selectors_with_valid_statements(class_ # @custom_given(EdxCourseEnrollmentActivated) def test_models_edx_edx_course_enrollment_activated_with_valid_statement( - #statement, + # statement, ): """Test that a `edx.course.enrollment.activated` statement has the expected `event_type` and `name`. @@ -52,7 +51,7 @@ def test_models_edx_edx_course_enrollment_activated_with_valid_statement( # @custom_given(EdxCourseEnrollmentDeactivated) def test_models_edx_edx_course_enrollment_deactivated_with_valid_statement( - #statement, + # statement, ): """Test that a `edx.course.enrollment.deactivated` statement has the expected `event_type` and `name`. @@ -64,7 +63,7 @@ def test_models_edx_edx_course_enrollment_deactivated_with_valid_statement( # @custom_given(EdxCourseEnrollmentModeChanged) def test_models_edx_edx_course_enrollment_mode_changed_with_valid_statement( - #statement, + # statement, ): """Test that a `edx.course.enrollment.mode_changed` statement has the expected `event_type` and `name`. @@ -76,7 +75,7 @@ def test_models_edx_edx_course_enrollment_mode_changed_with_valid_statement( # @custom_given(UIEdxCourseEnrollmentUpgradeClicked) def test_models_edx_ui_edx_course_enrollment_upgrade_clicked_with_valid_statement( - #statement, + # statement, ): """Test that a `edx.course.enrollment.upgrade_clicked` statement has the expected `event_type` and `name`. @@ -88,7 +87,7 @@ def test_models_edx_ui_edx_course_enrollment_upgrade_clicked_with_valid_statemen # @custom_given(EdxCourseEnrollmentUpgradeSucceeded) def test_models_edx_edx_course_enrollment_upgrade_succeeded_with_valid_statement( - #statement, + # statement, ): """Test that a `edx.course.enrollment.upgrade.succeeded` statement has the expected `event_type` and `name`. diff --git a/tests/models/edx/test_server.py b/tests/models/edx/test_server.py index aaa2fd396..6c79bbf9c 100644 --- a/tests/models/edx/test_server.py +++ b/tests/models/edx/test_server.py @@ -11,6 +11,7 @@ # from tests.fixtures.hypothesis_strategies import custom_given from tests.factories import mock_instance + def test_model_selector_server_get_model_with_valid_event(): """Test given a server statement, the get_model method should return the corresponding model. diff --git a/tests/models/edx/textbook_interaction/test_events.py b/tests/models/edx/textbook_interaction/test_events.py index f94c90729..d218c816e 100644 --- a/tests/models/edx/textbook_interaction/test_events.py +++ b/tests/models/edx/textbook_interaction/test_events.py @@ -14,6 +14,7 @@ # from tests.fixtures.hypothesis_strategies import custom_given from tests.factories import mock_instance + def test_fields_edx_textbook_interaction_base_event_field_with_valid_content(): """Test that a valid `TextbookInteractionBaseEventField` does not raise a `ValidationError`. @@ -61,9 +62,7 @@ def test_fields_edx_textbook_interaction_base_event_field_with_valid_content(): ), ), ) -def test_fields_edx_textbook_interaction_base_event_field_with_invalid_content( - chapter -): +def test_fields_edx_textbook_interaction_base_event_field_with_invalid_content(chapter): """Test that an invalid `TextbookInteractionBaseEventField` raises a `ValidationError`. """ @@ -76,9 +75,7 @@ def test_fields_edx_textbook_interaction_base_event_field_with_invalid_content( TextbookInteractionBaseEventField(**invalid_field) -def test_fields_edx_textbook_pdf_chapter_navigated_event_field_with_valid_content( - -): +def test_fields_edx_textbook_pdf_chapter_navigated_event_field_with_valid_content(): """Test that a valid `TextbookPdfChapterNavigatedEventField` does not raise a `ValidationError`. """ @@ -122,7 +119,7 @@ def test_fields_edx_textbook_pdf_chapter_navigated_event_field_with_valid_conten ), ) def test_fields_edx_textbook_pdf_chapter_navigated_event_field_with_invalid_content( - chapter + chapter, ): """Test that an invalid `TextbookPdfChapterNavigatedEventField` raises a `ValidationError`. diff --git a/tests/models/edx/textbook_interaction/test_statements.py b/tests/models/edx/textbook_interaction/test_statements.py index be22264fd..f20dc1c6f 100644 --- a/tests/models/edx/textbook_interaction/test_statements.py +++ b/tests/models/edx/textbook_interaction/test_statements.py @@ -3,7 +3,6 @@ import json import pytest -from hypothesis import strategies as st from ralph.models.edx.textbook_interaction.statements import ( UIBook, @@ -26,6 +25,7 @@ # from tests.fixtures.hypothesis_strategies import custom_builds, custom_given from tests.factories import mock_instance + @pytest.mark.parametrize( "class_", [ @@ -45,9 +45,7 @@ UITextbookPdfZoomMenuChanged, ], ) -def test_models_edx_ui_textbook_interaction_selectors_with_valid_statements( - class_ -): +def test_models_edx_ui_textbook_interaction_selectors_with_valid_statements(class_): """Test given a valid textbook interaction edX statement the `get_first_model` selector method should return the expected model. """ @@ -72,8 +70,7 @@ def test_models_edx_ui_textbook_pdf_thumbnails_toggled_with_valid_statement(): assert statement.name == "textbook.pdf.thumbnails.toggled" -def test_models_edx_ui_textbook_pdf_thumbnail_navigated_with_valid_statement( -): +def test_models_edx_ui_textbook_pdf_thumbnail_navigated_with_valid_statement(): """Test that a `textbook.pdf.thumbnail.navigated` statement has the expected `event_type` and `name`. """ @@ -82,8 +79,7 @@ def test_models_edx_ui_textbook_pdf_thumbnail_navigated_with_valid_statement( assert statement.name == "textbook.pdf.thumbnail.navigated" -def test_models_edx_ui_textbook_pdf_outline_toggled_with_valid_statement( -): +def test_models_edx_ui_textbook_pdf_outline_toggled_with_valid_statement(): """Test that a `textbook.pdf.outline.toggled` statement has the expected `event_type` and `name`. """ @@ -92,8 +88,7 @@ def test_models_edx_ui_textbook_pdf_outline_toggled_with_valid_statement( assert statement.name == "textbook.pdf.outline.toggled" -def test_models_edx_ui_textbook_pdf_chapter_navigated_with_valid_statement( -): +def test_models_edx_ui_textbook_pdf_chapter_navigated_with_valid_statement(): """Test that a `textbook.pdf.chapter.navigated` statement has the expected `event_type` and `name`. """ @@ -102,8 +97,7 @@ def test_models_edx_ui_textbook_pdf_chapter_navigated_with_valid_statement( assert statement.name == "textbook.pdf.chapter.navigated" -def test_models_edx_ui_textbook_pdf_page_navigated_with_valid_statement( -): +def test_models_edx_ui_textbook_pdf_page_navigated_with_valid_statement(): """Test that a `textbook.pdf.page.navigated` statement has the expected `event_type` and `name`. """ @@ -112,8 +106,7 @@ def test_models_edx_ui_textbook_pdf_page_navigated_with_valid_statement( assert statement.name == "textbook.pdf.page.navigated" -def test_models_edx_ui_textbook_pdf_zoom_buttons_changed_with_valid_statement( -): +def test_models_edx_ui_textbook_pdf_zoom_buttons_changed_with_valid_statement(): """Test that a `textbook.pdf.zoom.buttons.changed` statement has the expected `event_type` and `name`. """ @@ -158,9 +151,7 @@ def test_models_edx_ui_textbook_pdf_search_executed_with_valid_statement(): assert statement.name == "textbook.pdf.search.executed" -def test_models_edx_ui_textbook_pdf_search_navigated_next_with_valid_statement( - -): +def test_models_edx_ui_textbook_pdf_search_navigated_next_with_valid_statement(): """Test that a `textbook.pdf.search.navigatednext` statement has the expected `event_type` and `name`. """ @@ -169,9 +160,7 @@ def test_models_edx_ui_textbook_pdf_search_navigated_next_with_valid_statement( assert statement.name == "textbook.pdf.search.navigatednext" -def test_models_edx_ui_textbook_pdf_search_highlight_toggled_with_valid_statement( - -): +def test_models_edx_ui_textbook_pdf_search_highlight_toggled_with_valid_statement(): """Test that a `textbook.pdf.search.highlight.toggled` statement has the expected `event_type` and `name`. """ @@ -180,9 +169,7 @@ def test_models_edx_ui_textbook_pdf_search_highlight_toggled_with_valid_statemen assert statement.name == "textbook.pdf.search.highlight.toggled" -def test_models_edx_ui_textbook_pdf_search_case_sensitivity_toggled_with_valid_statement( # noqa - -): +def test_models_edx_ui_textbook_pdf_search_case_sensitivity_toggled_with_valid_statement(): # noqa """Test that a `textbook.pdf.searchcasesensitivity.toggled` statement has the expected `event_type` and `name`. """ diff --git a/tests/models/edx/video/test_events.py b/tests/models/edx/video/test_events.py index 5d980c83b..a6cc7f89c 100644 --- a/tests/models/edx/video/test_events.py +++ b/tests/models/edx/video/test_events.py @@ -10,6 +10,7 @@ # from tests.fixtures.hypothesis_strategies import custom_given from tests.factories import mock_instance + def test_models_edx_speed_change_video_event_field_with_valid_field(): """Test that a valid `SpeedChangeVideoEventField` does not raise a `ValidationError`. @@ -24,7 +25,7 @@ def test_models_edx_speed_change_video_event_field_with_valid_field(): ["0,75", "1", "-1.0", "1.30"], ) def test_models_edx_speed_change_video_event_field_with_invalid_old_speed_value( - old_speed + old_speed, ): """Test that an invalid `old_speed` value in `SpeedChangeVideoEventField` raises a `ValidationError`. @@ -42,7 +43,7 @@ def test_models_edx_speed_change_video_event_field_with_invalid_old_speed_value( ["0,75", "1", "-1.0", "1.30"], ) def test_models_edx_speed_change_video_event_field_with_invalid_new_speed_value( - new_speed + new_speed, ): """Test that an invalid `new_speed` value in `SpeedChangeVideoEventField` raises a `ValidationError`. diff --git a/tests/models/edx/video/test_statements.py b/tests/models/edx/video/test_statements.py index 39689cad6..a578704f2 100644 --- a/tests/models/edx/video/test_statements.py +++ b/tests/models/edx/video/test_statements.py @@ -21,6 +21,7 @@ # from tests.fixtures.hypothesis_strategies import custom_builds, custom_given from tests.factories import mock_instance + @pytest.mark.parametrize( "class_", [ @@ -45,46 +46,38 @@ def test_models_edx_video_selectors_with_valid_statements(class_): assert model is class_ -def test_models_edx_ui_play_video_with_valid_statement( - -): +def test_models_edx_ui_play_video_with_valid_statement(): statement = mock_instance(UIPlayVideo) """Test that a `play_video` statement has the expected `event_type`.""" assert statement.event_type == "play_video" -def test_models_edx_ui_pause_video_with_valid_statement( - -): +def test_models_edx_ui_pause_video_with_valid_statement(): statement = mock_instance(UIPauseVideo) """Test that a `pause_video` statement has the expected `event_type`.""" assert statement.event_type == "pause_video" -def test_models_edx_ui_load_video_with_valid_statement( -): +def test_models_edx_ui_load_video_with_valid_statement(): statement = mock_instance(UILoadVideo) """Test that a `load_video` statement has the expected `event_type` and `name`.""" assert statement.event_type == "load_video" assert statement.name in {"load_video", "edx.video.loaded"} -def test_models_edx_ui_seek_video_with_valid_statement( -): +def test_models_edx_ui_seek_video_with_valid_statement(): statement = mock_instance(UISeekVideo) """Test that a `seek_video` statement has the expected `event_type`.""" assert statement.event_type == "seek_video" -def test_models_edx_ui_stop_video_with_valid_statement( -): +def test_models_edx_ui_stop_video_with_valid_statement(): statement = mock_instance(UIStopVideo) """Test that a `stop_video` statement has the expected `event_type`.""" assert statement.event_type == "stop_video" -def test_models_edx_ui_hide_transcript_with_valid_statement( -): +def test_models_edx_ui_hide_transcript_with_valid_statement(): """Test that a `hide_transcript` statement has the expected `event_type` and `name`. """ @@ -93,8 +86,7 @@ def test_models_edx_ui_hide_transcript_with_valid_statement( assert statement.name in {"hide_transcript", "edx.video.transcript.hidden"} -def test_models_edx_ui_show_transcript_with_valid_statement( -): +def test_models_edx_ui_show_transcript_with_valid_statement(): """Test that a `show_transcript` statement has the expected `event_type` and `name. """ @@ -103,22 +95,19 @@ def test_models_edx_ui_show_transcript_with_valid_statement( assert statement.name in {"show_transcript", "edx.video.transcript.shown"} -def test_models_edx_ui_speed_change_video_with_valid_statement( -): +def test_models_edx_ui_speed_change_video_with_valid_statement(): """Test that a `speed_change_video` statement has the expected `event_type`.""" statement = mock_instance(UISpeedChangeVideo) assert statement.event_type == "speed_change_video" -def test_models_edx_ui_vide_hide_cc_menu_with_valid_statement( -): +def test_models_edx_ui_vide_hide_cc_menu_with_valid_statement(): """Test that a `video_hide_cc_menu` statement has the expected `event_type`.""" statement = mock_instance(UIVideoHideCCMenu) assert statement.event_type == "video_hide_cc_menu" -def test_models_edx_ui_video_show_cc_menu_with_valid_statement( -): +def test_models_edx_ui_video_show_cc_menu_with_valid_statement(): """Test that a `video_show_cc_menu` statement has the expected `event_type`.""" statement = mock_instance(UIVideoShowCCMenu) assert statement.event_type == "video_show_cc_menu" diff --git a/tests/models/test_converter.py b/tests/models/test_converter.py index 0043be5e9..ebe9d6b4b 100644 --- a/tests/models/test_converter.py +++ b/tests/models/test_converter.py @@ -5,7 +5,6 @@ from typing import Any, Optional import pytest -from hypothesis import HealthCheck, settings from pydantic import BaseModel from pydantic.error_wrappers import ValidationError @@ -332,7 +331,7 @@ def test_converter_convert_with_an_event_missing_a_conversion_set_raises_an_exce @pytest.mark.parametrize("valid_uuid", ["ee241f8b-174f-5bdb-bae9-c09de5fe017f"]) @pytest.mark.parametrize("invalid_platform_url", ["", "not an URL"]) def test_converter_convert_with_invalid_arguments_raises_an_exception( - valid_uuid, invalid_platform_url, caplog + valid_uuid, invalid_platform_url, caplog ): """Test given invalid arguments causing the conversion to fail at the validation step, the convert method should raise a ValidationError. diff --git a/tests/models/test_validator.py b/tests/models/test_validator.py index 976da564d..800e0452c 100644 --- a/tests/models/test_validator.py +++ b/tests/models/test_validator.py @@ -5,7 +5,6 @@ import logging import pytest -from hypothesis import HealthCheck, settings from pydantic import ValidationError, create_model from ralph.exceptions import BadFormatException, UnknownEventException @@ -17,6 +16,7 @@ # from tests.fixtures.hypothesis_strategies import custom_given from tests.factories import mock_instance + def test_models_validator_validate_with_no_events(caplog): """Test given no events, the validate method does not write error messages.""" result = Validator(ModelSelector(module="ralph.models.edx")).validate( @@ -143,12 +143,10 @@ def test_models_validator_validate_with_invalid_page_close_event_raises_an_excep @pytest.mark.parametrize("ignore_errors", [True, False]) @pytest.mark.parametrize("fail_on_unknown", [True, False]) # @custom_given(UIPageClose) -def test_models_validator_validate_with_valid_events( - ignore_errors, fail_on_unknown -): +def test_models_validator_validate_with_valid_events(ignore_errors, fail_on_unknown): """Test given a valid event the validate method should yield it.""" event = mock_instance(UIPageClose) - + event_str = event.json() event_dict = json.loads(event_str) validator = Validator(ModelSelector(module="ralph.models.edx")) diff --git a/tests/models/xapi/base/test_agents.py b/tests/models/xapi/base/test_agents.py index 56b4e248a..c4df32814 100644 --- a/tests/models/xapi/base/test_agents.py +++ b/tests/models/xapi/base/test_agents.py @@ -9,7 +9,6 @@ from ralph.models.xapi.base.agents import BaseXapiAgentWithMboxSha1Sum # from tests.fixtures.hypothesis_strategies import custom_given - from tests.factories import mock_xapi_instance @@ -30,13 +29,11 @@ def test_models_xapi_base_agent_with_mbox_sha1_sum_ifi_with_valid_field(): "1baccdd9abcdfd4ae9b24dedfa939c7deffa3db3a7", ], ) -def test_models_xapi_base_agent_with_mbox_sha1_sum_ifi_with_invalid_field( - mbox_sha1sum -): +def test_models_xapi_base_agent_with_mbox_sha1_sum_ifi_with_invalid_field(mbox_sha1sum): """Test an invalid `mbox_sha1sum` property in BaseXapiAgentWithMboxSha1Sum raises a `ValidationError`. """ - + field = mock_xapi_instance(BaseXapiAgentWithMboxSha1Sum) invalid_field = json.loads(field.json()) diff --git a/tests/models/xapi/base/test_groups.py b/tests/models/xapi/base/test_groups.py index 6ae2edfa2..6a0a7bd31 100644 --- a/tests/models/xapi/base/test_groups.py +++ b/tests/models/xapi/base/test_groups.py @@ -5,8 +5,8 @@ # from tests.fixtures.hypothesis_strategies import custom_given from tests.factories import mock_xapi_instance -def test_models_xapi_base_groups_group_common_properties_with_valid_field( -): + +def test_models_xapi_base_groups_group_common_properties_with_valid_field(): """Test a valid BaseXapiGroupCommonProperties has the expected `objectType` value. """ diff --git a/tests/models/xapi/base/test_objects.py b/tests/models/xapi/base/test_objects.py index 1a1b833a6..39f02375a 100644 --- a/tests/models/xapi/base/test_objects.py +++ b/tests/models/xapi/base/test_objects.py @@ -5,6 +5,7 @@ # from tests.fixtures.hypothesis_strategies import custom_given from tests.factories import mock_xapi_instance + def test_models_xapi_object_base_sub_statement_type_with_valid_field(): """Test a valid BaseXapiSubStatement has the expected `objectType` value.""" field = mock_xapi_instance(BaseXapiSubStatement) diff --git a/tests/models/xapi/base/test_results.py b/tests/models/xapi/base/test_results.py index 249cdc907..4465e20f8 100644 --- a/tests/models/xapi/base/test_results.py +++ b/tests/models/xapi/base/test_results.py @@ -10,6 +10,7 @@ # from tests.fixtures.hypothesis_strategies import custom_given from tests.factories import mock_xapi_instance + @pytest.mark.parametrize( "raw_value, min_value, max_value, error_msg", [ diff --git a/tests/models/xapi/base/test_statements.py b/tests/models/xapi/base/test_statements.py index 0edbbb6cf..699979e95 100644 --- a/tests/models/xapi/base/test_statements.py +++ b/tests/models/xapi/base/test_statements.py @@ -3,6 +3,7 @@ import json import pytest + # from hypothesis import settings # from hypothesis import strategies as st # from polyfactory import Use @@ -27,8 +28,7 @@ from ralph.utils import set_dict_value_from_path # from tests.fixtures.hypothesis_strategies import custom_builds, custom_given - -from tests.factories import mock_xapi_instance, ModelFactory +from tests.factories import ModelFactory, mock_xapi_instance @pytest.mark.parametrize( @@ -71,7 +71,9 @@ def test_models_xapi_base_statement_with_valid_null_values(path, value): property. """ - statement = mock_xapi_instance(BaseXapiStatement, object=mock_xapi_instance(BaseXapiActivity)) + statement = mock_xapi_instance( + BaseXapiStatement, object=mock_xapi_instance(BaseXapiActivity) + ) statement = statement.dict(exclude_none=True) @@ -261,7 +263,9 @@ def test_models_xapi_base_statement_with_invalid_extensions(path): An Extension "key" is an IRI. The LRS rejects with 400 a statement which has an extension key which is not a valid IRI, if an extension object is present. """ - statement = mock_xapi_instance(BaseXapiStatement, object=mock_xapi_instance(BaseXapiActivity)) + statement = mock_xapi_instance( + BaseXapiStatement, object=mock_xapi_instance(BaseXapiActivity) + ) statement = statement.dict(exclude_none=True) set_dict_value_from_path(statement, path.split("__"), "") @@ -326,11 +330,15 @@ def test_models_xapi_base_statement_with_invalid_group_objects(klass): BaseXapiIdentifiedGroupWithAccount, ] ) - statement = mock_xapi_instance(BaseXapiStatement, actor=mock_xapi_instance(actor_class)) + statement = mock_xapi_instance( + BaseXapiStatement, actor=mock_xapi_instance(actor_class) + ) kwargs = {"exclude_none": True} statement = statement.dict(**kwargs) - statement["actor"]["member"] = [mock_xapi_instance(klass).dict(**kwargs)] # TODO: check that nothing was lost + statement["actor"]["member"] = [ + mock_xapi_instance(klass).dict(**kwargs) + ] # TODO: check that nothing was lost err = "actor -> member -> 0 -> objectType\n unexpected value; permitted: 'Agent'" with pytest.raises(ValidationError, match=err): BaseXapiStatement(**statement) @@ -427,8 +435,9 @@ def test_models_xapi_base_statement_with_invalid_context_value(path, value): BaseXapiStatementRef, ] ) - statement = mock_xapi_instance(BaseXapiStatement, object=mock_xapi_instance(object_class)) - + statement = mock_xapi_instance( + BaseXapiStatement, object=mock_xapi_instance(object_class) + ) statement = statement.dict(exclude_none=True) set_dict_value_from_path(statement, path.split("__"), value) @@ -513,14 +522,16 @@ def test_models_xapi_base_statement_with_valid_version(): list(ModelSelector("ralph.models.xapi").model_rules), ) def test_models_xapi_base_statement_should_consider_valid_all_defined_xapi_models( - model + model, ): """Test that all defined xAPI models in the ModelSelector make valid statements.""" - + # All specific xAPI models should inherit BaseXapiStatement assert issubclass(model, BaseXapiStatement) statement = mock_xapi_instance(model) - statement = statement.json(exclude_none=True, by_alias=True) # TODO: check that we are not losing info by mocking random model + statement = statement.json( + exclude_none=True, by_alias=True + ) # TODO: check that we are not losing info by mocking random model try: BaseXapiStatement(**json.loads(statement)) except ValidationError as err: diff --git a/tests/models/xapi/base/test_unnested_objects.py b/tests/models/xapi/base/test_unnested_objects.py index edd9152b6..61e16be29 100644 --- a/tests/models/xapi/base/test_unnested_objects.py +++ b/tests/models/xapi/base/test_unnested_objects.py @@ -22,8 +22,7 @@ def test_models_xapi_base_object_statement_ref_type_with_valid_field(): assert field.objectType == "StatementRef" -def test_models_xapi_base_object_interaction_component_with_valid_field( -): +def test_models_xapi_base_object_interaction_component_with_valid_field(): """Test a valid BaseXapiInteractionComponent has the expected `id` regex.""" field = mock_xapi_instance(BaseXapiInteractionComponent) assert re.match(r"^[^\s]+$", field.id) @@ -33,9 +32,7 @@ def test_models_xapi_base_object_interaction_component_with_valid_field( "id_value", [" test_id", "\ntest"], ) -def test_models_xapi_base_object_interaction_component_with_invalid_field( - id_value -): +def test_models_xapi_base_object_interaction_component_with_invalid_field(id_value): """Test an invalid `id` property in BaseXapiInteractionComponent raises a `ValidationError`. """ @@ -48,8 +45,7 @@ def test_models_xapi_base_object_interaction_component_with_invalid_field( BaseXapiInteractionComponent(**invalid_property) -def test_models_xapi_base_object_activity_type_interaction_definition_with_valid_field( -): +def test_models_xapi_base_object_activity_type_interaction_definition_with_valid_field(): """Test a valid BaseXapiActivityInteractionDefinition has the expected `objectType` value. """ diff --git a/tests/models/xapi/concepts/test_activity_types.py b/tests/models/xapi/concepts/test_activity_types.py index 05b38f3ae..784c1f2f6 100644 --- a/tests/models/xapi/concepts/test_activity_types.py +++ b/tests/models/xapi/concepts/test_activity_types.py @@ -2,8 +2,6 @@ import json import pytest -from hypothesis import settings -from hypothesis import strategies as st from ralph.models.xapi.concepts.activity_types.acrossx_profile import ( MessageActivity, @@ -29,6 +27,7 @@ # from tests.fixtures.hypothesis_strategies import custom_builds, custom_given from tests.factories import mock_xapi_instance + @pytest.mark.parametrize( "class_, definition_type", [ @@ -49,9 +48,7 @@ (DocumentActivity, "http://id.tincanapi.com/activitytype/document"), ], ) -def test_models_xapi_concept_activity_types_with_valid_field( - class_, definition_type -): +def test_models_xapi_concept_activity_types_with_valid_field(class_, definition_type): """Test that a valid xAPI activity has the expected the `definition`.`type` value. """ diff --git a/tests/models/xapi/concepts/test_verbs.py b/tests/models/xapi/concepts/test_verbs.py index 7cc947608..c344eda3b 100644 --- a/tests/models/xapi/concepts/test_verbs.py +++ b/tests/models/xapi/concepts/test_verbs.py @@ -2,8 +2,6 @@ import json import pytest -from hypothesis import settings -from hypothesis import strategies as st from ralph.models.xapi.concepts.verbs.acrossx_profile import PostedVerb from ralph.models.xapi.concepts.verbs.activity_streams_vocabulary import ( @@ -45,6 +43,7 @@ # from tests.fixtures.hypothesis_strategies import custom_builds, custom_given from tests.factories import mock_xapi_instance + @pytest.mark.parametrize( "class_, verb_id", [ diff --git a/tests/models/xapi/test_lms.py b/tests/models/xapi/test_lms.py index b4073f70c..c855d3796 100644 --- a/tests/models/xapi/test_lms.py +++ b/tests/models/xapi/test_lms.py @@ -3,8 +3,6 @@ import json import pytest -from hypothesis import settings -from hypothesis import strategies as st from pydantic import ValidationError from ralph.models.selector import ModelSelector @@ -27,6 +25,7 @@ # from tests.fixtures.hypothesis_strategies import custom_builds, custom_given from tests.factories import mock_xapi_instance + @pytest.mark.parametrize( "class_", [ @@ -210,9 +209,7 @@ def test_models_xapi_lms_uploaded_audio_with_valid_statement(): [{"id": "https://foo.bar"}, {"id": "https://w3id.org/xapi/lms"}], ], ) -def test_models_xapi_lms_context_context_activities_with_valid_category( - category -): +def test_models_xapi_lms_context_context_activities_with_valid_category(category): """Test that a valid `LMSContextContextActivities` should not raise a `ValidationError`. """ @@ -239,9 +236,7 @@ def test_models_xapi_lms_context_context_activities_with_valid_category( [{"id": "https://foo.bar"}, {"id": "https://w3id.org/xapi/not-lms"}], ], ) -def test_models_xapi_lms_context_context_activities_with_invalid_category( - category -): +def test_models_xapi_lms_context_context_activities_with_invalid_category(category): """Test that an invalid `LMSContextContextActivities` should raise a `ValidationError`. """ diff --git a/tests/models/xapi/test_navigation.py b/tests/models/xapi/test_navigation.py index e31698457..d660ebffa 100644 --- a/tests/models/xapi/test_navigation.py +++ b/tests/models/xapi/test_navigation.py @@ -3,8 +3,6 @@ import json import pytest -from hypothesis import settings -from hypothesis import strategies as st from ralph.models.selector import ModelSelector from ralph.models.xapi.navigation.statements import PageTerminated, PageViewed @@ -12,6 +10,7 @@ # from tests.fixtures.hypothesis_strategies import custom_builds, custom_given from tests.factories import mock_xapi_instance + @pytest.mark.parametrize("class_", [PageTerminated, PageViewed]) def test_models_xapi_navigation_selectors_with_valid_statements(class_): """Test given a valid navigation xAPI statement the `get_first_model` diff --git a/tests/models/xapi/test_video.py b/tests/models/xapi/test_video.py index 44951399a..cb71a869e 100644 --- a/tests/models/xapi/test_video.py +++ b/tests/models/xapi/test_video.py @@ -3,8 +3,6 @@ import json import pytest -from hypothesis import settings -from hypothesis import strategies as st from pydantic import ValidationError from ralph.models.selector import ModelSelector @@ -199,9 +197,7 @@ def test_models_xapi_video_screen_change_interaction_with_valid_statement(): [{"id": "https://foo.bar"}, {"id": "https://w3id.org/xapi/video"}], ], ) -def test_models_xapi_video_context_activities_with_valid_category( - category -): +def test_models_xapi_video_context_activities_with_valid_category(category): """Test that a valid `VideoContextContextActivities` should not raise a `ValidationError`. """ @@ -228,9 +224,7 @@ def test_models_xapi_video_context_activities_with_valid_category( [{"id": "https://foo.bar"}, {"id": "https://w3id.org/xapi/not-video"}], ], ) -def test_models_xapi_video_context_activities_with_invalid_category( - category -): +def test_models_xapi_video_context_activities_with_invalid_category(category): """Test that an invalid `VideoContextContextActivities` should raise a `ValidationError`. """ diff --git a/tests/models/xapi/test_virtual_classroom.py b/tests/models/xapi/test_virtual_classroom.py index 1665a1c0f..1a7e09c6e 100644 --- a/tests/models/xapi/test_virtual_classroom.py +++ b/tests/models/xapi/test_virtual_classroom.py @@ -3,8 +3,6 @@ import json import pytest -from hypothesis import settings -from hypothesis import strategies as st from pydantic import ValidationError from ralph.models.selector import ModelSelector @@ -32,6 +30,7 @@ # from tests.fixtures.hypothesis_strategies import custom_builds, custom_given from tests.factories import mock_xapi_instance + @pytest.mark.parametrize( "class_", [ @@ -246,9 +245,7 @@ def test_models_xapi_virtual_classroom_answered_poll_with_valid_statement(): ) -def test_models_xapi_virtual_classroom_posted_public_message_with_valid_statement( - -): +def test_models_xapi_virtual_classroom_posted_public_message_with_valid_statement(): statement = mock_xapi_instance(VirtualClassroomPostedPublicMessage) """Test that a virtual classroom posted public message statement has the expected `verb`.`id` and `object`.`definition`.`type` property values. @@ -272,9 +269,7 @@ def test_models_xapi_virtual_classroom_posted_public_message_with_valid_statemen [{"id": "https://foo.bar"}, {"id": "https://w3id.org/xapi/virtual-classroom"}], ], ) -def test_models_xapi_virtual_classroom_context_activities_with_valid_category( - category -): +def test_models_xapi_virtual_classroom_context_activities_with_valid_category(category): """Test that a valid `VirtualClassroomContextContextActivities` should not raise a `ValidationError`. """ @@ -306,7 +301,7 @@ def test_models_xapi_virtual_classroom_context_activities_with_valid_category( ], ) def test_models_xapi_virtual_classroom_context_activities_with_invalid_category( - category + category, ): """Test that an invalid `VirtualClassroomContextContextActivities` should raise a `ValidationError`. diff --git a/tests/test_cli.py b/tests/test_cli.py index 6ab832f86..38a78c663 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -9,7 +9,6 @@ from click.exceptions import BadParameter from click.testing import CliRunner from elasticsearch.helpers import bulk, scan -from hypothesis import settings as hypothesis_settings from pydantic import ValidationError from ralph import cli as cli_module @@ -27,6 +26,7 @@ from ralph.models.edx.navigational.statements import UIPageClose from ralph.models.xapi.navigation.statements import PageTerminated +from tests.factories import mock_instance from tests.fixtures.backends import ( ES_TEST_HOSTS, ES_TEST_INDEX, @@ -34,8 +34,6 @@ WS_TEST_PORT, ) -from tests.factories import mock_instance - test_logger = logging.getLogger("ralph") From c9b8dc6264fd9fdce52c3b5173fe7d701461c81b Mon Sep 17 00:00:00 2001 From: lleeoo Date: Wed, 13 Dec 2023 15:53:47 +0100 Subject: [PATCH 17/19] lint --- pyproject.toml | 1 - src/ralph/conf.py | 4 - .../edx/problem_interaction/fields/events.py | 4 +- src/ralph/models/xapi/base/common.py | 14 -- .../models/xapi/virtual_classroom/contexts.py | 1 - tests/api/test_forwarding.py | 1 - tests/conftest.py | 4 - tests/factories.py | 58 ++----- tests/fixtures/hypothesis_configuration.py | 25 --- tests/fixtures/hypothesis_strategies.py | 148 ------------------ .../edx/converters/xapi/test_enrollment.py | 2 - .../edx/converters/xapi/test_navigational.py | 1 - .../models/edx/converters/xapi/test_video.py | 1 - tests/models/edx/navigational/test_events.py | 1 - .../edx/navigational/test_statements.py | 1 - .../open_response_assessment/test_events.py | 1 - .../test_statements.py | 1 - .../edx/peer_instruction/test_events.py | 1 - .../edx/peer_instruction/test_statements.py | 1 - .../edx/problem_interaction/test_events.py | 1 - .../problem_interaction/test_statements.py | 1 - tests/models/edx/test_base.py | 1 - tests/models/edx/test_browser.py | 1 - tests/models/edx/test_enrollment.py | 1 - tests/models/edx/test_server.py | 1 - .../edx/textbook_interaction/test_events.py | 1 - .../textbook_interaction/test_statements.py | 1 - tests/models/edx/video/test_events.py | 1 - tests/models/edx/video/test_statements.py | 1 - tests/models/test_converter.py | 1 - tests/models/test_validator.py | 1 - tests/models/xapi/base/test_agents.py | 1 - tests/models/xapi/base/test_groups.py | 1 - tests/models/xapi/base/test_objects.py | 1 - tests/models/xapi/base/test_results.py | 1 - tests/models/xapi/base/test_statements.py | 5 - .../models/xapi/base/test_unnested_objects.py | 1 - .../xapi/concepts/test_activity_types.py | 1 - tests/models/xapi/concepts/test_verbs.py | 1 - tests/models/xapi/test_lms.py | 1 - tests/models/xapi/test_navigation.py | 1 - tests/models/xapi/test_video.py | 1 - tests/models/xapi/test_virtual_classroom.py | 1 - 43 files changed, 14 insertions(+), 284 deletions(-) delete mode 100644 tests/fixtures/hypothesis_configuration.py delete mode 100644 tests/fixtures/hypothesis_strategies.py diff --git a/pyproject.toml b/pyproject.toml index b82227c97..8dc427572 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,7 +86,6 @@ dev = [ "black==23.11.0", "cryptography==41.0.5", "factory-boy==3.3.0", - "hypothesis==6.88.3", "logging-gelf==0.0.31", "mike==2.0.0", "mkdocs==1.5.3", diff --git a/src/ralph/conf.py b/src/ralph/conf.py index 61e3f0307..999bf3f36 100644 --- a/src/ralph/conf.py +++ b/src/ralph/conf.py @@ -38,7 +38,6 @@ MODEL_PATH_SEPARATOR = "__" -from pydantic import constr NonEmptyStr = Annotated[str, Field(min_length=1)] NonEmptyStrictStrPatch = Annotated[str, Field(min_length=1)] @@ -139,9 +138,6 @@ class ParserSettings(BaseModel): class XapiForwardingConfigurationSettings(BaseModel): """Pydantic model for xAPI forwarding configuration item.""" - # class Config: # TODO: done - # min_anystr_length = 1 - url: AnyUrl is_active: bool basic_username: NonEmptyStr diff --git a/src/ralph/models/edx/problem_interaction/fields/events.py b/src/ralph/models/edx/problem_interaction/fields/events.py index 54d2abf49..c277eb2fd 100644 --- a/src/ralph/models/edx/problem_interaction/fields/events.py +++ b/src/ralph/models/edx/problem_interaction/fields/events.py @@ -171,7 +171,7 @@ class ProblemCheckEventField(AbstractBaseEventField): answers: Dict[ Annotated[str, Field(regex=r"^[a-f0-9]{32}_[0-9]_[0-9]$")], - Union[List[str], str], + Union[str, List[str]], ] attempts: int correct_map: Dict[ @@ -208,7 +208,7 @@ class ProblemCheckFailEventField(AbstractBaseEventField): answers: Dict[ Annotated[str, Field(regex=r"^[a-f0-9]{32}_[0-9]_[0-9]$")], - Union[List[str], str], + Union[str, List[str]], ] failure: Union[Literal["closed"], Literal["unreset"]] problem_id: Annotated[ diff --git a/src/ralph/models/xapi/base/common.py b/src/ralph/models/xapi/base/common.py index 738d39a31..305ccccc8 100644 --- a/src/ralph/models/xapi/base/common.py +++ b/src/ralph/models/xapi/base/common.py @@ -41,17 +41,3 @@ def validate(tag: str) -> Type["LanguageTag"]: email_pattern = r"(^mailto:[-!#-'*+/-9=?A-Z^-~]+(\.[-!#-'*+/-9=?A-Z^-~]+)*|\"([]!#-[^-~ \t]|(\\[\t -~]))+\")@([-!#-'*+/-9=?A-Z^-~]+(\.[-!#-'*+/-9=?A-Z^-~]+)*|\[[\t -Z^-~]*])" MailtoEmail = Annotated[str, Field(regex=email_pattern)] - -# class MailtoEmail(str): -# """Pydantic custom data type validating `mailto:email` format.""" - -# @classmethod -# def __get_validators__(cls) -> Generator: -# def validate(mailto: str) -> Type["MailtoEmail"]: -# """Check whether the provided value follows the `mailto:email` format.""" -# if not mailto.startswith("mailto:"): -# raise TypeError(f"Invalid `mailto:email` value: {str(mailto)}") -# valid = validate_email(mailto[7:]) -# return cls(f"mailto:{valid[1]}") - -# yield validate diff --git a/src/ralph/models/xapi/virtual_classroom/contexts.py b/src/ralph/models/xapi/virtual_classroom/contexts.py index e34fbf443..f6ebf2c08 100644 --- a/src/ralph/models/xapi/virtual_classroom/contexts.py +++ b/src/ralph/models/xapi/virtual_classroom/contexts.py @@ -45,7 +45,6 @@ class VirtualClassroomContextContextActivities(BaseXapiContextContextActivities) List[Union[VirtualClassroomProfileActivity, BaseXapiActivity]], ] - # 3 validation errors: problem not here # TODO: Remove this message @validator("category") @classmethod def check_presence_of_profile_activity_category( diff --git a/tests/api/test_forwarding.py b/tests/api/test_forwarding.py index 947057f9f..e65d14ae8 100644 --- a/tests/api/test_forwarding.py +++ b/tests/api/test_forwarding.py @@ -10,7 +10,6 @@ from ralph.api.forwarding import forward_xapi_statements, get_active_xapi_forwardings from ralph.conf import Settings, XapiForwardingConfigurationSettings -# from tests.fixtures.hypothesis_strategies import custom_builds, custom_given from tests.factories import mock_instance diff --git a/tests/conftest.py b/tests/conftest.py index 6b1050e95..5160a8de1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,10 +1,6 @@ """Module py.test fixtures.""" -from .fixtures import ( - hypothesis_configuration, # noqa: F401 - hypothesis_strategies, # noqa: F401 -) from .fixtures.api import client # noqa: F401 from .fixtures.auth import ( # noqa: F401 basic_auth_credentials, diff --git a/tests/factories.py b/tests/factories.py index b093abb03..de561cd48 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -60,13 +60,13 @@ def prune(d: Any, exceptions: list = []): class ModelFactory(PolyfactoryModelFactory[T]): __allow_none_optionals__ = False __is_base_factory__ = True - __random_seed__ = 6 # TODO: remove this + # __random_seed__ = 6 # TODO: remove this @classmethod def get_provider_map(cls) -> dict[Any, Callable[[], Any]]: provider_map = super().get_provider_map() provider_map[LanguageTag] = lambda: LanguageTag("en-US") - provider_map[IRI] = lambda: IRI("https://w3id.org/xapi/video/verbs/played") + provider_map[IRI] = lambda: IRI(mock_url()) return provider_map @classmethod @@ -87,29 +87,6 @@ class BaseXapiResultScoreFactory(ModelFactory[BaseXapiResultScore]): raw = Decimal("11") -# TODO: put back ? -# class BaseXapiActivityInteractionDefinitionFactory( -# ModelFactory[BaseXapiActivityInteractionDefinition] -# ): -# __set_as_default_factory_for_type__ = True -# __model__ = BaseXapiActivityInteractionDefinition - -# correctResponsesPattern = None - - -def myfunc(): - raise Exception("WHAT ARE YOU EVEN DOING") - - -# TODO: put back ? -# class BaseXapiContextContextActivitiesFactory( -# ModelFactory[BaseXapiContextContextActivities] -# ): -# __model__ = BaseXapiContextContextActivities - -# category = myfunc - - class BaseXapiContextFactory(ModelFactory[BaseXapiContext]): __model__ = BaseXapiContext __set_as_default_factory_for_type__ = True @@ -117,24 +94,19 @@ class BaseXapiContextFactory(ModelFactory[BaseXapiContext]): revision = Ignore() platform = Ignore() - # TODO: see why this was added - # contextActivities = ( - # lambda: BaseXapiContextContextActivitiesFactory.build() or Ignore() - # ) - class LMSContextContextActivitiesFactory(ModelFactory[LMSContextContextActivities]): __model__ = LMSContextContextActivities __set_as_default_factory_for_type__ = True - category = lambda: mock_xapi_instance(LMSProfileActivity) # TODO: Uncomment + category = lambda: mock_xapi_instance(LMSProfileActivity) # noqa: E731 class VideoContextContextActivitiesFactory(ModelFactory[VideoContextContextActivities]): __model__ = VideoContextContextActivities __set_as_default_factory_for_type__ = True - category = lambda: mock_xapi_instance(VideoProfileActivity) + category = lambda: mock_xapi_instance(VideoProfileActivity) # noqa: E731 class VirtualClassroomStartedPollContextActivitiesFactory( @@ -143,7 +115,7 @@ class VirtualClassroomStartedPollContextActivitiesFactory( __model__ = VirtualClassroomStartedPollContextActivities __set_as_default_factory_for_type__ = True - category = lambda: mock_xapi_instance(VirtualClassroomProfileActivity) + category = lambda: mock_xapi_instance(VirtualClassroomProfileActivity) # noqa: E731 class VirtualClassroomAnsweredPollContextActivitiesFactory( @@ -152,7 +124,7 @@ class VirtualClassroomAnsweredPollContextActivitiesFactory( __model__ = VirtualClassroomAnsweredPollContextActivities __set_as_default_factory_for_type__ = True - category = lambda: mock_xapi_instance(VirtualClassroomProfileActivity) + category = lambda: mock_xapi_instance(VirtualClassroomProfileActivity) # noqa: E731 class VirtualClassroomPostedPublicMessageContextActivitiesFactory( @@ -161,32 +133,25 @@ class VirtualClassroomPostedPublicMessageContextActivitiesFactory( __model__ = VirtualClassroomPostedPublicMessageContextActivities __set_as_default_factory_for_type__ = True - category = lambda: mock_xapi_instance(VirtualClassroomProfileActivity) - - -# class VirtualClassroomAnsweredPollFactory(ModelFactory[VirtualClassroomAnsweredPollContextActivities]): -# __model__ = VirtualClassroomAnsweredPollContextActivities - -# category = lambda: mock_xapi_instance(VirtualClassroomProfileActivity) -# parent = lambda: mock_xapi_instance(VirtualClassroomActivity) # TODO: Remove this. Check that this is valid + category = lambda: mock_xapi_instance(VirtualClassroomProfileActivity) # noqa: E731 class UISeqPrev(ModelFactory[UISeqPrev]): __model__ = UISeqPrev __set_as_default_factory_for_type__ = True - event = lambda: mock_instance(NavigationalEventField, old=1, new=0) + event = lambda: mock_instance(NavigationalEventField, old=1, new=0) # noqa: E731 class UISeqNext(ModelFactory[UISeqNext]): __model__ = UISeqNext __set_as_default_factory_for_type__ = True - event = lambda: mock_instance(NavigationalEventField, old=0, new=1) + event = lambda: mock_instance(NavigationalEventField, old=0, new=1) # noqa: E731 def mock_xapi_instance(klass, *args, **kwargs): - """Generate a mock instance of a given class (`klass`).""" + """Generate a mock instance of a given xAPI model.""" # Avoid redifining custom factories if klass not in BaseFactory._factory_type_mapping: @@ -205,7 +170,7 @@ class KlassFactory(ModelFactory[klass]): def mock_instance(klass, *args, **kwargs): - """Generate a mock instance of a given class (`klass`).""" + """Generate a mock instance of a given model.""" # Avoid redifining custom factories if klass not in BaseFactory._factory_type_mapping: @@ -222,4 +187,5 @@ class KlassFactory(ModelFactory[klass]): def mock_url(): + """Mock a URL.""" return ModelFactory.__faker__.url() diff --git a/tests/fixtures/hypothesis_configuration.py b/tests/fixtures/hypothesis_configuration.py deleted file mode 100644 index 65c4020c5..000000000 --- a/tests/fixtures/hypothesis_configuration.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Hypothesis fixture configuration.""" - - -from hypothesis import provisional, settings -from hypothesis import strategies as st -from pydantic import AnyHttpUrl, AnyUrl, StrictStr - -from ralph.models.xapi.base.common import IRI, LanguageTag - -settings.register_profile("development", max_examples=1) -settings.load_profile("development") - -# from ralph.conf import NonEmptyStr, NonEmptyStrictStr -# st.register_type_strategy(NonEmptyStr, st.text(min_size=1)) -# st.register_type_strategy(NonEmptyStrictStr, st.text(min_size=1)) - -st.register_type_strategy(str, st.text(min_size=1)) -st.register_type_strategy(StrictStr, st.text(min_size=1)) -st.register_type_strategy(AnyUrl, provisional.urls()) -st.register_type_strategy(AnyHttpUrl, provisional.urls()) -st.register_type_strategy(IRI, provisional.urls()) -# st.register_type_strategy( -# MailtoEmail, st.builds(operator.add, st.just("mailto:"), st.emails()) -# ) -st.register_type_strategy(LanguageTag, st.just("en-US")) diff --git a/tests/fixtures/hypothesis_strategies.py b/tests/fixtures/hypothesis_strategies.py deleted file mode 100644 index c201c60dc..000000000 --- a/tests/fixtures/hypothesis_strategies.py +++ /dev/null @@ -1,148 +0,0 @@ -# """Hypothesis build strategies with special constraints.""" - -# import random -# from typing import Union - - -# from pydantic import BaseModel - -# from ralph.models.edx.navigational.fields.events import NavigationalEventField -# from ralph.models.edx.navigational.statements import UISeqNext, UISeqPrev -# from ralph.models.xapi.base.contexts import BaseXapiContext -# from ralph.models.xapi.base.results import BaseXapiResultScore -# from ralph.models.xapi.lms.contexts import ( -# LMSContextContextActivities, -# LMSProfileActivity, -# ) -# from ralph.models.xapi.video.contexts import ( -# VideoContextContextActivities, -# VideoProfileActivity, -# ) -# from ralph.models.xapi.virtual_classroom.contexts import ( -# VirtualClassroomContextContextActivities, -# VirtualClassroomProfileActivity, -# ) - -# OVERWRITTEN_STRATEGIES = {} - - -# def is_base_model(klass): -# """Return True if the given class is a subclass of the pydantic BaseModel.""" - -# try: -# return issubclass(klass, BaseModel) -# except TypeError: -# return False - - -# def get_strategy_from(annotation): -# """Infer a Hypothesis strategy from the given annotation.""" -# origin = getattr(annotation, "__origin__", None) -# args = getattr(annotation, "__args__", None) -# if is_base_model(annotation): -# return custom_builds(annotation) -# if origin is Union: -# return st.one_of( -# [get_strategy_from(t) for t in args if not isinstance(t, type(None))] -# ) -# if origin is list: -# return st.lists(get_strategy_from(args[0]), min_size=1) -# if origin is dict: -# keys = get_strategy_from(args[0]) -# values = get_strategy_from(args[1]) -# return st.dictionaries(keys, values, min_size=1) -# if annotation is None: -# return st.none() -# return st.from_type(annotation) - - -# def custom_builds( -# klass: BaseModel, _overwrite_default=True, **kwargs: Union[st.SearchStrategy, bool] -# ): -# """Return a fixed_dictionaries Hypothesis strategy for pydantic models. - -# Args: -# klass (BaseModel): The pydantic model for which to generate a strategy. -# _overwrite_default (bool): By default, fields overwritten by kwargs become -# required. If _overwrite_default is set to False, we keep the original field -# requirement (either required or optional). -# **kwargs (SearchStrategy or bool): If kwargs contain search strategies, they -# overwrite the default strategy for the given key. -# If kwargs contains booleans, they set whether the given key should be -# present (True) or omitted (False) in the generated model. -# """ - -# for special_class, special_kwargs in OVERWRITTEN_STRATEGIES.items(): -# if issubclass(klass, special_class): -# kwargs = dict(special_kwargs, **kwargs) -# break -# optional = {} -# required = {} -# for name, field in klass.__fields__.items(): -# arg = kwargs.get(name, None) -# if arg is False: -# continue -# is_required = field.required or (arg is not None and _overwrite_default) -# required_optional = required if is_required or arg is not None else optional -# field_strategy = get_strategy_from(field.outer_type_) if arg is None else arg -# required_optional[field.alias] = field_strategy -# if not required: -# # To avoid generating empty values -# key, value = random.choice(list(optional.items())) -# required[key] = value -# del optional[key] -# return st.fixed_dictionaries(required, optional=optional).map(klass.parse_obj) - -# def custom_given(*args: Union[BaseModel], **kwargs): -# """Wrap the Hypothesis `given` function. Replace st.builds with custom_builds.""" -# strategies = [] -# for arg in args: -# strategies.append(custom_builds(arg) if is_base_model(arg) else arg) -# return given(*strategies, **kwargs) - - -# def custom_given(model: BaseModel, **kwargs): - -# if issubclass(model, BaseXapiStatement): -# func = mock_xapi_instance -# else: -# func = mock_instance -# return given(func(model, **kwargs)) - -# def custom_given(model, **mock_kwargs): -# def decorator(function): - -# def new_function(*args, **kwargs): - -# if issubclass(model, BaseXapiStatement): -# instance = mock_xapi_instance(**mock_kwargs) -# else: -# instance = mock_instance(**mock_kwargs) - -# return function(instance, *args, **kwargs) -# return new_function -# return decorator - - -# OVERWRITTEN_STRATEGIES = { -# UISeqPrev: { -# "event": custom_builds(NavigationalEventField, old=st.just(1), new=st.just(0)) -# }, -# UISeqNext: { -# "event": custom_builds(NavigationalEventField, old=st.just(0), new=st.just(1)) -# }, -# BaseXapiContext: { -# "revision": False, -# "platform": False, -# }, -# BaseXapiResultScore: { -# "raw": False, -# "min": False, -# "max": False, -# }, -# LMSContextContextActivities: {"category": custom_builds(LMSProfileActivity)}, -# VideoContextContextActivities: {"category": custom_builds(VideoProfileActivity)}, -# VirtualClassroomContextContextActivities: { -# "category": custom_builds(VirtualClassroomProfileActivity) -# }, -# } diff --git a/tests/models/edx/converters/xapi/test_enrollment.py b/tests/models/edx/converters/xapi/test_enrollment.py index 87578b79b..f4c370a05 100644 --- a/tests/models/edx/converters/xapi/test_enrollment.py +++ b/tests/models/edx/converters/xapi/test_enrollment.py @@ -15,11 +15,9 @@ EdxCourseEnrollmentDeactivated, ) -# from tests.fixtures.hypothesis_strategies import custom_given from tests.factories import mock_instance, mock_url -# @custom_given(EdxCourseEnrollmentActivated, provisional.urls()) @pytest.mark.parametrize("uuid_namespace", ["ee241f8b-174f-5bdb-bae9-c09de5fe017f"]) def test_models_edx_converters_xapi_enrollment_edx_course_enrollment_activated_to_lms_registered_course( # noqa: E501 uuid_namespace, # , event, platform_url diff --git a/tests/models/edx/converters/xapi/test_navigational.py b/tests/models/edx/converters/xapi/test_navigational.py index e9f0660e2..14d87f490 100644 --- a/tests/models/edx/converters/xapi/test_navigational.py +++ b/tests/models/edx/converters/xapi/test_navigational.py @@ -9,7 +9,6 @@ from ralph.models.edx.converters.xapi.navigational import UIPageCloseToPageTerminated from ralph.models.edx.navigational.statements import UIPageClose -# from tests.fixtures.hypothesis_strategies import custom_given from tests.factories import mock_instance, mock_url diff --git a/tests/models/edx/converters/xapi/test_video.py b/tests/models/edx/converters/xapi/test_video.py index 63dcf498d..11e2fd6eb 100644 --- a/tests/models/edx/converters/xapi/test_video.py +++ b/tests/models/edx/converters/xapi/test_video.py @@ -21,7 +21,6 @@ UIStopVideo, ) -# from tests.fixtures.hypothesis_strategies import custom_given from tests.factories import mock_instance, mock_url diff --git a/tests/models/edx/navigational/test_events.py b/tests/models/edx/navigational/test_events.py index 57dc3db01..6ec8b8fbe 100644 --- a/tests/models/edx/navigational/test_events.py +++ b/tests/models/edx/navigational/test_events.py @@ -8,7 +8,6 @@ from ralph.models.edx.navigational.fields.events import NavigationalEventField -# from tests.fixtures.hypothesis_strategies import custom_given from tests.factories import mock_instance diff --git a/tests/models/edx/navigational/test_statements.py b/tests/models/edx/navigational/test_statements.py index 16694b3e9..675aae887 100644 --- a/tests/models/edx/navigational/test_statements.py +++ b/tests/models/edx/navigational/test_statements.py @@ -15,7 +15,6 @@ ) from ralph.models.selector import ModelSelector -# from tests.fixtures.hypothesis_strategies import custom_builds, custom_given from tests.factories import mock_instance diff --git a/tests/models/edx/open_response_assessment/test_events.py b/tests/models/edx/open_response_assessment/test_events.py index dbd14ee92..61641aa52 100644 --- a/tests/models/edx/open_response_assessment/test_events.py +++ b/tests/models/edx/open_response_assessment/test_events.py @@ -13,7 +13,6 @@ ORAGetSubmissionForStaffGradingEventField, ) -# from tests.fixtures.hypothesis_strategies import custom_given from tests.factories import mock_instance diff --git a/tests/models/edx/open_response_assessment/test_statements.py b/tests/models/edx/open_response_assessment/test_statements.py index 8e6a5a7b6..ed308f948 100644 --- a/tests/models/edx/open_response_assessment/test_statements.py +++ b/tests/models/edx/open_response_assessment/test_statements.py @@ -18,7 +18,6 @@ ) from ralph.models.selector import ModelSelector -# from tests.fixtures.hypothesis_strategies import custom_builds, custom_given from tests.factories import mock_instance diff --git a/tests/models/edx/peer_instruction/test_events.py b/tests/models/edx/peer_instruction/test_events.py index 2b44339f3..b828a29dd 100644 --- a/tests/models/edx/peer_instruction/test_events.py +++ b/tests/models/edx/peer_instruction/test_events.py @@ -7,7 +7,6 @@ from ralph.models.edx.peer_instruction.fields.events import PeerInstructionEventField -# from tests.fixtures.hypothesis_strategies import custom_given from tests.factories import mock_instance diff --git a/tests/models/edx/peer_instruction/test_statements.py b/tests/models/edx/peer_instruction/test_statements.py index 2fca67dfd..f4d01ce9c 100644 --- a/tests/models/edx/peer_instruction/test_statements.py +++ b/tests/models/edx/peer_instruction/test_statements.py @@ -11,7 +11,6 @@ ) from ralph.models.selector import ModelSelector -# from tests.fixtures.hypothesis_strategies import custom_builds, custom_given from tests.factories import mock_instance diff --git a/tests/models/edx/problem_interaction/test_events.py b/tests/models/edx/problem_interaction/test_events.py index 71f937c6b..e3abd1954 100644 --- a/tests/models/edx/problem_interaction/test_events.py +++ b/tests/models/edx/problem_interaction/test_events.py @@ -19,7 +19,6 @@ SaveProblemSuccessEventField, ) -# from tests.fixtures.hypothesis_strategies import custom_given from tests.factories import mock_instance diff --git a/tests/models/edx/problem_interaction/test_statements.py b/tests/models/edx/problem_interaction/test_statements.py index fdd69767e..26b119adc 100644 --- a/tests/models/edx/problem_interaction/test_statements.py +++ b/tests/models/edx/problem_interaction/test_statements.py @@ -24,7 +24,6 @@ ) from ralph.models.selector import ModelSelector -# from tests.fixtures.hypothesis_strategies import custom_builds, custom_given from tests.factories import mock_instance diff --git a/tests/models/edx/test_base.py b/tests/models/edx/test_base.py index fc3234cac..108284f81 100644 --- a/tests/models/edx/test_base.py +++ b/tests/models/edx/test_base.py @@ -8,7 +8,6 @@ from ralph.models.edx.base import BaseEdxModel -# from tests.fixtures.hypothesis_strategies import custom_given from tests.factories import mock_instance diff --git a/tests/models/edx/test_browser.py b/tests/models/edx/test_browser.py index e34bb4daf..856b5d98b 100644 --- a/tests/models/edx/test_browser.py +++ b/tests/models/edx/test_browser.py @@ -8,7 +8,6 @@ from ralph.models.edx.browser import BaseBrowserModel -# from tests.fixtures.hypothesis_strategies import custom_given from tests.factories import mock_instance diff --git a/tests/models/edx/test_enrollment.py b/tests/models/edx/test_enrollment.py index 8a688e73d..0df3baa92 100644 --- a/tests/models/edx/test_enrollment.py +++ b/tests/models/edx/test_enrollment.py @@ -13,7 +13,6 @@ ) from ralph.models.selector import ModelSelector -# from tests.fixtures.hypothesis_strategies import custom_builds, custom_given from tests.factories import mock_instance diff --git a/tests/models/edx/test_server.py b/tests/models/edx/test_server.py index 6c79bbf9c..ffee331f3 100644 --- a/tests/models/edx/test_server.py +++ b/tests/models/edx/test_server.py @@ -8,7 +8,6 @@ from ralph.models.edx.server import Server from ralph.models.selector import ModelSelector -# from tests.fixtures.hypothesis_strategies import custom_given from tests.factories import mock_instance diff --git a/tests/models/edx/textbook_interaction/test_events.py b/tests/models/edx/textbook_interaction/test_events.py index d218c816e..e1a03914f 100644 --- a/tests/models/edx/textbook_interaction/test_events.py +++ b/tests/models/edx/textbook_interaction/test_events.py @@ -11,7 +11,6 @@ TextbookPdfChapterNavigatedEventField, ) -# from tests.fixtures.hypothesis_strategies import custom_given from tests.factories import mock_instance diff --git a/tests/models/edx/textbook_interaction/test_statements.py b/tests/models/edx/textbook_interaction/test_statements.py index f20dc1c6f..6b522cba9 100644 --- a/tests/models/edx/textbook_interaction/test_statements.py +++ b/tests/models/edx/textbook_interaction/test_statements.py @@ -22,7 +22,6 @@ ) from ralph.models.selector import ModelSelector -# from tests.fixtures.hypothesis_strategies import custom_builds, custom_given from tests.factories import mock_instance diff --git a/tests/models/edx/video/test_events.py b/tests/models/edx/video/test_events.py index a6cc7f89c..b7b0bc830 100644 --- a/tests/models/edx/video/test_events.py +++ b/tests/models/edx/video/test_events.py @@ -7,7 +7,6 @@ from ralph.models.edx.video.fields.events import SpeedChangeVideoEventField -# from tests.fixtures.hypothesis_strategies import custom_given from tests.factories import mock_instance diff --git a/tests/models/edx/video/test_statements.py b/tests/models/edx/video/test_statements.py index a578704f2..4dc3694b1 100644 --- a/tests/models/edx/video/test_statements.py +++ b/tests/models/edx/video/test_statements.py @@ -18,7 +18,6 @@ ) from ralph.models.selector import ModelSelector -# from tests.fixtures.hypothesis_strategies import custom_builds, custom_given from tests.factories import mock_instance diff --git a/tests/models/test_converter.py b/tests/models/test_converter.py index ebe9d6b4b..829ac431f 100644 --- a/tests/models/test_converter.py +++ b/tests/models/test_converter.py @@ -23,7 +23,6 @@ from ralph.models.edx.converters.xapi.base import BaseConversionSet from ralph.models.edx.navigational.statements import UIPageClose -# from tests.fixtures.hypothesis_strategies import custom_given from tests.factories import mock_instance diff --git a/tests/models/test_validator.py b/tests/models/test_validator.py index 800e0452c..42a130b05 100644 --- a/tests/models/test_validator.py +++ b/tests/models/test_validator.py @@ -13,7 +13,6 @@ from ralph.models.selector import ModelSelector from ralph.models.validator import Validator -# from tests.fixtures.hypothesis_strategies import custom_given from tests.factories import mock_instance diff --git a/tests/models/xapi/base/test_agents.py b/tests/models/xapi/base/test_agents.py index c4df32814..a4acbc579 100644 --- a/tests/models/xapi/base/test_agents.py +++ b/tests/models/xapi/base/test_agents.py @@ -8,7 +8,6 @@ from ralph.models.xapi.base.agents import BaseXapiAgentWithMboxSha1Sum -# from tests.fixtures.hypothesis_strategies import custom_given from tests.factories import mock_xapi_instance diff --git a/tests/models/xapi/base/test_groups.py b/tests/models/xapi/base/test_groups.py index 6a0a7bd31..9bb41bbff 100644 --- a/tests/models/xapi/base/test_groups.py +++ b/tests/models/xapi/base/test_groups.py @@ -2,7 +2,6 @@ from ralph.models.xapi.base.groups import BaseXapiGroupCommonProperties -# from tests.fixtures.hypothesis_strategies import custom_given from tests.factories import mock_xapi_instance diff --git a/tests/models/xapi/base/test_objects.py b/tests/models/xapi/base/test_objects.py index 39f02375a..1cd225b3b 100644 --- a/tests/models/xapi/base/test_objects.py +++ b/tests/models/xapi/base/test_objects.py @@ -2,7 +2,6 @@ from ralph.models.xapi.base.objects import BaseXapiSubStatement -# from tests.fixtures.hypothesis_strategies import custom_given from tests.factories import mock_xapi_instance diff --git a/tests/models/xapi/base/test_results.py b/tests/models/xapi/base/test_results.py index 4465e20f8..5e796704b 100644 --- a/tests/models/xapi/base/test_results.py +++ b/tests/models/xapi/base/test_results.py @@ -7,7 +7,6 @@ from ralph.models.xapi.base.results import BaseXapiResultScore -# from tests.fixtures.hypothesis_strategies import custom_given from tests.factories import mock_xapi_instance diff --git a/tests/models/xapi/base/test_statements.py b/tests/models/xapi/base/test_statements.py index 699979e95..d1d98e57a 100644 --- a/tests/models/xapi/base/test_statements.py +++ b/tests/models/xapi/base/test_statements.py @@ -3,10 +3,6 @@ import json import pytest - -# from hypothesis import settings -# from hypothesis import strategies as st -# from polyfactory import Use from pydantic import ValidationError from ralph.models.selector import ModelSelector @@ -27,7 +23,6 @@ ) from ralph.utils import set_dict_value_from_path -# from tests.fixtures.hypothesis_strategies import custom_builds, custom_given from tests.factories import ModelFactory, mock_xapi_instance diff --git a/tests/models/xapi/base/test_unnested_objects.py b/tests/models/xapi/base/test_unnested_objects.py index 61e16be29..164fa41b0 100644 --- a/tests/models/xapi/base/test_unnested_objects.py +++ b/tests/models/xapi/base/test_unnested_objects.py @@ -12,7 +12,6 @@ BaseXapiStatementRef, ) -# from tests.fixtures.hypothesis_strategies import custom_given from tests.factories import mock_xapi_instance diff --git a/tests/models/xapi/concepts/test_activity_types.py b/tests/models/xapi/concepts/test_activity_types.py index 784c1f2f6..305f19881 100644 --- a/tests/models/xapi/concepts/test_activity_types.py +++ b/tests/models/xapi/concepts/test_activity_types.py @@ -24,7 +24,6 @@ VirtualClassroomActivity, ) -# from tests.fixtures.hypothesis_strategies import custom_builds, custom_given from tests.factories import mock_xapi_instance diff --git a/tests/models/xapi/concepts/test_verbs.py b/tests/models/xapi/concepts/test_verbs.py index c344eda3b..3d3fbf844 100644 --- a/tests/models/xapi/concepts/test_verbs.py +++ b/tests/models/xapi/concepts/test_verbs.py @@ -40,7 +40,6 @@ UnsharedScreenVerb, ) -# from tests.fixtures.hypothesis_strategies import custom_builds, custom_given from tests.factories import mock_xapi_instance diff --git a/tests/models/xapi/test_lms.py b/tests/models/xapi/test_lms.py index c855d3796..e03aed070 100644 --- a/tests/models/xapi/test_lms.py +++ b/tests/models/xapi/test_lms.py @@ -22,7 +22,6 @@ LMSUploadedVideo, ) -# from tests.fixtures.hypothesis_strategies import custom_builds, custom_given from tests.factories import mock_xapi_instance diff --git a/tests/models/xapi/test_navigation.py b/tests/models/xapi/test_navigation.py index d660ebffa..3244c1430 100644 --- a/tests/models/xapi/test_navigation.py +++ b/tests/models/xapi/test_navigation.py @@ -7,7 +7,6 @@ from ralph.models.selector import ModelSelector from ralph.models.xapi.navigation.statements import PageTerminated, PageViewed -# from tests.fixtures.hypothesis_strategies import custom_builds, custom_given from tests.factories import mock_xapi_instance diff --git a/tests/models/xapi/test_video.py b/tests/models/xapi/test_video.py index cb71a869e..5a1f2f074 100644 --- a/tests/models/xapi/test_video.py +++ b/tests/models/xapi/test_video.py @@ -20,7 +20,6 @@ VideoVolumeChangeInteraction, ) -# from tests.fixtures.hypothesis_strategies import custom_builds, custom_given from tests.factories import mock_xapi_instance diff --git a/tests/models/xapi/test_virtual_classroom.py b/tests/models/xapi/test_virtual_classroom.py index 1a7e09c6e..adf27bde2 100644 --- a/tests/models/xapi/test_virtual_classroom.py +++ b/tests/models/xapi/test_virtual_classroom.py @@ -27,7 +27,6 @@ VirtualClassroomUnsharedScreen, ) -# from tests.fixtures.hypothesis_strategies import custom_builds, custom_given from tests.factories import mock_xapi_instance From 5cc765045668591aac9a5f8af6457a5225f62ce7 Mon Sep 17 00:00:00 2001 From: lleeoo Date: Wed, 13 Dec 2023 18:58:30 +0100 Subject: [PATCH 18/19] wip --- src/ralph/models/edx/enrollment/statements.py | 21 ++++++++------ src/ralph/models/edx/server.py | 5 ++-- tests/factories.py | 28 +++++++++++++++++-- tests/models/edx/test_enrollment.py | 3 -- 4 files changed, 40 insertions(+), 17 deletions(-) diff --git a/src/ralph/models/edx/enrollment/statements.py b/src/ralph/models/edx/enrollment/statements.py index a381eecff..63da42853 100644 --- a/src/ralph/models/edx/enrollment/statements.py +++ b/src/ralph/models/edx/enrollment/statements.py @@ -35,7 +35,6 @@ class EdxCourseEnrollmentActivated(BaseServerModel): __selector__ = selector( event_source="server", event_type="edx.course.enrollment.activated" ) - event: Union[ Json[EnrollmentEventField], EnrollmentEventField, @@ -59,10 +58,12 @@ class EdxCourseEnrollmentDeactivated(BaseServerModel): event_source="server", event_type="edx.course.enrollment.deactivated" ) - event: Union[ - Json[EnrollmentEventField], - EnrollmentEventField, - ] + + event: Json[EnrollmentEventField] + # event: Union[ + # Json[EnrollmentEventField], + # EnrollmentEventField, + # ] event_type: Literal["edx.course.enrollment.deactivated"] name: Literal["edx.course.enrollment.deactivated"] @@ -83,10 +84,12 @@ class EdxCourseEnrollmentModeChanged(BaseServerModel): event_source="server", event_type="edx.course.enrollment.mode_changed" ) - event: Union[ - Json[EnrollmentEventField], - EnrollmentEventField, - ] + + event: Json[EnrollmentEventField] + # event: Union[ + # EnrollmentEventField, + # Json[EnrollmentEventField], + # ] event_type: Literal["edx.course.enrollment.mode_changed"] name: Literal["edx.course.enrollment.mode_changed"] diff --git a/src/ralph/models/edx/server.py b/src/ralph/models/edx/server.py index 095a1bdad..dd330892e 100644 --- a/src/ralph/models/edx/server.py +++ b/src/ralph/models/edx/server.py @@ -4,14 +4,14 @@ from pathlib import Path from typing import Union -from pydantic import Json +from pydantic import Json, validator from ralph.models.selector import LazyModelField, selector from .base import AbstractBaseEventField, BaseEdxModel if sys.version_info >= (3, 8): - from typing import Literal + from typing import Any, Literal else: from typing_extensions import Literal @@ -22,6 +22,7 @@ class BaseServerModel(BaseEdxModel): event_source: Literal["server"] + class ServerEventField(AbstractBaseEventField): """Pydantic model for common server `event` field.""" diff --git a/tests/factories.py b/tests/factories.py index de561cd48..60df509b6 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -52,7 +52,9 @@ def prune(d: Any, exceptions: list = []): elif isinstance(d, list): d_list = [prune(v) for v in d] return [v for v in d_list if v] - if d not in [[], {}, ""]: + # if d not in [[], {}, ""]: # TODO: put back ? + # return d + if d: return d return False @@ -168,6 +170,14 @@ class KlassFactory(ModelFactory[klass]): return klass(**kwargs) +def _call_all_callables(value): + if callable(value): + return value() + if isinstance(value, dict): + return {_call_all_callables(k):_call_all_callables(v) for k, v in value.items()} + elif isinstance(value, list): + return [_call_all_callables(v) for v in value] + return value def mock_instance(klass, *args, **kwargs): """Generate a mock instance of a given model.""" @@ -181,9 +191,21 @@ class KlassFactory(ModelFactory[klass]): else: KlassFactory = BaseFactory._factory_type_mapping[klass] - kwargs = KlassFactory.process_kwargs(*args, **kwargs) + return KlassFactory.build() + + # kwargs = KlassFactory.process_kwargs(*args, **kwargs) + + # print('OKAY ONE') + # from pprint import pprint + # pprint(kwargs) + # kwargs = _call_all_callables(kwargs) + + # print('OKAY TWO') + # from pprint import pprint + # pprint(kwargs) + + # return klass(**kwargs) - return klass(**kwargs) def mock_url(): diff --git a/tests/models/edx/test_enrollment.py b/tests/models/edx/test_enrollment.py index 0df3baa92..3cac1973a 100644 --- a/tests/models/edx/test_enrollment.py +++ b/tests/models/edx/test_enrollment.py @@ -31,7 +31,6 @@ def test_models_edx_edx_course_enrollment_selectors_with_valid_statements(class_ selector method should return the expected model. """ statement = json.loads(mock_instance(class_).json()) - # statement = json.loads(data.draw(custom_builds(class_)).json()) model = ModelSelector(module="ralph.models.edx").get_first_model(statement) assert model is class_ @@ -60,7 +59,6 @@ def test_models_edx_edx_course_enrollment_deactivated_with_valid_statement( assert statement.name == "edx.course.enrollment.deactivated" -# @custom_given(EdxCourseEnrollmentModeChanged) def test_models_edx_edx_course_enrollment_mode_changed_with_valid_statement( # statement, ): @@ -72,7 +70,6 @@ def test_models_edx_edx_course_enrollment_mode_changed_with_valid_statement( assert statement.name == "edx.course.enrollment.mode_changed" -# @custom_given(UIEdxCourseEnrollmentUpgradeClicked) def test_models_edx_ui_edx_course_enrollment_upgrade_clicked_with_valid_statement( # statement, ): From 2ef53485c8a87a8643bcdfcde03866f4beda0d50 Mon Sep 17 00:00:00 2001 From: lleeoo Date: Wed, 13 Dec 2023 19:00:18 +0100 Subject: [PATCH 19/19] wip --- src/ralph/models/edx/enrollment/statements.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/ralph/models/edx/enrollment/statements.py b/src/ralph/models/edx/enrollment/statements.py index 63da42853..753a88221 100644 --- a/src/ralph/models/edx/enrollment/statements.py +++ b/src/ralph/models/edx/enrollment/statements.py @@ -59,11 +59,10 @@ class EdxCourseEnrollmentDeactivated(BaseServerModel): ) - event: Json[EnrollmentEventField] - # event: Union[ - # Json[EnrollmentEventField], - # EnrollmentEventField, - # ] + event: Union[ + Json[EnrollmentEventField], + EnrollmentEventField, + ] event_type: Literal["edx.course.enrollment.deactivated"] name: Literal["edx.course.enrollment.deactivated"] @@ -85,11 +84,10 @@ class EdxCourseEnrollmentModeChanged(BaseServerModel): ) - event: Json[EnrollmentEventField] - # event: Union[ - # EnrollmentEventField, - # Json[EnrollmentEventField], - # ] + event: Union[ + EnrollmentEventField, + Json[EnrollmentEventField], + ] event_type: Literal["edx.course.enrollment.mode_changed"] name: Literal["edx.course.enrollment.mode_changed"]