Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions tests/test_update_product_deployment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import unittest

from tests import BaseTestCase


class TestUpdateProductDeployment(BaseTestCase):
def test_update_product_deployment_updates_both_fields_and_returns_200(self):
"""PUT updates deployment name and artifact_type and keeps slug unchanged."""
response = self.client.put(
"/products/test-product/test-deployment",
json={
"name": "Canonical Charmed Ceph Updated",
"artifact_type": "charm",
},
)
payload = response.get_json()

self.assertEqual(response.status_code, 200)
self.assertEqual(payload["slug"], "test-deployment")
self.assertEqual(payload["parent_product"], "test-product")
self.assertEqual(payload["name"], "Canonical Charmed Ceph Updated")
self.assertEqual(payload["artifact_type"], "charm")
self.assertIn("versions", payload)

def test_update_product_deployment_updates_name_only(self):
"""PUT updates only name when artifact_type is omitted."""
response = self.client.put(
"/products/test-product/test-deployment",
json={"name": "Updated Deployment Name"},
)
payload = response.get_json()

self.assertEqual(response.status_code, 200)
self.assertEqual(payload["slug"], "test-deployment")
self.assertEqual(payload["name"], "Updated Deployment Name")
self.assertEqual(payload["artifact_type"], "snap")

def test_update_product_deployment_updates_artifact_type_only(self):
"""PUT updates only artifact_type when name is omitted."""
response = self.client.put(
"/products/test-product/test-deployment",
json={"artifact_type": "rock"},
)
payload = response.get_json()

self.assertEqual(response.status_code, 200)
self.assertEqual(payload["slug"], "test-deployment")
self.assertEqual(payload["name"], "Test Deployment")
self.assertEqual(payload["artifact_type"], "rock")

def test_update_product_deployment_empty_body_returns_400(self):
"""PUT with empty body returns 400 using existing error shape."""
response = self.client.put(
"/products/test-product/test-deployment",
json={},
)
payload = response.get_json()

self.assertEqual(response.status_code, 400)
self.assertIn("error", payload)
self.assertEqual(payload["error"]["message"], "Invalid request.")
self.assertIn("details", payload["error"])

def test_update_product_deployment_invalid_artifact_type_returns_400(self):
"""PUT with invalid artifact_type returns 400 with field details."""
response = self.client.put(
"/products/test-product/test-deployment",
json={"artifact_type": "invalid"},
)
payload = response.get_json()

self.assertEqual(response.status_code, 400)
self.assertIn("error", payload)
self.assertEqual(payload["error"]["message"], "Invalid request.")
self.assertIn("details", payload["error"])
self.assertIn("artifact_type", payload["error"]["details"])

def test_update_product_deployment_whitespace_name_returns_400(self):
"""PUT with whitespace-only name returns 400 with name details."""
response = self.client.put(
"/products/test-product/test-deployment",
json={"name": " "},
)
payload = response.get_json()

self.assertEqual(response.status_code, 400)
self.assertIn("error", payload)
self.assertEqual(payload["error"]["message"], "Invalid request.")
self.assertIn("details", payload["error"])
self.assertIn("name", payload["error"]["details"])

def test_update_product_deployment_not_found_returns_404(self):
"""PUT unknown deployment returns 404 with identifying details."""
response = self.client.put(
"/products/test-product/does-not-exist",
json={"name": "Updated Deployment Name"},
)
payload = response.get_json()

self.assertEqual(response.status_code, 404)
self.assertIn("error", payload)
self.assertEqual(payload["error"]["message"], "Deployment not found.")
self.assertEqual(
payload["error"]["details"],
{
"product_slug": "test-product",
"deployment_slug": "does-not-exist",
},
)

def test_update_product_deployment_product_not_found_returns_404(self):
"""Unknown product returns 404 with identifying details."""
response = self.client.put(
"/products/does-not-exist/test-deployment",
json={
"name": "Updated Deployment Name",
"artifact_type": "snap",
},
)
payload = response.get_json()

self.assertEqual(response.status_code, 404)
self.assertIn("error", payload)
self.assertEqual(payload["error"]["message"], "Product not found.")
self.assertEqual(
payload["error"]["details"],
{"product_slug": "does-not-exist"},
)


if __name__ == "__main__":
unittest.main()
5 changes: 5 additions & 0 deletions webapp/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@
view_func=views.get_product_deployment,
methods=["GET"],
)
app.add_url_rule(
"/products/<string:product_slug>/<string:deployment_slug>",
view_func=views.update_product_deployment,
methods=["PUT"],
)


@app.errorhandler(422)
Expand Down
19 changes: 19 additions & 0 deletions webapp/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,25 @@ class CreateProductDeploymentBodySchema(NormalizeNameMixin, Schema):
)


class UpdateProductDeploymentBodySchema(NormalizeNameMixin, Schema):
"""
Schema for PUT /products/<product_slug>/<deployment_slug> request body.
"""

name = fields.String(required=False)
artifact_type = fields.String(
required=False, validate=OneOf(ARTIFACT_TYPES)
)

@validates_schema
def validate_at_least_one_field(self, data, **kwargs):
if "name" not in data and "artifact_type" not in data:
raise ValidationError(
"At least one of 'name' or 'artifact_type' must be provided.",
field_name="_schema",
)


class UpdateProductBodySchema(NormalizeNameMixin, Schema):
"""Schema for PUT /products/<product_slug> request body."""

Expand Down
56 changes: 56 additions & 0 deletions webapp/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
DeploymentSchema,
GetProductsQuerySchema,
ProductSchema,
UpdateProductDeploymentBodySchema,
UpdateProductBodySchema,
)

Expand Down Expand Up @@ -201,3 +202,58 @@ def update_product(product_slug, name):
}, 500

return ProductSchema().dump(product), 200


@use_kwargs(UpdateProductDeploymentBodySchema, location="json")
def update_product_deployment(
product_slug,
deployment_slug,
name=None,
artifact_type=None,
):
product = Product.query.filter_by(slug=product_slug).one_or_none()
if product is None:
return {
"error": {
"message": "Product not found.",
"details": {"product_slug": product_slug},
}
}, 404

deployment = (
Deployment.query.options(joinedload(Deployment.versions))
.filter_by(
parent_product=product_slug,
slug=deployment_slug,
)
.one_or_none()
)

if deployment is None:
return {
"error": {
"message": "Deployment not found.",
"details": {
"product_slug": product_slug,
"deployment_slug": deployment_slug,
},
}
}, 404

if name is not None:
deployment.name = name
if artifact_type is not None:
deployment.artifact_type = artifact_type

try:
db.session.commit()
except Exception:
db.session.remove()
return {
"error": {
"message": "Internal server error.",
"details": {},
}
}, 500

return DeploymentSchema().dump(deployment), 200
Loading