diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index c38ca5619..0d8bf7a73 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -6172,6 +6172,24 @@ - [ ] update_workteam +## scheduler +
+100% implemented + +- [X] create_schedule +- [X] create_schedule_group +- [X] delete_schedule +- [X] delete_schedule_group +- [X] get_schedule +- [X] get_schedule_group +- [X] list_schedule_groups +- [X] list_schedules +- [X] list_tags_for_resource +- [X] tag_resource +- [X] untag_resource +- [X] update_schedule +
+ ## sdb
50% implemented @@ -7081,7 +7099,6 @@ - sagemaker-metrics - sagemaker-runtime - savingsplans -- scheduler - schemas - securityhub - securitylake diff --git a/docs/docs/services/scheduler.rst b/docs/docs/services/scheduler.rst new file mode 100644 index 000000000..74fe1433f --- /dev/null +++ b/docs/docs/services/scheduler.rst @@ -0,0 +1,58 @@ +.. _implementedservice_scheduler: + +.. |start-h3| raw:: html + +

+ +.. |end-h3| raw:: html + +

+ +========= +scheduler +========= + +.. autoclass:: moto.scheduler.models.EventBridgeSchedulerBackend + +|start-h3| Example usage |end-h3| + +.. sourcecode:: python + + @mock_scheduler + def test_scheduler_behaviour: + boto3.client("scheduler") + ... + + + +|start-h3| Implemented features for this service |end-h3| + +- [X] create_schedule + + The ClientToken parameter is not yet implemented + + +- [X] create_schedule_group + + The ClientToken parameter is not yet implemented + + +- [X] delete_schedule +- [X] delete_schedule_group +- [X] get_schedule +- [X] get_schedule_group +- [X] list_schedule_groups + + The MaxResults-parameter and pagination options are not yet implemented + + +- [ ] list_schedules +- [X] list_tags_for_resource +- [X] tag_resource +- [X] untag_resource +- [X] update_schedule + + The ClientToken is not yet implemented + + + diff --git a/moto/__init__.py b/moto/__init__.py index 2ab5615e5..682906ec6 100644 --- a/moto/__init__.py +++ b/moto/__init__.py @@ -143,6 +143,7 @@ mock_route53resolver = lazy_load( mock_s3 = lazy_load(".s3", "mock_s3") mock_s3control = lazy_load(".s3control", "mock_s3control") mock_sagemaker = lazy_load(".sagemaker", "mock_sagemaker") +mock_scheduler = lazy_load(".scheduler", "mock_scheduler") mock_sdb = lazy_load(".sdb", "mock_sdb") mock_secretsmanager = lazy_load(".secretsmanager", "mock_secretsmanager") mock_servicequotas = lazy_load( diff --git a/moto/backend_index.py b/moto/backend_index.py index 20cee8a4b..623777c19 100644 --- a/moto/backend_index.py +++ b/moto/backend_index.py @@ -148,6 +148,7 @@ backend_url_patterns = [ re.compile("https?://([0-9]+)\\.s3-control\\.(.+)\\.amazonaws\\.com"), ), ("sagemaker", re.compile("https?://api\\.sagemaker\\.(.+)\\.amazonaws.com")), + ("scheduler", re.compile("https?://scheduler\\.(.+)\\.amazonaws\\.com")), ("sdb", re.compile("https?://sdb\\.(.+)\\.amazonaws\\.com")), ("secretsmanager", re.compile("https?://secretsmanager\\.(.+)\\.amazonaws\\.com")), ( diff --git a/moto/scheduler/__init__.py b/moto/scheduler/__init__.py new file mode 100644 index 000000000..369fe0fa5 --- /dev/null +++ b/moto/scheduler/__init__.py @@ -0,0 +1,5 @@ +"""scheduler module initialization; sets value for base decorator.""" +from .models import scheduler_backends +from ..core.models import base_decorator + +mock_scheduler = base_decorator(scheduler_backends) diff --git a/moto/scheduler/exceptions.py b/moto/scheduler/exceptions.py new file mode 100644 index 000000000..cb126594b --- /dev/null +++ b/moto/scheduler/exceptions.py @@ -0,0 +1,12 @@ +"""Exceptions raised by the scheduler service.""" +from moto.core.exceptions import JsonRESTError + + +class ScheduleNotFound(JsonRESTError): + def __init__(self) -> None: + super().__init__("ResourceNotFoundException", "Schedule not found") + + +class ScheduleGroupNotFound(JsonRESTError): + def __init__(self) -> None: + super().__init__("ResourceNotFoundException", "ScheduleGroup not found") diff --git a/moto/scheduler/models.py b/moto/scheduler/models.py new file mode 100644 index 000000000..9e1df7739 --- /dev/null +++ b/moto/scheduler/models.py @@ -0,0 +1,240 @@ +"""EventBridgeSchedulerBackend class with methods for supported APIs.""" +from typing import Any, Dict, List, Iterable, Optional + +from moto.core import BaseBackend, BackendDict, BaseModel +from moto.core.utils import unix_time +from moto.utilities.tagging_service import TaggingService + +from .exceptions import ScheduleNotFound, ScheduleGroupNotFound + + +class Schedule(BaseModel): + def __init__( + self, + region: str, + account_id: str, + group_name: str, + name: str, + description: Optional[str], + schedule_expression: str, + schedule_expression_timezone: Optional[str], + flexible_time_window: Dict[str, Any], + target: Dict[str, Any], + state: Optional[str], + kms_key_arn: Optional[str], + start_date: Optional[str], + end_date: Optional[str], + ): + self.name = name + self.group_name = group_name + self.description = description + self.arn = ( + f"arn:aws:scheduler:{region}:{account_id}:schedule/{group_name}/{name}" + ) + self.schedule_expression = schedule_expression + self.schedule_expression_timezone = schedule_expression_timezone + self.flexible_time_window = flexible_time_window + self.target = Schedule.validate_target(target) + self.state = state or "ENABLED" + self.kms_key_arn = kms_key_arn + self.start_date = start_date + self.end_date = end_date + + @staticmethod + def validate_target(target: Dict[str, Any]) -> Dict[str, Any]: # type: ignore[misc] + if "RetryPolicy" not in target: + target["RetryPolicy"] = { + "MaximumEventAgeInSeconds": 86400, + "MaximumRetryAttempts": 185, + } + return target + + def to_dict(self, short: bool = False) -> Dict[str, Any]: + dct: Dict[str, Any] = { + "Arn": self.arn, + "Name": self.name, + "GroupName": self.group_name, + "Description": self.description, + "ScheduleExpression": self.schedule_expression, + "ScheduleExpressionTimezone": self.schedule_expression_timezone, + "FlexibleTimeWindow": self.flexible_time_window, + "Target": self.target, + "State": self.state, + "KmsKeyArn": self.kms_key_arn, + "StartDate": self.start_date, + "EndDate": self.end_date, + } + if short: + dct["Target"] = {"Arn": dct["Target"]["Arn"]} + return dct + + +class ScheduleGroup(BaseModel): + def __init__(self, region: str, account_id: str, name: str): + self.name = name + self.arn = f"arn:aws:scheduler:{region}:{account_id}:schedule-group/{name}" + self.schedules: Dict[str, Schedule] = dict() + self.created_on = None if self.name == "default" else unix_time() + self.last_modified = None if self.name == "default" else unix_time() + + def add_schedule(self, schedule: Schedule) -> None: + self.schedules[schedule.name] = schedule + + def get_schedule(self, name: str) -> Schedule: + if name not in self.schedules: + raise ScheduleNotFound + return self.schedules[name] + + def delete_schedule(self, name: str) -> None: + self.schedules.pop(name) + + def to_dict(self) -> Dict[str, Any]: + return { + "Arn": self.arn, + "CreationDate": self.created_on, + "LastModificationDate": self.last_modified, + "Name": self.name, + "State": "ACTIVE", + } + + +class EventBridgeSchedulerBackend(BaseBackend): + """Implementation of EventBridgeScheduler APIs.""" + + def __init__(self, region_name: str, account_id: str): + super().__init__(region_name, account_id) + self.schedules: List[Schedule] = list() + self.schedule_groups = { + "default": ScheduleGroup( + region=region_name, account_id=account_id, name="default" + ) + } + self.tagger = TaggingService() + + def create_schedule( + self, + description: str, + end_date: str, + flexible_time_window: Dict[str, Any], + group_name: str, + kms_key_arn: str, + name: str, + schedule_expression: str, + schedule_expression_timezone: str, + start_date: str, + state: str, + target: Dict[str, Any], + ) -> Schedule: + """ + The ClientToken parameter is not yet implemented + """ + group = self.schedule_groups[group_name or "default"] + schedule = Schedule( + region=self.region_name, + account_id=self.account_id, + group_name=group.name, + name=name, + description=description, + schedule_expression=schedule_expression, + schedule_expression_timezone=schedule_expression_timezone, + flexible_time_window=flexible_time_window, + target=target, + state=state, + kms_key_arn=kms_key_arn, + start_date=start_date, + end_date=end_date, + ) + group.add_schedule(schedule) + return schedule + + def get_schedule(self, group_name: Optional[str], name: str) -> Schedule: + group = self.get_schedule_group(group_name) + return group.get_schedule(name) + + def delete_schedule(self, group_name: Optional[str], name: str) -> None: + group = self.get_schedule_group(group_name) + group.delete_schedule(name) + + def update_schedule( + self, + description: str, + end_date: str, + flexible_time_window: Dict[str, Any], + group_name: str, + kms_key_arn: str, + name: str, + schedule_expression: str, + schedule_expression_timezone: str, + start_date: str, + state: str, + target: Dict[str, Any], + ) -> Schedule: + """ + The ClientToken is not yet implemented + """ + schedule = self.get_schedule(group_name=group_name, name=name) + schedule.schedule_expression = schedule_expression + schedule.schedule_expression_timezone = schedule_expression_timezone + schedule.flexible_time_window = flexible_time_window + schedule.target = Schedule.validate_target(target) + schedule.description = description + schedule.state = state + schedule.kms_key_arn = kms_key_arn + schedule.start_date = start_date + schedule.end_date = end_date + return schedule + + def list_schedules( + self, group_names: Optional[str], state: Optional[str] + ) -> Iterable[Schedule]: + """ + The following parameters are not yet implemented: MaxResults, NamePrefix, NextToken + """ + results = [] + for group in self.schedule_groups.values(): + if not group_names or group.name in group_names: + for schedule in group.schedules.values(): + if not state or schedule.state == state: + results.append(schedule) + return results + + def create_schedule_group( + self, name: str, tags: List[Dict[str, str]] + ) -> ScheduleGroup: + """ + The ClientToken parameter is not yet implemented + """ + group = ScheduleGroup( + region=self.region_name, account_id=self.account_id, name=name + ) + self.schedule_groups[name] = group + self.tagger.tag_resource(group.arn, tags) + return group + + def get_schedule_group(self, group_name: Optional[str]) -> ScheduleGroup: + if (group_name or "default") not in self.schedule_groups: + raise ScheduleGroupNotFound + return self.schedule_groups[group_name or "default"] + + def list_schedule_groups(self) -> Iterable[ScheduleGroup]: + """ + The MaxResults-parameter and pagination options are not yet implemented + """ + return self.schedule_groups.values() + + def delete_schedule_group(self, name: Optional[str]) -> None: + self.schedule_groups.pop(name or "default") + + def list_tags_for_resource( + self, resource_arn: str + ) -> Dict[str, List[Dict[str, str]]]: + return self.tagger.list_tags_for_resource(resource_arn) + + def tag_resource(self, resource_arn: str, tags: List[Dict[str, str]]) -> None: + self.tagger.tag_resource(resource_arn, tags) + + def untag_resource(self, resource_arn: str, tag_keys: List[str]) -> None: + self.tagger.untag_resource_using_names(resource_arn, tag_keys) + + +scheduler_backends = BackendDict(EventBridgeSchedulerBackend, "scheduler") diff --git a/moto/scheduler/responses.py b/moto/scheduler/responses.py new file mode 100644 index 000000000..9517840e6 --- /dev/null +++ b/moto/scheduler/responses.py @@ -0,0 +1,142 @@ +"""Handles incoming scheduler requests, invokes methods, returns responses.""" +import json +from typing import Any +from urllib.parse import unquote + +from moto.core.common_types import TYPE_RESPONSE +from moto.core.responses import BaseResponse +from .models import scheduler_backends, EventBridgeSchedulerBackend + + +class EventBridgeSchedulerResponse(BaseResponse): + """Handler for EventBridgeScheduler requests and responses.""" + + def __init__(self) -> None: + super().__init__(service_name="scheduler") + + @property + def scheduler_backend(self) -> EventBridgeSchedulerBackend: + """Return backend instance specific for this region.""" + return scheduler_backends[self.current_account][self.region] + + def create_schedule(self) -> str: + description = self._get_param("Description") + end_date = self._get_param("EndDate") + flexible_time_window = self._get_param("FlexibleTimeWindow") + group_name = self._get_param("GroupName") + kms_key_arn = self._get_param("KmsKeyArn") + name = self.uri.split("/")[-1] + schedule_expression = self._get_param("ScheduleExpression") + schedule_expression_timezone = self._get_param("ScheduleExpressionTimezone") + start_date = self._get_param("StartDate") + state = self._get_param("State") + target = self._get_param("Target") + schedule = self.scheduler_backend.create_schedule( + description=description, + end_date=end_date, + flexible_time_window=flexible_time_window, + group_name=group_name, + kms_key_arn=kms_key_arn, + name=name, + schedule_expression=schedule_expression, + schedule_expression_timezone=schedule_expression_timezone, + start_date=start_date, + state=state, + target=target, + ) + return json.dumps(dict(ScheduleArn=schedule.arn)) + + def get_schedule(self) -> str: + group_name = self._get_param("groupName") + full_url = self.uri.split("?")[0] + name = full_url.split("/")[-1] + schedule = self.scheduler_backend.get_schedule(group_name, name) + return json.dumps(schedule.to_dict()) + + def delete_schedule(self) -> str: + group_name = self._get_param("groupName") + name = self.uri.split("?")[0].split("/")[-1] + self.scheduler_backend.delete_schedule(group_name, name) + return "{}" + + def update_schedule(self) -> str: + group_name = self._get_param("groupName") + name = self.uri.split("?")[0].split("/")[-1] + description = self._get_param("Description") + end_date = self._get_param("EndDate") + flexible_time_window = self._get_param("FlexibleTimeWindow") + kms_key_arn = self._get_param("KmsKeyArn") + schedule_expression = self._get_param("ScheduleExpression") + schedule_expression_timezone = self._get_param("ScheduleExpressionTimezone") + start_date = self._get_param("StartDate") + state = self._get_param("State") + target = self._get_param("Target") + schedule = self.scheduler_backend.update_schedule( + description=description, + end_date=end_date, + flexible_time_window=flexible_time_window, + group_name=group_name, + kms_key_arn=kms_key_arn, + name=name, + schedule_expression=schedule_expression, + schedule_expression_timezone=schedule_expression_timezone, + start_date=start_date, + state=state, + target=target, + ) + return json.dumps(dict(ScheduleArn=schedule.arn)) + + def list_schedules(self) -> str: + group_names = self.querystring.get("ScheduleGroup") + state = self._get_param("State") + schedules = self.scheduler_backend.list_schedules(group_names, state) + return json.dumps({"Schedules": [sch.to_dict(short=True) for sch in schedules]}) + + def create_schedule_group(self) -> str: + name = self._get_param("Name") + tags = self._get_param("Tags") + schedule_group = self.scheduler_backend.create_schedule_group( + name=name, + tags=tags, + ) + return json.dumps(dict(ScheduleGroupArn=schedule_group.arn)) + + def get_schedule_group(self) -> str: + group_name = self.uri.split("?")[0].split("/")[-1] + group = self.scheduler_backend.get_schedule_group(group_name) + return json.dumps(group.to_dict()) + + def delete_schedule_group(self) -> str: + group_name = self.uri.split("?")[0].split("/")[-1] + self.scheduler_backend.delete_schedule_group(group_name) + return "{}" + + def list_schedule_groups(self) -> str: + schedule_groups = self.scheduler_backend.list_schedule_groups() + return json.dumps(dict(ScheduleGroups=[sg.to_dict() for sg in schedule_groups])) + + def list_tags_for_resource(self) -> TYPE_RESPONSE: + resource_arn = unquote(self.uri.split("/tags/")[-1]) + tags = self.scheduler_backend.list_tags_for_resource(resource_arn) + return 200, {}, json.dumps(tags) + + def tag_resource(self) -> TYPE_RESPONSE: + resource_arn = unquote(self.uri.split("/tags/")[-1]) + tags = json.loads(self.body)["Tags"] + self.scheduler_backend.tag_resource(resource_arn, tags) + return 200, {}, "{}" + + def untag_resource(self) -> TYPE_RESPONSE: + resource_arn = unquote(self.uri.split("?")[0].split("/tags/")[-1]) + tag_keys = self.querystring.get("TagKeys") + self.scheduler_backend.untag_resource(resource_arn, tag_keys) # type: ignore + return 200, {}, "{}" + + def tags(self, request: Any, full_url: str, headers: Any) -> TYPE_RESPONSE: + super().setup_class(request, full_url, headers) + if request.method == "POST": + return self.tag_resource() + elif request.method == "DELETE": + return self.untag_resource() + else: + return self.list_tags_for_resource() diff --git a/moto/scheduler/urls.py b/moto/scheduler/urls.py new file mode 100644 index 000000000..ddd71fa77 --- /dev/null +++ b/moto/scheduler/urls.py @@ -0,0 +1,19 @@ +"""scheduler base URL and path.""" +from .responses import EventBridgeSchedulerResponse + +url_bases = [ + r"https?://scheduler\.(.+)\.amazonaws\.com", +] + + +response = EventBridgeSchedulerResponse() + + +url_paths = { + "{0}/schedules$": response.dispatch, + "{0}/schedules/(?P[^/]+)$": response.dispatch, + "{0}/schedule-groups$": response.dispatch, + "{0}/schedule-groups/(?P[^/]+)$": response.dispatch, + "{0}/tags/(?P.+)$": response.tags, + "{0}/tags/arn:aws:scheduler:(?P[^/]+):(?P[^/]+):schedule/(?P[^/]+)/(?P[^/]+)/?$": response.tags, +} diff --git a/setup.cfg b/setup.cfg index 2f4529ffb..ec3d9b04c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -235,7 +235,7 @@ disable = W,C,R,E enable = anomalous-backslash-in-string, arguments-renamed, dangerous-default-value, deprecated-module, function-redefined, import-self, redefined-builtin, redefined-outer-name, reimported, pointless-statement, super-with-arguments, unused-argument, unused-import, unused-variable, useless-else-on-loop, wildcard-import [mypy] -files= moto/a*,moto/b*,moto/c*,moto/d*,moto/e*,moto/f*,moto/g*,moto/i*,moto/k*,moto/l*,moto/m*,moto/n*,moto/o*,moto/p*,moto/q*,moto/rdsdata +files= moto/a*,moto/b*,moto/c*,moto/d*,moto/e*,moto/f*,moto/g*,moto/i*,moto/k*,moto/l*,moto/m*,moto/n*,moto/o*,moto/p*,moto/q*,moto/rdsdata,moto/scheduler show_column_numbers=True show_error_codes = True disable_error_code=abstract diff --git a/tests/terraformtests/terraform-tests.success.txt b/tests/terraformtests/terraform-tests.success.txt index 11c70d14f..7011067bb 100644 --- a/tests/terraformtests/terraform-tests.success.txt +++ b/tests/terraformtests/terraform-tests.success.txt @@ -494,6 +494,9 @@ s3: - TestAccS3ObjectsDataSource_fetchOwner sagemaker: - TestAccSageMakerPrebuiltECRImageDataSource +scheduler: + - TestAccSchedulerSchedule_ + - TestAccSchedulerScheduleGroup_ secretsmanager: - TestAccSecretsManagerSecretDataSource_basic - TestAccSecretsManagerSecretPolicy_ diff --git a/tests/test_scheduler/__init__.py b/tests/test_scheduler/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_scheduler/test_schedule_groups.py b/tests/test_scheduler/test_schedule_groups.py new file mode 100644 index 000000000..ced28e4af --- /dev/null +++ b/tests/test_scheduler/test_schedule_groups.py @@ -0,0 +1,49 @@ +"""Unit tests for scheduler-supported APIs.""" +import boto3 +import pytest + +from botocore.exceptions import ClientError +from moto import mock_scheduler +from moto.core import DEFAULT_ACCOUNT_ID + +# See our Development Tips on writing tests for hints on how to write good tests: +# http://docs.getmoto.org/en/latest/docs/contributing/development_tips/tests.html + + +@mock_scheduler +def test_create_get_delete_schedule_group(): + client = boto3.client("scheduler", region_name="eu-west-1") + arn = client.create_schedule_group(Name="sg")["ScheduleGroupArn"] + + assert arn == f"arn:aws:scheduler:eu-west-1:{DEFAULT_ACCOUNT_ID}:schedule-group/sg" + + group = client.get_schedule_group(Name="sg") + assert group["Arn"] == arn + assert group["Name"] == "sg" + assert group["State"] == "ACTIVE" + + client.delete_schedule_group(Name="sg") + + with pytest.raises(ClientError) as exc: + client.get_schedule_group(Name="sg") + err = exc.value.response["Error"] + assert err["Code"] == "ResourceNotFoundException" + + +@mock_scheduler +def test_list_schedule_groups(): + client = boto3.client("scheduler", region_name="ap-southeast-1") + + # The default group is always active + groups = client.list_schedule_groups()["ScheduleGroups"] + assert len(groups) == 1 + assert ( + groups[0]["Arn"] + == f"arn:aws:scheduler:ap-southeast-1:{DEFAULT_ACCOUNT_ID}:schedule-group/default" + ) + + arn1 = client.create_schedule_group(Name="sg")["ScheduleGroupArn"] + + groups = client.list_schedule_groups()["ScheduleGroups"] + assert len(groups) == 2 + assert groups[1]["Arn"] == arn1 diff --git a/tests/test_scheduler/test_scheduler.py b/tests/test_scheduler/test_scheduler.py new file mode 100644 index 000000000..5531a3b0f --- /dev/null +++ b/tests/test_scheduler/test_scheduler.py @@ -0,0 +1,167 @@ +"""Unit tests for scheduler-supported APIs.""" +import boto3 +import pytest + +from botocore.client import ClientError +from moto import mock_scheduler +from moto.core import DEFAULT_ACCOUNT_ID + +# See our Development Tips on writing tests for hints on how to write good tests: +# http://docs.getmoto.org/en/latest/docs/contributing/development_tips/tests.html + + +@mock_scheduler +def test_create_get_schedule(): + client = boto3.client("scheduler", region_name="eu-west-1") + arn = client.create_schedule( + Name="my-schedule", + ScheduleExpression="some cron", + FlexibleTimeWindow={ + "MaximumWindowInMinutes": 4, + "Mode": "OFF", + }, + Target={ + "Arn": "not supported yet", + "RoleArn": "n/a", + }, + )["ScheduleArn"] + + assert ( + arn + == f"arn:aws:scheduler:eu-west-1:{DEFAULT_ACCOUNT_ID}:schedule/default/my-schedule" + ) + + resp = client.get_schedule(Name="my-schedule") + assert resp["Arn"] == arn + assert resp["Name"] == "my-schedule" + assert resp["ScheduleExpression"] == "some cron" + assert resp["FlexibleTimeWindow"] == { + "MaximumWindowInMinutes": 4, + "Mode": "OFF", + } + assert resp["Target"] == { + "Arn": "not supported yet", + "RoleArn": "n/a", + "RetryPolicy": {"MaximumEventAgeInSeconds": 86400, "MaximumRetryAttempts": 185}, + } + + +@mock_scheduler +def test_create_get_delete__in_different_group(): + client = boto3.client("scheduler", region_name="eu-west-1") + + client.create_schedule_group(Name="sg") + schedule_arn = client.create_schedule( + Name="my-schedule", + GroupName="sg", + ScheduleExpression="some cron", + FlexibleTimeWindow={ + "MaximumWindowInMinutes": 4, + "Mode": "OFF", + }, + Target={ + "Arn": "not supported yet", + "RoleArn": "n/a", + }, + )["ScheduleArn"] + + assert ( + schedule_arn + == "arn:aws:scheduler:eu-west-1:123456789012:schedule/sg/my-schedule" + ) + + schedule = client.get_schedule(GroupName="sg", Name="my-schedule") + assert schedule["Arn"] == schedule_arn + + client.delete_schedule(GroupName="sg", Name="my-schedule") + + with pytest.raises(ClientError) as exc: + client.get_schedule(GroupName="sg", Name="my-schedule") + err = exc.value.response["Error"] + assert err["Code"] == "ResourceNotFoundException" + + +@mock_scheduler +def test_update_schedule(): + client = boto3.client("scheduler", region_name="eu-west-1") + + client.create_schedule( + Name="my-schedule", + ScheduleExpression="some cron", + FlexibleTimeWindow={ + "MaximumWindowInMinutes": 4, + "Mode": "OFF", + }, + Target={ + "Arn": "not supported yet", + "RoleArn": "n/a", + }, + ) + + client.update_schedule( + Name="my-schedule", + Description="new desc", + ScheduleExpression="new cron", + FlexibleTimeWindow={ + "MaximumWindowInMinutes": 4, + "Mode": "OFF", + }, + State="DISABLED", + Target={ + "Arn": "different arn", + "RoleArn": "n/a", + }, + ) + + schedule = client.get_schedule(Name="my-schedule") + assert schedule["Description"] == "new desc" + assert schedule["ScheduleExpression"] == "new cron" + assert schedule["State"] == "DISABLED" + assert schedule["Target"] == { + "Arn": "different arn", + "RoleArn": "n/a", + "RetryPolicy": {"MaximumEventAgeInSeconds": 86400, "MaximumRetryAttempts": 185}, + } + + +@mock_scheduler +def test_get_schedule_for_unknown_group(): + client = boto3.client("scheduler", region_name="eu-west-1") + + with pytest.raises(ClientError) as exc: + client.get_schedule(GroupName="unknown", Name="my-schedule") + err = exc.value.response["Error"] + assert err["Code"] == "ResourceNotFoundException" + + +@mock_scheduler +def test_list_schedules(): + client = boto3.client("scheduler", region_name="eu-west-1") + + schedules = client.list_schedules()["Schedules"] + assert schedules == [] + + client.create_schedule_group(Name="group2") + + for group in ["default", "group2"]: + for schedule in ["sch1", "sch2"]: + for state in ["ENABLED", "DISABLED"]: + client.create_schedule( + Name=f"{schedule}_{state}", + GroupName=group, + State=state, + ScheduleExpression="some cron", + FlexibleTimeWindow={"MaximumWindowInMinutes": 4, "Mode": "OFF"}, + Target={"Arn": "not supported yet", "RoleArn": "n/a"}, + ) + + schedules = client.list_schedules()["Schedules"] + assert len(schedules) == 8 + # The ListSchedules command should not return the entire Target-dictionary + assert schedules[0]["Target"] == {"Arn": "not supported yet"} + + schedules = client.list_schedules(GroupName="group2")["Schedules"] + assert len(schedules) == 4 + + schedules = client.list_schedules(State="ENABLED")["Schedules"] + assert len(schedules) == 4 diff --git a/tests/test_scheduler/test_scheduler_tags.py b/tests/test_scheduler/test_scheduler_tags.py new file mode 100644 index 000000000..c1c7d25d0 --- /dev/null +++ b/tests/test_scheduler/test_scheduler_tags.py @@ -0,0 +1,57 @@ +import boto3 + +from moto import mock_scheduler + + +@mock_scheduler +def test_schedule_tags(): + client = boto3.client("scheduler", "us-east-1") + arn = client.create_schedule( + Name="my-schedule", + ScheduleExpression="some cron", + FlexibleTimeWindow={ + "MaximumWindowInMinutes": 4, + "Mode": "OFF", + }, + Target={ + "Arn": "not supported yet", + "RoleArn": "n/a", + }, + )["ScheduleArn"] + + resp = client.list_tags_for_resource(ResourceArn=arn) + assert resp["Tags"] == [] + + client.tag_resource( + ResourceArn=arn, + Tags=[{"Key": "k1", "Value": "v1"}, {"Key": "k2", "Value": "v2"}], + ) + + resp = client.list_tags_for_resource(ResourceArn=arn) + assert resp["Tags"] == [{"Key": "k1", "Value": "v1"}, {"Key": "k2", "Value": "v2"}] + + client.untag_resource(ResourceArn=arn, TagKeys=["k1"]) + + resp = client.list_tags_for_resource(ResourceArn=arn) + assert resp["Tags"] == [{"Key": "k2", "Value": "v2"}] + + +@mock_scheduler +def test_schedule_group_tags(): + client = boto3.client("scheduler", "us-east-1") + arn = client.create_schedule_group( + Name="my-schedule", Tags=[{"Key": "k1", "Value": "v1"}] + )["ScheduleGroupArn"] + + resp = client.list_tags_for_resource(ResourceArn=arn) + assert resp["Tags"] == [{"Key": "k1", "Value": "v1"}] + + client.tag_resource(ResourceArn=arn, Tags=[{"Key": "k2", "Value": "v2"}]) + + resp = client.list_tags_for_resource(ResourceArn=arn) + assert resp["Tags"] == [{"Key": "k1", "Value": "v1"}, {"Key": "k2", "Value": "v2"}] + + client.untag_resource(ResourceArn=arn, TagKeys=["k1"]) + + resp = client.list_tags_for_resource(ResourceArn=arn) + assert resp["Tags"] == [{"Key": "k2", "Value": "v2"}] diff --git a/tests/test_scheduler/test_server.py b/tests/test_scheduler/test_server.py new file mode 100644 index 000000000..3e7ef2f42 --- /dev/null +++ b/tests/test_scheduler/test_server.py @@ -0,0 +1,10 @@ +from moto import server as server + + +def test_list_tags(): + test_client = server.create_backend_app("scheduler").test_client() + res = test_client.get( + "/tags/arn%3Aaws%3Ascheduler%3Aus-east-1%3A123456789012%3Aschedule%2Fdefault%2Fmy-schedule" + ) + + assert res.data == b'{"Tags": []}'