Feature - Budgets service (#4619)

This commit is contained in:
Bert Blommers 2021-11-23 08:57:15 -01:00 committed by GitHub
parent 68ffb303b5
commit e4e58f777d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 720 additions and 5 deletions

View File

@ -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>

View 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

View File

@ -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):

View File

@ -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
View File

@ -0,0 +1,4 @@
from .models import budgets_backend
budgets_backends = {"global": budgets_backend}
mock_budgets = budgets_backend.decorator

View 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
View 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
View 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
View File

@ -0,0 +1,10 @@
from .responses import BudgetsResponse
url_bases = [
r"https?://budgets\.amazonaws\.com",
]
url_paths = {
"{0}/$": BudgetsResponse.dispatch,
}

View File

@ -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(

View File

View 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. "
)

View 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."
)

View 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})