Feature - Budgets service (#4619)
This commit is contained in:
parent
68ffb303b5
commit
e4e58f777d
@ -299,6 +299,34 @@
|
||||
- [ ] update_scheduling_policy
|
||||
</details>
|
||||
|
||||
## budgets
|
||||
<details>
|
||||
<summary>31% implemented</summary>
|
||||
|
||||
- [X] create_budget
|
||||
- [ ] create_budget_action
|
||||
- [X] create_notification
|
||||
- [ ] create_subscriber
|
||||
- [X] delete_budget
|
||||
- [ ] delete_budget_action
|
||||
- [X] delete_notification
|
||||
- [ ] delete_subscriber
|
||||
- [X] describe_budget
|
||||
- [ ] describe_budget_action
|
||||
- [ ] describe_budget_action_histories
|
||||
- [ ] describe_budget_actions_for_account
|
||||
- [ ] describe_budget_actions_for_budget
|
||||
- [ ] describe_budget_performance_history
|
||||
- [X] describe_budgets
|
||||
- [X] describe_notifications_for_budget
|
||||
- [ ] describe_subscribers_for_notification
|
||||
- [ ] execute_budget_action
|
||||
- [ ] update_budget
|
||||
- [ ] update_budget_action
|
||||
- [ ] update_notification
|
||||
- [ ] update_subscriber
|
||||
</details>
|
||||
|
||||
## cloudformation
|
||||
<details>
|
||||
<summary>27% implemented</summary>
|
||||
@ -4675,7 +4703,6 @@
|
||||
- autoscaling-plans
|
||||
- backup
|
||||
- braket
|
||||
- budgets
|
||||
- ce
|
||||
- chime
|
||||
- chime-sdk-identity
|
||||
@ -4865,4 +4892,4 @@
|
||||
- workmailmessageflow
|
||||
- workspaces
|
||||
- xray
|
||||
</details>
|
||||
</details>
|
60
docs/docs/services/budgets.rst
Normal file
60
docs/docs/services/budgets.rst
Normal file
@ -0,0 +1,60 @@
|
||||
.. _implementedservice_budgets:
|
||||
|
||||
.. |start-h3| raw:: html
|
||||
|
||||
<h3>
|
||||
|
||||
.. |end-h3| raw:: html
|
||||
|
||||
</h3>
|
||||
|
||||
=======
|
||||
budgets
|
||||
=======
|
||||
|
||||
.. autoclass:: moto.budgets.models.BudgetsBackend
|
||||
|
||||
|start-h3| Example usage |end-h3|
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
@mock_budgets
|
||||
def test_budgets_behaviour:
|
||||
boto3.client("budgets")
|
||||
...
|
||||
|
||||
|
||||
|
||||
|start-h3| Implemented features for this service |end-h3|
|
||||
|
||||
- [X] create_budget
|
||||
- [ ] create_budget_action
|
||||
- [X] create_notification
|
||||
- [ ] create_subscriber
|
||||
- [X] delete_budget
|
||||
- [ ] delete_budget_action
|
||||
- [X] delete_notification
|
||||
- [ ] delete_subscriber
|
||||
- [X] describe_budget
|
||||
- [ ] describe_budget_action
|
||||
- [ ] describe_budget_action_histories
|
||||
- [ ] describe_budget_actions_for_account
|
||||
- [ ] describe_budget_actions_for_budget
|
||||
- [ ] describe_budget_performance_history
|
||||
- [X] describe_budgets
|
||||
|
||||
Pagination is not yet implemented
|
||||
|
||||
|
||||
- [X] describe_notifications_for_budget
|
||||
|
||||
Pagination has not yet been implemented
|
||||
|
||||
|
||||
- [ ] describe_subscribers_for_notification
|
||||
- [ ] execute_budget_action
|
||||
- [ ] update_budget
|
||||
- [ ] update_budget_action
|
||||
- [ ] update_notification
|
||||
- [ ] update_subscriber
|
||||
|
@ -38,7 +38,7 @@ mock_lambda = lazy_load(
|
||||
)
|
||||
mock_lambda_deprecated = lazy_load(".awslambda", "mock_lambda_deprecated")
|
||||
mock_batch = lazy_load(".batch", "mock_batch")
|
||||
mock_batch = lazy_load(".batch", "mock_batch")
|
||||
mock_budgets = lazy_load(".budgets", "mock_budgets")
|
||||
mock_cloudformation = lazy_load(".cloudformation", "mock_cloudformation")
|
||||
mock_cloudformation_deprecated = lazy_load(
|
||||
".cloudformation", "mock_cloudformation_deprecated"
|
||||
@ -169,7 +169,7 @@ mock_mediastoredata = lazy_load(
|
||||
)
|
||||
mock_efs = lazy_load(".efs", "mock_efs")
|
||||
mock_wafv2 = lazy_load(".wafv2", "mock_wafv2")
|
||||
mock_sdb = lazy_load(".sdb", "mock_sdb", boto3_name="sdb")
|
||||
mock_sdb = lazy_load(".sdb", "mock_sdb")
|
||||
|
||||
|
||||
class MockAll(ContextDecorator):
|
||||
|
@ -11,6 +11,7 @@ backend_url_patterns = [
|
||||
("athena", re.compile("https?://athena\\.(.+)\\.amazonaws\\.com")),
|
||||
("autoscaling", re.compile("https?://autoscaling\\.(.+)\\.amazonaws\\.com")),
|
||||
("batch", re.compile("https?://batch\\.(.+)\\.amazonaws.com")),
|
||||
("budgets", re.compile("https?://budgets\\.amazonaws\\.com")),
|
||||
("cloudformation", re.compile("https?://cloudformation\\.(.+)\\.amazonaws\\.com")),
|
||||
("cloudtrail", re.compile("https?://cloudtrail\\.(.+)\\.amazonaws\\.com")),
|
||||
("cloudwatch", re.compile("https?://monitoring\\.(.+)\\.amazonaws.com")),
|
||||
|
4
moto/budgets/__init__.py
Normal file
4
moto/budgets/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
from .models import budgets_backend
|
||||
|
||||
budgets_backends = {"global": budgets_backend}
|
||||
mock_budgets = budgets_backend.decorator
|
29
moto/budgets/exceptions.py
Normal file
29
moto/budgets/exceptions.py
Normal file
@ -0,0 +1,29 @@
|
||||
"""Exceptions raised by the budgets service."""
|
||||
from moto.core.exceptions import JsonRESTError
|
||||
|
||||
|
||||
class DuplicateRecordException(JsonRESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self, record_type, record_name):
|
||||
super().__init__(
|
||||
__class__.__name__,
|
||||
f"Error creating {record_type}: {record_name} - the {record_type} already exists.",
|
||||
)
|
||||
|
||||
|
||||
class NotFoundException(JsonRESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self, message):
|
||||
super().__init__(__class__.__name__, message)
|
||||
|
||||
|
||||
class BudgetMissingLimit(JsonRESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
"InvalidParameterException",
|
||||
"Unable to create/update budget - please provide one of the followings: Budget Limit/ Planned Budget Limit/ Auto Adjust Data",
|
||||
)
|
126
moto/budgets/models.py
Normal file
126
moto/budgets/models.py
Normal file
@ -0,0 +1,126 @@
|
||||
from collections import defaultdict
|
||||
from copy import deepcopy
|
||||
from datetime import datetime
|
||||
from moto.core import BaseBackend, BaseModel
|
||||
from moto.core.utils import unix_time
|
||||
|
||||
from .exceptions import BudgetMissingLimit, DuplicateRecordException, NotFoundException
|
||||
|
||||
|
||||
class Notification(BaseModel):
|
||||
def __init__(self, details, subscribers):
|
||||
self.details = details
|
||||
self.subscribers = subscribers
|
||||
|
||||
|
||||
class Budget(BaseModel):
|
||||
def __init__(self, budget, notifications):
|
||||
if "BudgetLimit" not in budget and "PlannedBudgetLimits" not in budget:
|
||||
raise BudgetMissingLimit()
|
||||
# Storing the budget as a Dict for now - if we need more control, we can always read/write it back
|
||||
self.budget = budget
|
||||
self.notifications = [
|
||||
Notification(details=x["Notification"], subscribers=x["Subscribers"])
|
||||
for x in notifications
|
||||
]
|
||||
self.budget["LastUpdatedTime"] = unix_time()
|
||||
if "TimePeriod" not in self.budget:
|
||||
first_day_of_month = datetime.now().replace(
|
||||
day=1, hour=0, minute=0, second=0, microsecond=0
|
||||
)
|
||||
self.budget["TimePeriod"] = {
|
||||
"Start": unix_time(first_day_of_month),
|
||||
"End": 3706473600, # "2087-06-15T00:00:00+00:00"
|
||||
}
|
||||
|
||||
def to_dict(self):
|
||||
cp = deepcopy(self.budget)
|
||||
if "CalculatedSpend" not in cp:
|
||||
cp["CalculatedSpend"] = {
|
||||
"ActualSpend": {"Amount": "0", "Unit": "USD"},
|
||||
"ForecastedSpend": {"Amount": "0", "Unit": "USD"},
|
||||
}
|
||||
if self.budget["BudgetType"] == "COST" and "CostTypes" not in cp:
|
||||
cp["CostTypes"] = {
|
||||
"IncludeCredit": True,
|
||||
"IncludeDiscount": True,
|
||||
"IncludeOtherSubscription": True,
|
||||
"IncludeRecurring": True,
|
||||
"IncludeRefund": True,
|
||||
"IncludeSubscription": True,
|
||||
"IncludeSupport": True,
|
||||
"IncludeTax": True,
|
||||
"IncludeUpfront": True,
|
||||
"UseAmortized": False,
|
||||
"UseBlended": False,
|
||||
}
|
||||
return cp
|
||||
|
||||
def add_notification(self, details, subscribers):
|
||||
self.notifications.append(Notification(details, subscribers))
|
||||
|
||||
def delete_notification(self, details):
|
||||
self.notifications = [n for n in self.notifications if n.details != details]
|
||||
|
||||
def get_notifications(self):
|
||||
return [n.details for n in self.notifications]
|
||||
|
||||
|
||||
class BudgetsBackend(BaseBackend):
|
||||
"""Implementation of Budgets APIs."""
|
||||
|
||||
def __init__(self):
|
||||
# {"account_id": {"budget_name": Budget}}
|
||||
self.budgets = defaultdict(dict)
|
||||
|
||||
def create_budget(self, account_id, budget, notifications):
|
||||
budget_name = budget["BudgetName"]
|
||||
if budget_name in self.budgets[account_id]:
|
||||
raise DuplicateRecordException(
|
||||
record_type="budget", record_name=budget_name
|
||||
)
|
||||
self.budgets[account_id][budget_name] = Budget(budget, notifications)
|
||||
|
||||
def describe_budget(self, account_id, budget_name):
|
||||
if budget_name not in self.budgets[account_id]:
|
||||
raise NotFoundException(
|
||||
f"Unable to get budget: {budget_name} - the budget doesn't exist."
|
||||
)
|
||||
return self.budgets[account_id][budget_name].to_dict()
|
||||
|
||||
def describe_budgets(self, account_id):
|
||||
"""
|
||||
Pagination is not yet implemented
|
||||
"""
|
||||
return [budget.to_dict() for budget in self.budgets[account_id].values()]
|
||||
|
||||
def delete_budget(self, account_id, budget_name):
|
||||
if budget_name not in self.budgets[account_id]:
|
||||
msg = f"Unable to delete budget: {budget_name} - the budget doesn't exist. Try creating it first. "
|
||||
raise NotFoundException(msg)
|
||||
self.budgets[account_id].pop(budget_name)
|
||||
|
||||
def create_notification(self, account_id, budget_name, notification, subscribers):
|
||||
if budget_name not in self.budgets[account_id]:
|
||||
raise NotFoundException(
|
||||
"Unable to create notification - the budget doesn't exist."
|
||||
)
|
||||
self.budgets[account_id][budget_name].add_notification(
|
||||
details=notification, subscribers=subscribers
|
||||
)
|
||||
|
||||
def delete_notification(self, account_id, budget_name, notification):
|
||||
if budget_name not in self.budgets[account_id]:
|
||||
raise NotFoundException(
|
||||
"Unable to delete notification - the budget doesn't exist."
|
||||
)
|
||||
self.budgets[account_id][budget_name].delete_notification(details=notification)
|
||||
|
||||
def describe_notifications_for_budget(self, account_id, budget_name):
|
||||
"""
|
||||
Pagination has not yet been implemented
|
||||
"""
|
||||
return self.budgets[account_id][budget_name].get_notifications()
|
||||
|
||||
|
||||
budgets_backend = BudgetsBackend()
|
66
moto/budgets/responses.py
Normal file
66
moto/budgets/responses.py
Normal file
@ -0,0 +1,66 @@
|
||||
import json
|
||||
|
||||
from moto.core.responses import BaseResponse
|
||||
from .models import budgets_backend
|
||||
|
||||
|
||||
class BudgetsResponse(BaseResponse):
|
||||
def create_budget(self):
|
||||
account_id = self._get_param("AccountId")
|
||||
budget = self._get_param("Budget")
|
||||
notifications = self._get_param("NotificationsWithSubscribers", [])
|
||||
budgets_backend.create_budget(
|
||||
account_id=account_id, budget=budget, notifications=notifications
|
||||
)
|
||||
return json.dumps(dict())
|
||||
|
||||
def describe_budget(self):
|
||||
account_id = self._get_param("AccountId")
|
||||
budget_name = self._get_param("BudgetName")
|
||||
budget = budgets_backend.describe_budget(
|
||||
account_id=account_id, budget_name=budget_name
|
||||
)
|
||||
return json.dumps(dict(Budget=budget))
|
||||
|
||||
def describe_budgets(self):
|
||||
account_id = self._get_param("AccountId")
|
||||
budgets = budgets_backend.describe_budgets(account_id=account_id)
|
||||
return json.dumps(dict(Budgets=budgets, nextToken=None))
|
||||
|
||||
def delete_budget(self):
|
||||
account_id = self._get_param("AccountId")
|
||||
budget_name = self._get_param("BudgetName")
|
||||
budgets_backend.delete_budget(
|
||||
account_id=account_id, budget_name=budget_name,
|
||||
)
|
||||
return json.dumps(dict())
|
||||
|
||||
def create_notification(self):
|
||||
account_id = self._get_param("AccountId")
|
||||
budget_name = self._get_param("BudgetName")
|
||||
notification = self._get_param("Notification")
|
||||
subscribers = self._get_param("Subscribers")
|
||||
budgets_backend.create_notification(
|
||||
account_id=account_id,
|
||||
budget_name=budget_name,
|
||||
notification=notification,
|
||||
subscribers=subscribers,
|
||||
)
|
||||
return json.dumps(dict())
|
||||
|
||||
def delete_notification(self):
|
||||
account_id = self._get_param("AccountId")
|
||||
budget_name = self._get_param("BudgetName")
|
||||
notification = self._get_param("Notification")
|
||||
budgets_backend.delete_notification(
|
||||
account_id=account_id, budget_name=budget_name, notification=notification,
|
||||
)
|
||||
return json.dumps(dict())
|
||||
|
||||
def describe_notifications_for_budget(self):
|
||||
account_id = self._get_param("AccountId")
|
||||
budget_name = self._get_param("BudgetName")
|
||||
notifications = budgets_backend.describe_notifications_for_budget(
|
||||
account_id=account_id, budget_name=budget_name,
|
||||
)
|
||||
return json.dumps(dict(Notifications=notifications, NextToken=None))
|
10
moto/budgets/urls.py
Normal file
10
moto/budgets/urls.py
Normal file
@ -0,0 +1,10 @@
|
||||
from .responses import BudgetsResponse
|
||||
|
||||
url_bases = [
|
||||
r"https?://budgets\.amazonaws\.com",
|
||||
]
|
||||
|
||||
|
||||
url_paths = {
|
||||
"{0}/$": BudgetsResponse.dispatch,
|
||||
}
|
@ -119,7 +119,10 @@ class DomainDispatcherApplication(object):
|
||||
# S3 is the last resort when the target is also unknown
|
||||
service, region = DEFAULT_SERVICE_REGION
|
||||
|
||||
if service == "mediastore" and not target:
|
||||
if service == "budgets":
|
||||
# Budgets is global
|
||||
host = f"{service}.amazonaws.com"
|
||||
elif service == "mediastore" and not target:
|
||||
# All MediaStore API calls have a target header
|
||||
# If no target is set, assume we're trying to reach the mediastore-data service
|
||||
host = "data.{service}.{region}.amazonaws.com".format(
|
||||
|
0
tests/test_budgets/__init__.py
Normal file
0
tests/test_budgets/__init__.py
Normal file
176
tests/test_budgets/test_budgets.py
Normal file
176
tests/test_budgets/test_budgets.py
Normal file
@ -0,0 +1,176 @@
|
||||
import boto3
|
||||
import pytest
|
||||
|
||||
from botocore.exceptions import ClientError
|
||||
import sure # noqa # pylint: disable=unused-import
|
||||
from datetime import datetime
|
||||
from moto import mock_budgets
|
||||
from moto.core import ACCOUNT_ID
|
||||
|
||||
|
||||
@mock_budgets
|
||||
def test_create_and_describe_budget_minimal_params():
|
||||
client = boto3.client("budgets", region_name="us-east-1")
|
||||
resp = client.create_budget(
|
||||
AccountId=ACCOUNT_ID,
|
||||
Budget={
|
||||
"BudgetLimit": {"Amount": "10", "Unit": "USD"},
|
||||
"BudgetName": "testbudget",
|
||||
"TimeUnit": "DAILY",
|
||||
"BudgetType": "COST",
|
||||
},
|
||||
)
|
||||
resp["ResponseMetadata"]["HTTPStatusCode"].should.equal(200)
|
||||
|
||||
budget = client.describe_budget(AccountId=ACCOUNT_ID, BudgetName="testbudget")[
|
||||
"Budget"
|
||||
]
|
||||
budget.should.have.key("BudgetLimit")
|
||||
budget["BudgetLimit"].should.have.key("Amount")
|
||||
budget["BudgetLimit"]["Amount"].should.equal("10")
|
||||
budget["BudgetLimit"].should.have.key("Unit")
|
||||
budget["BudgetLimit"]["Unit"].should.equal("USD")
|
||||
budget.should.have.key("BudgetName").equals("testbudget")
|
||||
budget.should.have.key("TimeUnit").equals("DAILY")
|
||||
budget.should.have.key("BudgetType").equals("COST")
|
||||
budget.should.have.key("CalculatedSpend")
|
||||
budget["CalculatedSpend"].should.have.key("ActualSpend")
|
||||
budget["CalculatedSpend"]["ActualSpend"].should.equal(
|
||||
{"Amount": "0", "Unit": "USD"}
|
||||
)
|
||||
budget["CalculatedSpend"].should.have.key("ForecastedSpend")
|
||||
budget["CalculatedSpend"]["ForecastedSpend"].should.equal(
|
||||
{"Amount": "0", "Unit": "USD"}
|
||||
)
|
||||
budget.should.have.key("CostTypes")
|
||||
budget["CostTypes"].should.have.key("IncludeCredit").equals(True)
|
||||
budget["CostTypes"].should.have.key("IncludeDiscount").equals(True)
|
||||
budget["CostTypes"].should.have.key("IncludeOtherSubscription").equals(True)
|
||||
budget["CostTypes"].should.have.key("IncludeRecurring").equals(True)
|
||||
budget["CostTypes"].should.have.key("IncludeRefund").equals(True)
|
||||
budget["CostTypes"].should.have.key("IncludeSubscription").equals(True)
|
||||
budget["CostTypes"].should.have.key("IncludeSupport").equals(True)
|
||||
budget["CostTypes"].should.have.key("IncludeTax").equals(True)
|
||||
budget["CostTypes"].should.have.key("IncludeUpfront").equals(True)
|
||||
budget["CostTypes"].should.have.key("UseAmortized").equals(False)
|
||||
budget["CostTypes"].should.have.key("UseBlended").equals(False)
|
||||
budget.should.have.key("LastUpdatedTime").should.be.a(datetime)
|
||||
budget.should.have.key("TimePeriod")
|
||||
budget["TimePeriod"].should.have.key("Start")
|
||||
budget["TimePeriod"]["Start"].should.be.a(datetime)
|
||||
budget["TimePeriod"].should.have.key("End")
|
||||
budget["TimePeriod"]["End"].should.be.a(datetime)
|
||||
budget.should.have.key("TimeUnit").equals("DAILY")
|
||||
|
||||
|
||||
@mock_budgets
|
||||
def test_create_existing_budget():
|
||||
client = boto3.client("budgets", region_name="us-east-1")
|
||||
client.create_budget(
|
||||
AccountId=ACCOUNT_ID,
|
||||
Budget={
|
||||
"BudgetLimit": {"Amount": "10", "Unit": "USD"},
|
||||
"BudgetName": "testb",
|
||||
"TimeUnit": "DAILY",
|
||||
"BudgetType": "COST",
|
||||
},
|
||||
)
|
||||
|
||||
with pytest.raises(ClientError) as exc:
|
||||
client.create_budget(
|
||||
AccountId=ACCOUNT_ID,
|
||||
Budget={
|
||||
"BudgetLimit": {"Amount": "10", "Unit": "USD"},
|
||||
"BudgetName": "testb",
|
||||
"TimeUnit": "DAILY",
|
||||
"BudgetType": "COST",
|
||||
},
|
||||
)
|
||||
err = exc.value.response["Error"]
|
||||
err["Code"].should.equal("DuplicateRecordException")
|
||||
err["Message"].should.equal(
|
||||
"Error creating budget: testb - the budget already exists."
|
||||
)
|
||||
|
||||
|
||||
@mock_budgets
|
||||
def test_create_budget_without_limit_param():
|
||||
client = boto3.client("budgets", region_name="us-east-1")
|
||||
with pytest.raises(ClientError) as exc:
|
||||
client.create_budget(
|
||||
AccountId=ACCOUNT_ID,
|
||||
Budget={"BudgetName": "testb", "TimeUnit": "DAILY", "BudgetType": "COST"},
|
||||
)
|
||||
err = exc.value.response["Error"]
|
||||
err["Code"].should.equal("InvalidParameterException")
|
||||
err["Message"].should.equal(
|
||||
"Unable to create/update budget - please provide one of the followings: Budget Limit/ Planned Budget Limit/ Auto Adjust Data"
|
||||
)
|
||||
|
||||
|
||||
@mock_budgets
|
||||
def test_describe_unknown_budget():
|
||||
client = boto3.client("budgets", region_name="us-east-1")
|
||||
with pytest.raises(ClientError) as exc:
|
||||
client.describe_budget(AccountId=ACCOUNT_ID, BudgetName="unknown")
|
||||
err = exc.value.response["Error"]
|
||||
err["Code"].should.equal("NotFoundException")
|
||||
err["Message"].should.equal(
|
||||
"Unable to get budget: unknown - the budget doesn't exist."
|
||||
)
|
||||
|
||||
|
||||
@mock_budgets
|
||||
def test_describe_no_budgets():
|
||||
client = boto3.client("budgets", region_name="us-east-1")
|
||||
resp = client.describe_budgets(AccountId=ACCOUNT_ID)
|
||||
resp.should.have.key("Budgets").equals([])
|
||||
|
||||
|
||||
@mock_budgets
|
||||
def test_create_and_describe_all_budgets():
|
||||
client = boto3.client("budgets", region_name="us-east-1")
|
||||
client.create_budget(
|
||||
AccountId=ACCOUNT_ID,
|
||||
Budget={
|
||||
"BudgetLimit": {"Amount": "10", "Unit": "USD"},
|
||||
"BudgetName": "testbudget",
|
||||
"TimeUnit": "DAILY",
|
||||
"BudgetType": "COST",
|
||||
},
|
||||
)
|
||||
|
||||
res = client.describe_budgets(AccountId=ACCOUNT_ID)
|
||||
res["Budgets"].should.have.length_of(1)
|
||||
|
||||
|
||||
@mock_budgets
|
||||
def test_delete_budget():
|
||||
client = boto3.client("budgets", region_name="us-east-1")
|
||||
client.create_budget(
|
||||
AccountId=ACCOUNT_ID,
|
||||
Budget={
|
||||
"BudgetLimit": {"Amount": "10", "Unit": "USD"},
|
||||
"BudgetName": "b1",
|
||||
"TimeUnit": "DAILY",
|
||||
"BudgetType": "COST",
|
||||
},
|
||||
)
|
||||
|
||||
resp = client.delete_budget(AccountId=ACCOUNT_ID, BudgetName="b1")
|
||||
resp["ResponseMetadata"]["HTTPStatusCode"].should.equal(200)
|
||||
|
||||
res = client.describe_budgets(AccountId=ACCOUNT_ID)
|
||||
res["Budgets"].should.have.length_of(0)
|
||||
|
||||
|
||||
@mock_budgets
|
||||
def test_delete_unknown_budget():
|
||||
client = boto3.client("budgets", region_name="us-east-1")
|
||||
with pytest.raises(ClientError) as exc:
|
||||
client.delete_budget(AccountId=ACCOUNT_ID, BudgetName="unknown")
|
||||
err = exc.value.response["Error"]
|
||||
err["Code"].should.equal("NotFoundException")
|
||||
err["Message"].should.equal(
|
||||
"Unable to delete budget: unknown - the budget doesn't exist. Try creating it first. "
|
||||
)
|
197
tests/test_budgets/test_notifications.py
Normal file
197
tests/test_budgets/test_notifications.py
Normal file
@ -0,0 +1,197 @@
|
||||
import boto3
|
||||
import pytest
|
||||
|
||||
from botocore.exceptions import ClientError
|
||||
import sure # noqa # pylint: disable=unused-import
|
||||
from moto import mock_budgets
|
||||
from moto.core import ACCOUNT_ID
|
||||
|
||||
|
||||
@mock_budgets
|
||||
def test_create_and_describe_notification():
|
||||
client = boto3.client("budgets", region_name="us-east-1")
|
||||
client.create_budget(
|
||||
AccountId=ACCOUNT_ID,
|
||||
Budget={
|
||||
"BudgetLimit": {"Amount": "10", "Unit": "USD"},
|
||||
"BudgetName": "testbudget",
|
||||
"TimeUnit": "DAILY",
|
||||
"BudgetType": "COST",
|
||||
},
|
||||
NotificationsWithSubscribers=[
|
||||
{
|
||||
"Notification": {
|
||||
"NotificationType": "ACTUAL",
|
||||
"ComparisonOperator": "EQUAL_TO",
|
||||
"Threshold": 123.0,
|
||||
"ThresholdType": "ABSOLUTE_VALUE",
|
||||
"NotificationState": "ALARM",
|
||||
},
|
||||
"Subscribers": [
|
||||
{"SubscriptionType": "EMAIL", "Address": "admin@moto.com"},
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
res = client.describe_notifications_for_budget(
|
||||
AccountId=ACCOUNT_ID, BudgetName="testbudget"
|
||||
)
|
||||
res.should.have.key("Notifications").length_of(1)
|
||||
notification = res["Notifications"][0]
|
||||
notification.should.have.key("NotificationType").should.equal("ACTUAL")
|
||||
notification.should.have.key("ComparisonOperator").should.equal("EQUAL_TO")
|
||||
notification.should.have.key("Threshold").should.equal(123)
|
||||
notification.should.have.key("ThresholdType").should.equal("ABSOLUTE_VALUE")
|
||||
notification.should.have.key("NotificationState").should.equal("ALARM")
|
||||
|
||||
|
||||
@mock_budgets
|
||||
def test_create_notification():
|
||||
client = boto3.client("budgets", region_name="us-east-1")
|
||||
client.create_budget(
|
||||
AccountId=ACCOUNT_ID,
|
||||
Budget={
|
||||
"BudgetLimit": {"Amount": "10", "Unit": "USD"},
|
||||
"BudgetName": "testbudget",
|
||||
"TimeUnit": "DAILY",
|
||||
"BudgetType": "COST",
|
||||
},
|
||||
NotificationsWithSubscribers=[
|
||||
{
|
||||
"Notification": {
|
||||
"NotificationType": "ACTUAL",
|
||||
"ComparisonOperator": "EQUAL_TO",
|
||||
"Threshold": 123.0,
|
||||
"ThresholdType": "ABSOLUTE_VALUE",
|
||||
"NotificationState": "ALARM",
|
||||
},
|
||||
"Subscribers": [
|
||||
{"SubscriptionType": "EMAIL", "Address": "admin@moto.com"},
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
res = client.create_notification(
|
||||
AccountId=ACCOUNT_ID,
|
||||
BudgetName="testbudget",
|
||||
Notification={
|
||||
"NotificationType": "ACTUAL",
|
||||
"ComparisonOperator": "GREATER_THAN",
|
||||
"Threshold": 0.0,
|
||||
"ThresholdType": "ABSOLUTE_VALUE",
|
||||
"NotificationState": "OK",
|
||||
},
|
||||
Subscribers=[{"SubscriptionType": "SNS", "Address": "arn:sns:topic:mytopic"}],
|
||||
)
|
||||
res["ResponseMetadata"]["HTTPStatusCode"].should.equal(200)
|
||||
|
||||
res = client.describe_notifications_for_budget(
|
||||
AccountId=ACCOUNT_ID, BudgetName="testbudget"
|
||||
)
|
||||
res.should.have.key("Notifications").length_of(2)
|
||||
n_1 = res["Notifications"][0]
|
||||
n_1.should.have.key("NotificationType").should.equal("ACTUAL")
|
||||
n_1.should.have.key("ComparisonOperator").should.equal("EQUAL_TO")
|
||||
n_1.should.have.key("Threshold").should.equal(123)
|
||||
n_1.should.have.key("ThresholdType").should.equal("ABSOLUTE_VALUE")
|
||||
n_1.should.have.key("NotificationState").should.equal("ALARM")
|
||||
n_2 = res["Notifications"][1]
|
||||
n_2.should.have.key("NotificationType").should.equal("ACTUAL")
|
||||
n_2.should.have.key("ComparisonOperator").should.equal("GREATER_THAN")
|
||||
n_2.should.have.key("Threshold").should.equal(0)
|
||||
n_2.should.have.key("ThresholdType").should.equal("ABSOLUTE_VALUE")
|
||||
n_2.should.have.key("NotificationState").should.equal("OK")
|
||||
|
||||
|
||||
@mock_budgets
|
||||
def test_create_notification_unknown_budget():
|
||||
client = boto3.client("budgets", region_name="us-east-1")
|
||||
|
||||
with pytest.raises(ClientError) as exc:
|
||||
client.create_notification(
|
||||
AccountId=ACCOUNT_ID,
|
||||
BudgetName="testbudget",
|
||||
Notification={
|
||||
"NotificationType": "FORECASTED", # doesn't exist
|
||||
"ComparisonOperator": "EQUAL_TO",
|
||||
"Threshold": 123.0,
|
||||
"ThresholdType": "ABSOLUTE_VALUE",
|
||||
"NotificationState": "ALARM",
|
||||
},
|
||||
Subscribers=[{"SubscriptionType": "EMAIL", "Address": "admin@moto.com"}],
|
||||
)
|
||||
err = exc.value.response["Error"]
|
||||
err["Code"].should.equal("NotFoundException")
|
||||
err["Message"].should.equal(
|
||||
"Unable to create notification - the budget doesn't exist."
|
||||
)
|
||||
|
||||
|
||||
@mock_budgets
|
||||
def test_delete_notification():
|
||||
client = boto3.client("budgets", region_name="us-east-1")
|
||||
client.create_budget(
|
||||
AccountId=ACCOUNT_ID,
|
||||
Budget={
|
||||
"BudgetLimit": {"Amount": "10", "Unit": "USD"},
|
||||
"BudgetName": "testbudget",
|
||||
"TimeUnit": "DAILY",
|
||||
"BudgetType": "COST",
|
||||
},
|
||||
NotificationsWithSubscribers=[
|
||||
{
|
||||
"Notification": {
|
||||
"NotificationType": "ACTUAL",
|
||||
"ComparisonOperator": "EQUAL_TO",
|
||||
"Threshold": 123.0,
|
||||
"ThresholdType": "ABSOLUTE_VALUE",
|
||||
"NotificationState": "ALARM",
|
||||
},
|
||||
"Subscribers": [
|
||||
{"SubscriptionType": "EMAIL", "Address": "admin@moto.com"},
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
client.delete_notification(
|
||||
AccountId=ACCOUNT_ID,
|
||||
BudgetName="testbudget",
|
||||
Notification={
|
||||
"NotificationType": "ACTUAL",
|
||||
"ComparisonOperator": "EQUAL_TO",
|
||||
"Threshold": 123.0,
|
||||
"ThresholdType": "ABSOLUTE_VALUE",
|
||||
"NotificationState": "ALARM",
|
||||
},
|
||||
)
|
||||
|
||||
res = client.describe_notifications_for_budget(
|
||||
AccountId=ACCOUNT_ID, BudgetName="testbudget"
|
||||
)
|
||||
res.should.have.key("Notifications").length_of(0)
|
||||
|
||||
|
||||
@mock_budgets
|
||||
def test_delete_notification_unknown_budget():
|
||||
client = boto3.client("budgets", region_name="us-east-1")
|
||||
|
||||
with pytest.raises(ClientError) as exc:
|
||||
client.delete_notification(
|
||||
AccountId=ACCOUNT_ID,
|
||||
BudgetName="testbudget",
|
||||
Notification={
|
||||
"NotificationType": "FORECASTED",
|
||||
"ComparisonOperator": "EQUAL_TO",
|
||||
"Threshold": 123.0,
|
||||
"ThresholdType": "ABSOLUTE_VALUE",
|
||||
"NotificationState": "ALARM",
|
||||
},
|
||||
)
|
||||
err = exc.value.response["Error"]
|
||||
err["Code"].should.equal("NotFoundException")
|
||||
err["Message"].should.equal(
|
||||
"Unable to delete notification - the budget doesn't exist."
|
||||
)
|
16
tests/test_budgets/test_server.py
Normal file
16
tests/test_budgets/test_server.py
Normal file
@ -0,0 +1,16 @@
|
||||
import json
|
||||
import sure # noqa # pylint: disable=unused-import
|
||||
|
||||
import moto.server as server
|
||||
from moto import mock_budgets
|
||||
|
||||
|
||||
@mock_budgets
|
||||
def test_budgets_describe_budgets():
|
||||
backend = server.create_backend_app("budgets")
|
||||
test_client = backend.test_client()
|
||||
|
||||
headers = {"X-Amz-Target": "AWSBudgetServiceGateway.DescribeBudgets"}
|
||||
resp = test_client.post("/", headers=headers, json={})
|
||||
resp.status_code.should.equal(200)
|
||||
json.loads(resp.data).should.equal({"Budgets": [], "nextToken": None})
|
Loading…
Reference in New Issue
Block a user