Repository: Azure/typespec-azure
Pull request #2440: azure-http-specs, fix mockapi in flattenProperties and ARM cases
Author: login: weidongxu-microsoft
Created: 2025-03-25T06:51:36Z | Merged: 2025-03-26T05:55:47Z

- fix simple error in flattenProperties and ARM PUT
- fix content-type for ARM PATCH -- it should be "application/json" (this does not affect test, though)
- add handler func for ARM PATCH, that only validate the property that matters (PATCH allows unchanged property either not sent, or sent with old value)


Diff:
diff --git a/.chronus/changes/azure-http-specs_fix-mockapi-2025-2-25-15-59-52.md b/.chronus/changes/azure-http-specs_fix-mockapi-2025-2-25-15-59-52.md
new file mode 100644
index 0000000000..e42c5cc0f7
--- /dev/null
+++ b/.chronus/changes/azure-http-specs_fix-mockapi-2025-2-25-15-59-52.md
@@ -0,0 +1,7 @@
+---
+changeKind: fix
+packages:
+  - "@azure-tools/azure-http-specs"
+---
+
+Fix mockapi for flattenProperties and ARM
\ No newline at end of file
diff --git a/packages/azure-http-specs/specs/azure/client-generator-core/flatten-property/mockapi.ts b/packages/azure-http-specs/specs/azure/client-generator-core/flatten-property/mockapi.ts
index 90ea9e0daf..8b5205d8f9 100644
--- a/packages/azure-http-specs/specs/azure/client-generator-core/flatten-property/mockapi.ts
+++ b/packages/azure-http-specs/specs/azure/client-generator-core/flatten-property/mockapi.ts
@@ -6,7 +6,7 @@ function createMockApiDefinitions(route: string, request: any, response: any): M
     uri: `/azure/client-generator-core/flatten-property/${route}`,
     method: "put",
     request: {
-      body: request,
+      body: json(request),
     },
     response: {
       status: 200,
diff --git a/packages/azure-http-specs/specs/azure/resource-manager/common-properties/mockapi.ts b/packages/azure-http-specs/specs/azure/resource-manager/common-properties/mockapi.ts
index b7c49d8db6..599d16b9bd 100644
--- a/packages/azure-http-specs/specs/azure/resource-manager/common-properties/mockapi.ts
+++ b/packages/azure-http-specs/specs/azure/resource-manager/common-properties/mockapi.ts
@@ -1,4 +1,11 @@
-import { json, passOnCode, passOnSuccess, ScenarioMockApi } from "@typespec/spec-api";
+import { deepEquals } from "@typespec/compiler/utils";
+import {
+  json,
+  passOnCode,
+  passOnSuccess,
+  ScenarioMockApi,
+  ValidationError,
+} from "@typespec/spec-api";
 
 export const Scenarios: Record<string, ScenarioMockApi> = {};
 
@@ -88,6 +95,11 @@ Scenarios.Azure_ResourceManager_CommonProperties_ManagedIdentity_createWithSyste
     method: "put",
     request: {
       body: json({
+        location: "eastus",
+        tags: {
+          tagKey1: "tagValue1",
+        },
+        properties: {},
         identity: createExpectedIdentity,
       }),
       pathParams: {
@@ -104,6 +116,21 @@ Scenarios.Azure_ResourceManager_CommonProperties_ManagedIdentity_createWithSyste
       body: json(validSystemAssignedManagedIdentityResource),
     },
     kind: "MockApiDefinition",
+    handler: (req) => {
+      // .NET SDK would not send "properties" property, if it is empty.
+      // Hence here we only verify "identity" property.
+      if (!deepEquals(req.body["identity"], createExpectedIdentity)) {
+        throw new ValidationError(
+          "Body should contain 'identity' property",
+          createExpectedIdentity,
+          req.body,
+        );
+      }
+      return {
+        status: 200,
+        body: json(validSystemAssignedManagedIdentityResource),
+      };
+    },
   });
 
 Scenarios.Azure_ResourceManager_CommonProperties_ManagedIdentity_updateWithUserAssignedAndSystemAssigned =
@@ -114,9 +141,6 @@ Scenarios.Azure_ResourceManager_CommonProperties_ManagedIdentity_updateWithUserA
       body: json({
         identity: updateExpectedIdentity,
       }),
-      headers: {
-        "Content-Type": "application/merge-patch+json",
-      },
       pathParams: {
         subscriptionId: SUBSCRIPTION_ID_EXPECTED,
         resourceGroup: RESOURCE_GROUP_EXPECTED,
@@ -131,6 +155,19 @@ Scenarios.Azure_ResourceManager_CommonProperties_ManagedIdentity_updateWithUserA
       body: json(validUserAssignedAndSystemAssignedManagedIdentityResource),
     },
     kind: "MockApiDefinition",
+    handler: (req) => {
+      if (!deepEquals(req.body["identity"], updateExpectedIdentity)) {
+        throw new ValidationError(
+          "Body should contain 'identity' property",
+          updateExpectedIdentity,
+          req.body,
+        );
+      }
+      return {
+        status: 200,
+        body: json(validUserAssignedAndSystemAssignedManagedIdentityResource),
+      };
+    },
   });
 
 Scenarios.Azure_ResourceManager_CommonProperties_Error_getForPredefinedError = passOnCode(404, {
@@ -165,6 +202,7 @@ Scenarios.Azure_ResourceManager_CommonProperties_Error_createForUserDefinedError
   method: "put",
   request: {
     body: json({
+      location: "eastus",
       properties: {
         username: "00",
       },
diff --git a/packages/azure-http-specs/specs/azure/resource-manager/resources/mockapi.ts b/packages/azure-http-specs/specs/azure/resource-manager/resources/mockapi.ts
index cd452f0f93..dfd1413047 100644
--- a/packages/azure-http-specs/specs/azure/resource-manager/resources/mockapi.ts
+++ b/packages/azure-http-specs/specs/azure/resource-manager/resources/mockapi.ts
@@ -1,4 +1,4 @@
-import { json, passOnSuccess, ScenarioMockApi } from "@typespec/spec-api";
+import { json, passOnSuccess, ScenarioMockApi, ValidationError } from "@typespec/spec-api";
 
 export const Scenarios: Record<string, ScenarioMockApi> = {};
 
@@ -344,9 +344,6 @@ Scenarios.Azure_ResourceManager_Resources_ExtensionsResources_update = passOnSuc
           description: "valid2",
         },
       }),
-      headers: {
-        "Content-Type": "application/json",
-      },
     },
     response: {
       status: 200,
@@ -376,9 +373,6 @@ Scenarios.Azure_ResourceManager_Resources_ExtensionsResources_update = passOnSuc
           description: "valid2",
         },
       }),
-      headers: {
-        "Content-Type": "application/json",
-      },
     },
     response: {
       status: 200,
@@ -407,9 +401,6 @@ Scenarios.Azure_ResourceManager_Resources_ExtensionsResources_update = passOnSuc
           description: "valid2",
         },
       }),
-      headers: {
-        "Content-Type": "application/json",
-      },
     },
     response: {
       status: 200,
@@ -441,9 +432,6 @@ Scenarios.Azure_ResourceManager_Resources_ExtensionsResources_update = passOnSuc
           description: "valid2",
         },
       }),
-      headers: {
-        "Content-Type": "application/json",
-      },
     },
     response: {
       status: 200,
@@ -674,9 +662,6 @@ Scenarios.Azure_ResourceManager_Resources_LocationResources_update = passOnSucce
         description: "valid2",
       },
     }),
-    headers: {
-      "Content-Type": "application/json",
-    },
   },
   response: {
     status: 200,
@@ -790,9 +775,6 @@ Scenarios.Azure_ResourceManager_Resources_Singleton_update = passOnSuccess({
         description: "valid2",
       },
     }),
-    headers: {
-      "Content-Type": "application/merge-patch+json",
-    },
   },
   response: {
     status: 200,
@@ -805,6 +787,25 @@ Scenarios.Azure_ResourceManager_Resources_Singleton_update = passOnSuccess({
     }),
   },
   kind: "MockApiDefinition",
+  handler: (req) => {
+    if (req.body["properties"]["description"] !== "valid2") {
+      throw new ValidationError(
+        "Body should contain 'properties.description' property",
+        "valid2",
+        req.body,
+      );
+    }
+    return {
+      status: 200,
+      body: json({
+        ...validSingletonResource,
+        properties: {
+          provisioningState: "Succeeded",
+          description: "valid2",
+        },
+      }),
+    };
+  },
 });
 
 Scenarios.Azure_ResourceManager_Resources_Singleton_listByResourceGroup = passOnSuccess({
@@ -910,14 +911,11 @@ Scenarios.Azure_ResourceManager_Resources_TopLevel_update = passOnSuccess({
     query: {
       "api-version": "2023-12-01-preview",
     },
-    body: json(
-      {
-        properties: {
-          description: "valid2",
-        },
+    body: json({
+      properties: {
+        description: "valid2",
       },
-      "application/merge-patch+json",
-    ),
+    }),
   },
   response: {
     status: 200,
@@ -930,6 +928,25 @@ Scenarios.Azure_ResourceManager_Resources_TopLevel_update = passOnSuccess({
     }),
   },
   kind: "MockApiDefinition",
+  handler: (req) => {
+    if (req.body["properties"]["description"] !== "valid2") {
+      throw new ValidationError(
+        "Body should contain 'properties.description' property",
+        "valid2",
+        req.body,
+      );
+    }
+    return {
+      status: 200,
+      body: json({
+        ...validTopLevelResource,
+        properties: {
+          provisioningState: "Succeeded",
+          description: "valid2",
+        },
+      }),
+    };
+  },
 });
 
 Scenarios.Azure_ResourceManager_Resources_TopLevel_delete = passOnSuccess({
@@ -1045,14 +1062,11 @@ Scenarios.Azure_ResourceManager_Resources_Nested_update = passOnSuccess({
       nestedResourceName: "nested",
     },
     query: { "api-version": "2023-12-01-preview" },
-    body: json(
-      {
-        properties: {
-          description: "valid2",
-        },
+    body: json({
+      properties: {
+        description: "valid2",
       },
-      "application/merge-patch+json",
-    ),
+    }),
   },
   response: {
     status: 200,


Reviews:
user: login: weidongxu-microsoft
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-25T08:07:59Z
user: login: pshao25
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-25T08:11:12Z
user: login: weidongxu-microsoft
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-25T08:14:34Z
user: login: XiaofeiCao
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T08:23:04Z
user: login: pshao25
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T00:29:14Z
user: login: pshao25
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T00:29:23Z
user: login: timotheeguerin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T00:52:01Z
user: login: weidongxu-microsoft
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T01:04:21Z
user: login: weidongxu-microsoft
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T01:07:23Z
user: login: timotheeguerin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T01:22:31Z
user: login: pshao25
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T01:52:53Z
user: login: weidongxu-microsoft
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T02:00:10Z
user: login: pshao25
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T02:25:42Z
user: login: pshao25
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T02:28:03Z
user: login: weidongxu-microsoft
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T02:30:42Z
user: login: pshao25
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T03:05:39Z
user: login: weidongxu-microsoft
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T03:23:19Z
user: login: pshao25
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-26T03:56:30Z

Review comments:
diff_hunk: @@ -791,7 +791,7 @@ Scenarios.Azure_ResourceManager_Resources_Singleton_update = passOnSuccess({
       },
     }),
     headers: {
-      "Content-Type": "application/merge-patch+json",
path: packages/azure-http-specs/specs/azure/resource-manager/resources/mockapi.ts
user: login: weidongxu-microsoft
body: PATCH in ARM is "application/json" (though part of it behavior like a json-merge-patch)
https://github.com/Azure/azure-rest-api-specs/blob/main/specification/standbypool/resource-manager/Microsoft.StandbyPool/stable/2025-03-01/standbypool.json#L334
created_at: 2025-03-25T08:07:59Z
author_association: MEMBER
diff_hunk: @@ -791,7 +791,7 @@ Scenarios.Azure_ResourceManager_Resources_Singleton_update = passOnSuccess({
       },
     }),
     headers: {
-      "Content-Type": "application/merge-patch+json",
path: packages/azure-http-specs/specs/azure/resource-manager/resources/mockapi.ts
user: login: pshao25
body: We wrapped the body with `json`. Do we need to add the header once more?
created_at: 2025-03-25T08:11:12Z
author_association: MEMBER
diff_hunk: @@ -791,7 +791,7 @@ Scenarios.Azure_ResourceManager_Resources_Singleton_update = passOnSuccess({
       },
     }),
     headers: {
-      "Content-Type": "application/merge-patch+json",
path: packages/azure-http-specs/specs/azure/resource-manager/resources/mockapi.ts
user: login: weidongxu-microsoft
body: Emm, seems not. Other API does not have this `content-type` header.
Let me try remove it.

<-- removed
created_at: 2025-03-25T08:14:34Z
author_association: MEMBER
diff_hunk: @@ -916,7 +917,7 @@ Scenarios.Azure_ResourceManager_Resources_TopLevel_update = passOnSuccess({
           description: "valid2",
         },
       },
-      "application/merge-patch+json",
+      "application/json",
path: packages/azure-http-specs/specs/azure/resource-manager/resources/mockapi.ts
user: login: pshao25
body: Do we need this?
created_at: 2025-03-26T00:29:14Z
author_association: MEMBER
diff_hunk: @@ -1051,7 +1071,7 @@ Scenarios.Azure_ResourceManager_Resources_Nested_update = passOnSuccess({
           description: "valid2",
         },
       },
-      "application/merge-patch+json",
+      "application/json",
path: packages/azure-http-specs/specs/azure/resource-manager/resources/mockapi.ts
user: login: pshao25
body: And this?
created_at: 2025-03-26T00:29:23Z
author_association: MEMBER
diff_hunk: @@ -7,3 +7,5 @@ packages:
   - "!core/packages/http-client-csharp/**"
   - "!core/packages/http-client-java/**"
   - "!core/packages/http-client-python/**"
+  - "!core/packages/typespec-vs/**"
+  - "!core/packages/typespec-vscode/**"
path: pnpm-workspace.yaml
user: login: timotheeguerin
body: why is this added here? we do want to be able to build them here too, they are part of the core workspace
created_at: 2025-03-26T00:52:01Z
author_association: MEMBER
diff_hunk: @@ -7,3 +7,5 @@ packages:
   - "!core/packages/http-client-csharp/**"
   - "!core/packages/http-client-java/**"
   - "!core/packages/http-client-python/**"
+  - "!core/packages/typespec-vs/**"
+  - "!core/packages/typespec-vscode/**"
path: pnpm-workspace.yaml
user: login: weidongxu-microsoft
body: Sorry, this file should not be included, reverted (I typically do this in local, as build of these plugins required specific version of .NET lib) -- appreciate any suggestion that can do e.g. "only build essential lib on pnpm build" or "only build dependent lib of this azure-http-specs lib".
created_at: 2025-03-26T01:04:21Z
author_association: MEMBER
diff_hunk: @@ -916,7 +917,7 @@ Scenarios.Azure_ResourceManager_Resources_TopLevel_update = passOnSuccess({
           description: "valid2",
         },
       },
-      "application/merge-patch+json",
+      "application/json",
path: packages/azure-http-specs/specs/azure/resource-manager/resources/mockapi.ts
user: login: weidongxu-microsoft
body: Thanks Pan. Didn't aware there is 2 different ways to write this content-type...
created_at: 2025-03-26T01:07:23Z
author_association: MEMBER
diff_hunk: @@ -7,3 +7,5 @@ packages:
   - "!core/packages/http-client-csharp/**"
   - "!core/packages/http-client-java/**"
   - "!core/packages/http-client-python/**"
+  - "!core/packages/typespec-vs/**"
+  - "!core/packages/typespec-vscode/**"
path: pnpm-workspace.yaml
user: login: timotheeguerin
body: Hhm, they should skip if you don't have the right requirements locally.

Can you file an issue with what you see.

I think we might want to find a more generalized way to skip things if you don't have the right items 
created_at: 2025-03-26T01:22:31Z
author_association: MEMBER
diff_hunk: @@ -88,6 +95,11 @@ Scenarios.Azure_ResourceManager_CommonProperties_ManagedIdentity_createWithSyste
     method: "put",
     request: {
       body: json({
+        location: "eastus",
+        tags: {
+          tagKey1: "tagValue1",
+        },
+        properties: {},
path: packages/azure-http-specs/specs/azure/resource-manager/common-properties/mockapi.ts
user: login: pshao25
body: The empty `{}` is what Java needs?
created_at: 2025-03-26T01:52:53Z
author_association: MEMBER
diff_hunk: @@ -88,6 +95,11 @@ Scenarios.Azure_ResourceManager_CommonProperties_ManagedIdentity_createWithSyste
     method: "put",
     request: {
       body: json({
+        location: "eastus",
+        tags: {
+          tagKey1: "tagValue1",
+        },
+        properties: {},
path: packages/azure-http-specs/specs/azure/resource-manager/common-properties/mockapi.ts
user: login: weidongxu-microsoft
body: I think Java sends the empty object in `properties` property.

The resource has a model for `properties`. Just it only contain read-only property. So, in create, have the empty object looks correct to me.
https://github.com/Azure/typespec-azure/blob/a1ab10e20e53d4738f196d0df539bba01d842083/packages/azure-http-specs/specs/azure/resource-manager/common-properties/managed-identity.tsp#L18-L36

the serialize code of this model in Java is this. (de-serialize code would parse `provisioningState` property)
```
    @Override
    public JsonWriter toJson(JsonWriter jsonWriter) throws IOException {
        jsonWriter.writeStartObject();
        return jsonWriter.writeEndObject();
    }
```
created_at: 2025-03-26T02:00:10Z
author_association: MEMBER
diff_hunk: @@ -88,6 +95,11 @@ Scenarios.Azure_ResourceManager_CommonProperties_ManagedIdentity_createWithSyste
     method: "put",
     request: {
       body: json({
+        location: "eastus",
+        tags: {
+          tagKey1: "tagValue1",
+        },
+        properties: {},
path: packages/azure-http-specs/specs/azure/resource-manager/common-properties/mockapi.ts
user: login: pshao25
body: If user don't set the properties, shouldn't we send nothing?
created_at: 2025-03-26T02:25:42Z
author_association: MEMBER
diff_hunk: @@ -88,6 +95,11 @@ Scenarios.Azure_ResourceManager_CommonProperties_ManagedIdentity_createWithSyste
     method: "put",
     request: {
       body: json({
+        location: "eastus",
+        tags: {
+          tagKey1: "tagValue1",
+        },
+        properties: {},
path: packages/azure-http-specs/specs/azure/resource-manager/common-properties/mockapi.ts
user: login: pshao25
body: I tried to find out a workaround in .net to send the `{}`, but failed.
created_at: 2025-03-26T02:28:03Z
author_association: MEMBER
diff_hunk: @@ -88,6 +95,11 @@ Scenarios.Azure_ResourceManager_CommonProperties_ManagedIdentity_createWithSyste
     method: "put",
     request: {
       body: json({
+        location: "eastus",
+        tags: {
+          tagKey1: "tagValue1",
+        },
+        properties: {},
path: packages/azure-http-specs/specs/azure/resource-manager/common-properties/mockapi.ts
user: login: weidongxu-microsoft
body: Sending a `{}` and sending nothing is not exactly same...

Would Dapeng know about how this be sent? I assume ARM resource would send a resource `properties` on PUT in almost all cases. Just in this case there is no writable property in this model.

If had to, I can do a handler like the PATCH, that only verify the `identity` property.
created_at: 2025-03-26T02:30:42Z
author_association: MEMBER
diff_hunk: @@ -88,6 +95,11 @@ Scenarios.Azure_ResourceManager_CommonProperties_ManagedIdentity_createWithSyste
     method: "put",
     request: {
       body: json({
+        location: "eastus",
+        tags: {
+          tagKey1: "tagValue1",
+        },
+        properties: {},
path: packages/azure-http-specs/specs/azure/resource-manager/common-properties/mockapi.ts
user: login: pshao25
body: Our logic is if you don't touch the properties, then send nothing. Or maybe you can have a value set for `Properties.ProvisioningState` so that we can touch it?
created_at: 2025-03-26T03:05:39Z
author_association: MEMBER
diff_hunk: @@ -88,6 +95,11 @@ Scenarios.Azure_ResourceManager_CommonProperties_ManagedIdentity_createWithSyste
     method: "put",
     request: {
       body: json({
+        location: "eastus",
+        tags: {
+          tagKey1: "tagValue1",
+        },
+        properties: {},
path: packages/azure-http-specs/specs/azure/resource-manager/common-properties/mockapi.ts
user: login: weidongxu-microsoft
body: Yeah, I can add a writable property within `properties`. But it would require test code change in all emitters (as they now need to set this). -- maybe we can do this next time something breaks in this test

So here I just add a `handler` to again only verify the `identity` whether this test is for.
created_at: 2025-03-26T03:23:19Z
author_association: MEMBER

PR comments:
user: login: azure-sdk
created_at: 2025-03-25T06:53:55Z
body: <!--chronus-github-change-commenter-->
All changed packages have been documented.
 - :white_check_mark: `@azure-tools/azure-http-specs`

<details>
<summary>Show changes</summary>

### `@azure-tools/azure-http-specs` - _fix_ [✏️](https://github.com/weidongxu-microsoft/typespec-azure/edit/azure-http-specs_fix-mockapi/.chronus/changes/azure-http-specs_fix-mockapi-2025-2-25-15-59-52.md?pr=/Azure/typespec-azure/pull/2440)
> Fix mockapi for flattenProperties and ARM
</details>

author_association: COLLABORATOR
user: login: azure-sdk
created_at: 2025-03-25T07:07:44Z
body: <!-- _TSP_TRYIT_COMMENT_ -->
You can try these changes here

| [🛝 Playground]( https://cadlplayground.z22.web.core.windows.net/typespec-azure/prs/2440/)|[🌐 Website](https://tspwebsitepr.z22.web.core.windows.net/typespec-azure/prs/2440/) |
| ---|--- |
author_association: COLLABORATOR
user: login: antkmsft
created_at: 2025-04-03T00:33:57Z
body: @weidongxu-microsoft, now the test started failing for us, see #2491 
author_association: MEMBER
user: login: weidongxu-microsoft
created_at: 2025-04-03T02:07:34Z
body: > @weidongxu-microsoft, now the test started failing for us, see #2491

I've answered in your issue.

My guess is that your spector/spec-api lib is outdated (all of these libs need to upgrade together -- my change to `json()` was because of the new spector/spec-api lib with their internal changes).
author_association: MEMBER