Repository: getmoto/moto
Pull request #8947 (2025-06-03 21:05:12)

[NetworkFirewall] Adding of UpdateToken attribute
Added the `update_token` NetworkFirewall's to the `describe_firewall` Response. UpdateToken also used in other client API calls for future usage.


Patch:
diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md
index 3d35c5c87..715256649 100644
--- a/IMPLEMENTATION_COVERAGE.md
+++ b/IMPLEMENTATION_COVERAGE.md
@@ -1012,7 +1012,7 @@
 <summary>10% implemented</summary>
 
 - [ ] add_facet_to_object
-- [ ] apply_schema
+- [X] apply_schema
 - [ ] attach_object
 - [ ] attach_policy
 - [ ] attach_to_index
@@ -1023,12 +1023,12 @@
 - [ ] create_facet
 - [ ] create_index
 - [ ] create_object
-- [ ] create_schema
+- [X] create_schema
 - [ ] create_typed_link_facet
 - [X] delete_directory
 - [ ] delete_facet
 - [ ] delete_object
-- [ ] delete_schema
+- [X] delete_schema
 - [ ] delete_typed_link_facet
 - [ ] detach_from_index
 - [ ] detach_object
@@ -1046,7 +1046,7 @@
 - [ ] get_typed_link_facet_information
 - [ ] list_applied_schema_arns
 - [ ] list_attached_indices
-- [ ] list_development_schema_arns
+- [X] list_development_schema_arns
 - [X] list_directories
 - [ ] list_facet_attributes
 - [ ] list_facet_names
@@ -1060,12 +1060,12 @@
 - [ ] list_object_policies
 - [ ] list_outgoing_typed_links
 - [ ] list_policy_attachments
-- [ ] list_published_schema_arns
+- [X] list_published_schema_arns
 - [X] list_tags_for_resource
 - [ ] list_typed_link_facet_attributes
 - [ ] list_typed_link_facet_names
 - [ ] lookup_policy
-- [ ] publish_schema
+- [X] publish_schema
 - [ ] put_schema_from_json
 - [ ] remove_facet_from_object
 - [X] tag_resource
@@ -4262,7 +4262,7 @@
 - [X] delete_partition
 - [ ] delete_partition_index
 - [X] delete_registry
-- [ ] delete_resource_policy
+- [X] delete_resource_policy
 - [X] delete_schema
 - [ ] delete_schema_versions
 - [ ] delete_security_configuration
diff --git a/moto/clouddirectory/exceptions.py b/moto/clouddirectory/exceptions.py
index 7b2d582ec..8e3892849 100644
--- a/moto/clouddirectory/exceptions.py
+++ b/moto/clouddirectory/exceptions.py
@@ -18,3 +18,33 @@ class InvalidArnException(JsonRESTError):
             "Message": "Invalid Arn",
         }
         self.description = json.dumps(body)
+
+
+class ResourceNotFoundException(JsonRESTError):
+    def __init__(self, resource_id: str):
+        super().__init__("ResourceNotFoundException", "Resource not found")
+        body = {
+            "ResourceId": resource_id,
+            "Message": "Resource not found",
+        }
+        self.description = json.dumps(body)
+
+
+class SchemaAlreadyPublishedException(JsonRESTError):
+    def __init__(self, schema_arn: str):
+        super().__init__("SchemaAlreadyPublishedException", "Schema already published")
+        body = {
+            "SchemaArn": schema_arn,
+            "Message": "Schema already published",
+        }
+        self.description = json.dumps(body)
+
+
+class SchemaAlreadyExistsException(JsonRESTError):
+    def __init__(self, schema_arn: str):
+        super().__init__("SchemaAlreadyExistsException", "Schema already exists")
+        body = {
+            "SchemaArn": schema_arn,
+            "Message": "Schema already exists",
+        }
+        self.description = json.dumps(body)
diff --git a/moto/clouddirectory/models.py b/moto/clouddirectory/models.py
index 40142e42b..f75ef4282 100644
--- a/moto/clouddirectory/models.py
+++ b/moto/clouddirectory/models.py
@@ -8,7 +8,11 @@ from moto.core.common_models import BaseModel
 from moto.utilities.paginator import paginate
 from moto.utilities.tagging_service import TaggingService
 
-from .exceptions import InvalidArnException
+from .exceptions import (
+    InvalidArnException,
+    ResourceNotFoundException,
+    SchemaAlreadyPublishedException,
+)
 
 PAGINATION_MODEL = {
     "list_directories": {
@@ -16,7 +20,19 @@ PAGINATION_MODEL = {
         "limit_key": "max_results",
         "limit_default": 100,
         "unique_attribute": "directory_arn",
-    }
+    },
+    "list_development_schema_arns": {
+        "input_token": "next_token",
+        "limit_key": "max_results",
+        "limit_default": 100,
+        "unique_attribute": "schema_arn",
+    },
+    "list_published_schema_arns": {
+        "input_token": "next_token",
+        "limit_key": "max_results",
+        "limit_default": 100,
+        "unique_attribute": "schema_arn",
+    },
 }
 
 
@@ -50,13 +66,62 @@ class CloudDirectoryBackend(BaseBackend):
     def __init__(self, region_name: str, account_id: str) -> None:
         super().__init__(region_name, account_id)
         self.directories: Dict[str, Directory] = {}
+        self.schemas_states: Dict[str, List[str]] = {
+            "development": [],
+            "published": [],
+            "applied": [],
+        }
         self.tagger = TaggingService()
 
+    def apply_schema(self, directory_arn: str, published_schema_arn: str) -> None:
+        directory = self.directories.get(directory_arn)
+        if not directory:
+            raise ResourceNotFoundException(directory_arn)
+        if published_schema_arn not in self.schemas_states["published"]:
+            raise ResourceNotFoundException(published_schema_arn)
+        directory.schema_arn = published_schema_arn
+        return
+
+    def publish_schema(
+        self, name: str, version: str, development_schema_arn: str, minor_version: str
+    ) -> str:
+        schema_arn = f"arn:aws:clouddirectory:{self.region_name}:{self.account_id}:schema/published/{name}/{version}/{minor_version}"
+        if development_schema_arn in self.schemas_states["published"]:
+            raise SchemaAlreadyPublishedException(development_schema_arn)
+        if development_schema_arn in self.schemas_states["development"]:
+            self.schemas_states["development"].remove(development_schema_arn)
+            self.schemas_states["published"].append(schema_arn)
+        else:
+            raise ResourceNotFoundException(development_schema_arn)
+        return schema_arn
+
     def create_directory(self, name: str, schema_arn: str) -> Directory:
         directory = Directory(self.account_id, self.region_name, name, schema_arn)
         self.directories[directory.directory_arn] = directory
         return directory
 
+    def create_schema(self, name: str) -> str:
+        self.schema_arn = f"arn:aws:clouddirectory:{self.region_name}:{self.account_id}:schema/development/{name}"
+        self.schemas_states["development"].append(self.schema_arn)
+        return self.schema_arn
+
+    def delete_schema(self, schema_arn: str) -> None:
+        if schema_arn in self.schemas_states["development"]:
+            self.schemas_states["development"].remove(schema_arn)
+        elif schema_arn in self.schemas_states["published"]:
+            self.schemas_states["published"].remove(schema_arn)
+        else:
+            raise ResourceNotFoundException(schema_arn)
+        return
+
+    @paginate(pagination_model=PAGINATION_MODEL)
+    def list_development_schema_arns(self) -> List[str]:
+        return self.schemas_states["development"]
+
+    @paginate(pagination_model=PAGINATION_MODEL)
+    def list_published_schema_arns(self) -> List[str]:
+        return self.schemas_states["published"]
+
     @paginate(pagination_model=PAGINATION_MODEL)
     def list_directories(self, state: str) -> List[Directory]:
         directories = list(self.directories.values())
diff --git a/moto/clouddirectory/responses.py b/moto/clouddirectory/responses.py
index c967791de..3038ee26c 100644
--- a/moto/clouddirectory/responses.py
+++ b/moto/clouddirectory/responses.py
@@ -18,9 +18,36 @@ class CloudDirectoryResponse(BaseResponse):
         """Return backend instance specific for this region."""
         return clouddirectory_backends[self.current_account][self.region]
 
+    def apply_schema(self) -> str:
+        directory_arn = self.headers.get("x-amz-data-partition")
+        published_schema_arn = self._get_param("PublishedSchemaArn")
+        self.clouddirectory_backend.apply_schema(
+            directory_arn=directory_arn,
+            published_schema_arn=published_schema_arn,
+        )
+        return json.dumps(
+            {
+                "AppliedSchemaArn": published_schema_arn,
+                "DirectoryArn": directory_arn,
+            }
+        )
+
+    def publish_schema(self) -> str:
+        development_schema_arn = self.headers.get("x-amz-data-partition")
+        version = self._get_param("Version")
+        minor_version = self._get_param("MinorVersion")
+        name = self._get_param("Name")
+        schema = self.clouddirectory_backend.publish_schema(
+            name=name,
+            version=version,
+            minor_version=minor_version,
+            development_schema_arn=development_schema_arn,
+        )
+        return json.dumps({"PublishedSchemaArn": schema})
+
     def create_directory(self) -> str:
         name = self._get_param("Name")
-        schema_arn = self._get_param("SchemaArn")
+        schema_arn = self.headers.get("x-amz-data-partition")
         directory = self.clouddirectory_backend.create_directory(
             name=name,
             schema_arn=schema_arn,
@@ -35,6 +62,13 @@ class CloudDirectoryResponse(BaseResponse):
             )
         )
 
+    def create_schema(self) -> str:
+        name = self._get_param("Name")
+        schema = self.clouddirectory_backend.create_schema(
+            name=name,
+        )
+        return json.dumps(dict(SchemaArn=schema))
+
     def list_directories(self) -> str:
         next_token = self._get_param("NextToken")
         max_results = self._get_param("MaxResults")
@@ -72,6 +106,33 @@ class CloudDirectoryResponse(BaseResponse):
         )
         return json.dumps(dict(DirectoryArn=directory_arn))
 
+    def delete_schema(self) -> str:
+        # Retrieve arn from headers
+        # https://docs.aws.amazon.com/clouddirectory/latest/APIReference/API_DeleteSchema.html
+        arn = self.headers.get("x-amz-data-partition")
+        self.clouddirectory_backend.delete_schema(
+            schema_arn=arn,
+        )
+        return json.dumps(dict(SchemaArn=arn))
+
+    def list_development_schema_arns(self) -> str:
+        next_token = self._get_param("NextToken")
+        max_results = self._get_param("MaxResults")
+        schemas, next_token = self.clouddirectory_backend.list_development_schema_arns(
+            next_token=next_token,
+            max_results=max_results,
+        )
+        return json.dumps(dict(SchemaArns=schemas, NextToken=next_token))
+
+    def list_published_schema_arns(self) -> str:
+        next_token = self._get_param("NextToken")
+        max_results = self._get_param("MaxResults")
+        schemas, next_token = self.clouddirectory_backend.list_published_schema_arns(
+            next_token=next_token,
+            max_results=max_results,
+        )
+        return json.dumps(dict(SchemaArns=schemas, NextToken=next_token))
+
     def get_directory(self) -> str:
         # Retrieve arn from headers
         # https://docs.aws.amazon.com/clouddirectory/latest/APIReference/API_GetDirectory.html
diff --git a/moto/clouddirectory/urls.py b/moto/clouddirectory/urls.py
index e82051ba4..ef70b8a98 100644
--- a/moto/clouddirectory/urls.py
+++ b/moto/clouddirectory/urls.py
@@ -8,10 +8,16 @@ url_bases = [
 
 url_paths = {
     "{0}/amazonclouddirectory/2017-01-11/directory/create$": CloudDirectoryResponse.dispatch,
+    "{0}/amazonclouddirectory/2017-01-11/schema/create$": CloudDirectoryResponse.dispatch,
     "{0}/amazonclouddirectory/2017-01-11/directory/list$": CloudDirectoryResponse.dispatch,
     "{0}/amazonclouddirectory/2017-01-11/tags/add$": CloudDirectoryResponse.dispatch,
     "{0}/amazonclouddirectory/2017-01-11/tags/remove$": CloudDirectoryResponse.dispatch,
     "{0}/amazonclouddirectory/2017-01-11/directory$": CloudDirectoryResponse.dispatch,
     "{0}/amazonclouddirectory/2017-01-11/directory/get$": CloudDirectoryResponse.dispatch,
     "{0}/amazonclouddirectory/2017-01-11/tags$": CloudDirectoryResponse.dispatch,
+    "{0}/amazonclouddirectory/2017-01-11/schema$": CloudDirectoryResponse.dispatch,
+    "{0}/amazonclouddirectory/2017-01-11/schema/development$": CloudDirectoryResponse.dispatch,
+    "{0}/amazonclouddirectory/2017-01-11/schema/published$": CloudDirectoryResponse.dispatch,
+    "{0}/amazonclouddirectory/2017-01-11/schema/apply$": CloudDirectoryResponse.dispatch,
+    "{0}/amazonclouddirectory/2017-01-11/schema/publish$": CloudDirectoryResponse.dispatch,
 }
diff --git a/moto/glue/models.py b/moto/glue/models.py
index a81bafc42..af3a9e621 100644
--- a/moto/glue/models.py
+++ b/moto/glue/models.py
@@ -1360,6 +1360,32 @@ class GlueBackend(BaseBackend):
 
         return response
 
+    def delete_resource_policy(
+        self,
+        resource_arn: Optional[str] = None,
+        policy_hash_condition: Optional[str] = None,
+    ) -> Dict[str, str]:
+        if resource_arn is None:
+            resource_arn = self.default_catalog_arn
+
+        if resource_arn not in self.resource_policies:
+            raise EntityNotFoundException(
+                "Policy does not exist",
+            )
+        policy_dict = self.resource_policies.get(resource_arn, {})
+
+        if (
+            policy_hash_condition
+            and policy_dict
+            and policy_hash_condition != policy_dict.get("PolicyHash")
+        ):
+            raise JsonRESTError(
+                "ConcurrentModificationException", "Policy hash condition not met"
+            )
+
+        del self.resource_policies[resource_arn]
+        return {}
+
 
 class FakeDatabase(BaseModel):
     def __init__(
diff --git a/moto/glue/responses.py b/moto/glue/responses.py
index 6effc42b3..8ff93753d 100644
--- a/moto/glue/responses.py
+++ b/moto/glue/responses.py
@@ -910,3 +910,14 @@ class GlueResponse(BaseResponse):
         )
 
         return json.dumps(response, cls=DateTimeEncoder)
+
+    def delete_resource_policy(self) -> str:
+        params = json.loads(self.body)
+        policy_hash_condition = params.get("PolicyHashCondition")
+        resource_arn = params.get("ResourceArn")
+        self.glue_backend.delete_resource_policy(
+            resource_arn=resource_arn,
+            policy_hash_condition=policy_hash_condition,
+        )
+
+        return "{}"
diff --git a/moto/networkfirewall/models.py b/moto/networkfirewall/models.py
index 7e8ed45e8..7ae859aa6 100644
--- a/moto/networkfirewall/models.py
+++ b/moto/networkfirewall/models.py
@@ -57,6 +57,7 @@ class NetworkFirewallModel(BaseModel):
 
         self.arn = f"arn:aws:network-firewall:{region_name}:{account_id}:firewall/{self.firewall_name}"
 
+        self.update_token = "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d"
         self.firewall_status = {
             "Status": "READY",
             "ConfigurationSyncStateSummary": "IN_SYNC",
diff --git a/moto/networkfirewall/responses.py b/moto/networkfirewall/responses.py
index 4f7d30391..d3175a996 100644
--- a/moto/networkfirewall/responses.py
+++ b/moto/networkfirewall/responses.py
@@ -102,6 +102,7 @@ class NetworkFirewallResponse(BaseResponse):
         )
         return json.dumps(
             dict(
+                UpdateToken=firewall.update_token,
                 Firewall=firewall.to_dict(),
                 FirewallStatus=firewall.firewall_status,
             )


Test patch:
diff --git a/tests/test_clouddirectory/test_clouddirectory.py b/tests/test_clouddirectory/test_clouddirectory.py
index 9aa08fcc8..36cbe59dc 100644
--- a/tests/test_clouddirectory/test_clouddirectory.py
+++ b/tests/test_clouddirectory/test_clouddirectory.py
@@ -1,6 +1,7 @@
 """Unit tests for clouddirectory-supported APIs."""
 
 import boto3
+import pytest
 
 from moto import mock_aws
 from tests import DEFAULT_ACCOUNT_ID
@@ -10,9 +11,8 @@ from tests import DEFAULT_ACCOUNT_ID
 def test_create_directory():
     region = "us-west-2"
     client = boto3.client("clouddirectory", region_name=region)
-    schema_arn = (
-        f"arn:aws:clouddirectory:{region}:{DEFAULT_ACCOUNT_ID}:directory/test-schema/1"
-    )
+    schema = client.create_schema(Name="test-schema")
+    schema_arn = schema["SchemaArn"]
     resp = client.create_directory(SchemaArn=schema_arn, Name="test-directory")
     assert (
         resp["DirectoryArn"]
@@ -20,6 +20,109 @@ def test_create_directory():
     )
     assert resp["Name"] == "test-directory"
     assert "ObjectIdentifier" in resp
+    assert resp["AppliedSchemaArn"] == schema_arn
+
+
+@mock_aws
+def test_create_schema():
+    region = "us-west-2"
+    client = boto3.client("clouddirectory", region_name=region)
+    resp = client.create_schema(Name="test-schema")
+    assert (
+        resp["SchemaArn"]
+        == f"arn:aws:clouddirectory:{region}:{DEFAULT_ACCOUNT_ID}:schema/development/test-schema"
+    )
+
+
+@mock_aws
+def test_apply_schema():
+    region = "us-west-2"
+    client = boto3.client("clouddirectory", region_name=region)
+    schema = client.create_schema(Name="test-schema")
+    schema_arn = schema["SchemaArn"]
+    pub_schema = client.publish_schema(
+        DevelopmentSchemaArn=schema_arn,
+        Name="test-schema",
+        Version="1",
+        MinorVersion="0",
+    )
+    pub_schema_arn = pub_schema["PublishedSchemaArn"]
+    directory = client.create_directory(SchemaArn=pub_schema_arn, Name="test-directory")
+    directory_arn = directory["DirectoryArn"]
+    resp = client.apply_schema(
+        PublishedSchemaArn=pub_schema_arn, DirectoryArn=directory_arn
+    )
+    assert resp["AppliedSchemaArn"] == pub_schema_arn
+    assert resp["DirectoryArn"] == directory_arn
+
+
+@mock_aws
+def test_publish_schema():
+    region = "us-west-2"
+    client = boto3.client("clouddirectory", region_name=region)
+    schema = client.create_schema(Name="test-schema")
+    schema_arn = schema["SchemaArn"]
+    resp = client.publish_schema(
+        DevelopmentSchemaArn=schema_arn,
+        Name="test-schema",
+        Version="1",
+        MinorVersion="0",
+    )
+    assert (
+        resp["PublishedSchemaArn"]
+        == f"arn:aws:clouddirectory:{region}:{DEFAULT_ACCOUNT_ID}:schema/published/test-schema/1/0"
+    )
+
+
+@mock_aws
+def test_list_development_schema_arns():
+    region = "us-west-2"
+    client = boto3.client("clouddirectory", region_name=region)
+    schema_arn1 = client.create_schema(Name="test-schema1")["SchemaArn"]
+    schema_arn2 = client.create_schema(Name="test-schema2")["SchemaArn"]
+
+    resp = client.list_development_schema_arns()
+    assert len(resp["SchemaArns"]) == 2
+    assert schema_arn1 in resp["SchemaArns"]
+    assert schema_arn2 in resp["SchemaArns"]
+
+
+@mock_aws
+def test_list_published_schema_arns():
+    region = "us-west-2"
+    client = boto3.client("clouddirectory", region_name=region)
+    schema_arn1 = client.create_schema(Name="test-schema1")["SchemaArn"]
+    schema_arn2 = client.create_schema(Name="test-schema2")["SchemaArn"]
+
+    published_arn1 = client.publish_schema(
+        DevelopmentSchemaArn=schema_arn1,
+        Name="test-schema1",
+        Version="1",
+        MinorVersion="0",
+    )["PublishedSchemaArn"]
+
+    published_arn2 = client.publish_schema(
+        DevelopmentSchemaArn=schema_arn2,
+        Name="test-schema2",
+        Version="1",
+        MinorVersion="0",
+    )["PublishedSchemaArn"]
+
+    resp = client.list_published_schema_arns()
+    assert len(resp["SchemaArns"]) == 2
+    assert published_arn1 in resp["SchemaArns"]
+    assert published_arn2 in resp["SchemaArns"]
+
+
+@mock_aws
+def test_delete_schema():
+    region = "us-west-2"
+    client = boto3.client("clouddirectory", region_name=region)
+    schema_arn = client.create_schema(Name="test-schema")["SchemaArn"]
+    client.create_directory(SchemaArn=schema_arn, Name="test-directory")
+    client.delete_schema(SchemaArn=schema_arn)
+    resp = client.list_development_schema_arns()
+    assert len(resp["SchemaArns"]) == 0
 
 
 @mock_aws
@@ -152,3 +255,139 @@ def test_get_directory():
     assert directory["DirectoryArn"] == directory_arns[0]
     assert directory["State"] == "ENABLED"
     assert "CreationDateTime" in directory
+
+
+@mock_aws
+def test_apply_schema_with_nonexistent_schema():
+    region = "us-west-2"
+    client = boto3.client("clouddirectory", region_name=region)
+
+    schema = client.create_schema(Name="test-schema")
+    schema_arn = schema["SchemaArn"]
+    pub_schema = client.publish_schema(
+        DevelopmentSchemaArn=schema_arn,
+        Name="test-schema",
+        Version="1",
+        MinorVersion="0",
+    )
+    pub_schema_arn = pub_schema["PublishedSchemaArn"]
+    directory = client.create_directory(SchemaArn=pub_schema_arn, Name="test-directory")
+    directory_arn = directory["DirectoryArn"]
+
+    non_existent_schema_arn = (
+        f"arn:aws:clouddirectory:{region}:123456789012:schema/published/nonexistent/1/0"
+    )
+
+    with pytest.raises(client.exceptions.ResourceNotFoundException):
+        client.apply_schema(
+            PublishedSchemaArn=non_existent_schema_arn, DirectoryArn=directory_arn
+        )
+
+
+@mock_aws
+def test_apply_schema_updates_directory_schema():
+    region = "us-west-2"
+    client = boto3.client("clouddirectory", region_name=region)
+
+    schema1 = client.create_schema(Name="test-schema1")
+    schema1_arn = schema1["SchemaArn"]
+    pub_schema1 = client.publish_schema(
+        DevelopmentSchemaArn=schema1_arn,
+        Name="test-schema1",
+        Version="1",
+        MinorVersion="0",
+    )
+    pub_schema1_arn = pub_schema1["PublishedSchemaArn"]
+
+    directory = client.create_directory(
+        SchemaArn=pub_schema1_arn, Name="test-directory"
+    )
+    directory_arn = directory["DirectoryArn"]
+
+    schema2 = client.create_schema(Name="test-schema2")
+    schema2_arn = schema2["SchemaArn"]
+    pub_schema2 = client.publish_schema(
+        DevelopmentSchemaArn=schema2_arn,
+        Name="test-schema2",
+        Version="1",
+        MinorVersion="0",
+    )
+    pub_schema2_arn = pub_schema2["PublishedSchemaArn"]
+
+    # Apply second schema to directory
+    resp = client.apply_schema(
+        PublishedSchemaArn=pub_schema2_arn, DirectoryArn=directory_arn
+    )
+
+    # Verify schema was updated
+    assert resp["AppliedSchemaArn"] == pub_schema2_arn
+
+
+@mock_aws
+def test_publish_nonexistent_schema():
+    region = "us-west-2"
+    client = boto3.client("clouddirectory", region_name=region)
+
+    non_existent_schema_arn = f"arn:aws:clouddirectory:{region}:{DEFAULT_ACCOUNT_ID}:schema/development/nonexistent"
+
+    with pytest.raises(client.exceptions.ResourceNotFoundException):
+        client.publish_schema(
+            DevelopmentSchemaArn=non_existent_schema_arn,
+            Name="nonexistent",
+            Version="1",
+            MinorVersion="0",
+        )
+
+
+@mock_aws
+def test_publish_schema_state_transition():
+    region = "us-west-2"
+    client = boto3.client("clouddirectory", region_name=region)
+
+    dev_schema = client.create_schema(Name="test-schema")
+    dev_schema_arn = dev_schema["SchemaArn"]
+
+    dev_schemas = client.list_development_schema_arns()
+    assert dev_schema_arn in dev_schemas["SchemaArns"]
+
+    published = client.publish_schema(
+        DevelopmentSchemaArn=dev_schema_arn,
+        Name="test-schema",
+        Version="1",
+        MinorVersion="0",
+    )
+    published_schema_arn = published["PublishedSchemaArn"]
+
+    dev_schemas = client.list_development_schema_arns()
+    pub_schemas = client.list_published_schema_arns()
+
+    assert dev_schema_arn not in dev_schemas["SchemaArns"]
+    assert published_schema_arn in pub_schemas["SchemaArns"]
+    assert (
+        published_schema_arn
+        == f"arn:aws:clouddirectory:{region}:{DEFAULT_ACCOUNT_ID}:schema/published/test-schema/1/0"
+    )
+
+
+@mock_aws
+def test_publish_already_published_schema():
+    region = "us-west-2"
+    client = boto3.client("clouddirectory", region_name=region)
+
+    schema = client.create_schema(Name="test-schema")
+    schema_arn = schema["SchemaArn"]
+    new_schema_arn = client.publish_schema(
+        DevelopmentSchemaArn=schema_arn,
+        Name="test-schema",
+        Version="1",
+        MinorVersion="0",
+    )
+
+    # Try to publish again with same development schema
+    with pytest.raises(client.exceptions.SchemaAlreadyPublishedException):
+        client.publish_schema(
+            DevelopmentSchemaArn=new_schema_arn["PublishedSchemaArn"],
+            Name="test-schema",
+            Version="1",
+            MinorVersion="0",
+        )
diff --git a/tests/test_glue/test_glue.py b/tests/test_glue/test_glue.py
index 01bbedd85..4ffb30a14 100644
--- a/tests/test_glue/test_glue.py
+++ b/tests/test_glue/test_glue.py
@@ -1287,3 +1287,74 @@ def test_put_resource_policy_error_conditions():
             PolicyExistsCondition="MUST_EXIST",
         )
     assert "ResourceNumberLimitExceededException" in str(excinfo.value)
+
+
+@mock_aws
+def test_delete_resource_policy():
+    client = boto3.client("glue", region_name="us-east-1")
+
+    policy_json = json.dumps(
+        {
+            "Version": "2012-10-17",
+            "Statement": [
+                {
+                    "Effect": "Allow",
+                    "Principal": {"Service": "glue.amazonaws.com"},
+                    "Action": ["glue:*"],
+                    "Resource": ["*"],
+                }
+            ],
+        }
+    )
+    catalog_arn = "arn:aws:glue:us-east-1:123456789012:catalog"
+    client.put_resource_policy(PolicyInJson=policy_json, ResourceArn=catalog_arn)
+    catalog_response = client.get_resource_policy(ResourceArn=catalog_arn)
+
+    assert "PolicyInJson" in catalog_response
+    assert "PolicyHash" in catalog_response
+
+    # Delete resource policy
+    client.delete_resource_policy(ResourceArn=catalog_arn)
+
+    catalog_response = client.get_resource_policy(ResourceArn=catalog_arn)
+    assert "PolicyInJson" not in catalog_response
+    assert "PolicyHash" not in catalog_response
+
+
+@mock_aws
+def test_delete_resource_policy_error_conditions():
+    client = boto3.client("glue", region_name="us-east-1")
+
+    policy_json = json.dumps(
+        {
+            "Version": "2012-10-17",
+            "Statement": [
+                {
+                    "Effect": "Allow",
+                    "Principal": {"Service": "glue.amazonaws.com"},
+                    "Action": ["glue:*"],
+                    "Resource": ["*"],
+                }
+            ],
+        }
+    )
+    catalog_arn = "arn:aws:glue:us-east-1:123456789012:catalog"
+
+    # Delete resource policy that does not exist.
+    with pytest.raises(ClientError) as error_info:
+        client.delete_resource_policy(ResourceArn=catalog_arn)
+
+    assert error_info.value.response["Error"]["Code"] == "EntityNotFoundException"
+
+    # Create resource policy
+    catalog_arn = "arn:aws:glue:us-east-1:123456789012:catalog"
+    client.put_resource_policy(PolicyInJson=policy_json, ResourceArn=catalog_arn)
+
+    with pytest.raises(ClientError) as error_info:
+        client.delete_resource_policy(
+            ResourceArn=catalog_arn,
+            PolicyHashCondition="incorrect-hash",
+        )
+    assert (
+        error_info.value.response["Error"]["Code"] == "ConcurrentModificationException"
+    )
diff --git a/tests/test_networkfirewall/test_networkfirewall.py b/tests/test_networkfirewall/test_networkfirewall.py
index acebcdcc3..5b6789131 100644
--- a/tests/test_networkfirewall/test_networkfirewall.py
+++ b/tests/test_networkfirewall/test_networkfirewall.py
@@ -141,6 +141,7 @@ def test_describe_firewall():
     assert resp["Firewall"]["FirewallPolicyChangeProtection"] is False
     assert resp["Firewall"]["Description"] == "Test firewall"
     assert resp["Firewall"]["Tags"] == [{"Key": "Name", "Value": "test-firewall"}]
+    assert resp["UpdateToken"] == "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d"
 
     # Describe the firewall using the name
     resp_name = client.describe_firewall(FirewallName="test-firewall")