Repository: shopware/frontends
Pull request #978 (2024-06-06 11:51:36)

refactor!: change useCustomerPassword error handling
### Description

<!-- Describe the changes you did and which issue you're closing -->

<!-- example: closes #007 -->
Part of #941

Breaking the API of `useCustomerPassword` for consistency in error handling.

### Type of change

<!-- Comment out the type of change your PR is making -->

<!-- Bug fix (non-breaking change that fixes an issue) -->
<!-- New feature (non-breaking change which adds functionality) -->
<!-- Breaking change (fix or feature that would cause existing functionality to not work as expected) -->

Breaking change


vercel[bot]: [vc]: #mSgF96NBfy6y/gI39C+BMKARXSqbX6W3O0bb/0B1oW0=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJzaG9wd2FyZS1mcm9udGVuZHMtZG9jcyIsInJvb3REaXJlY3RvcnkiOiJhcHBzL2RvY3MvIiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3Nob3B3YXJlLWZyb250ZW5kcy9zaG9wd2FyZS1mcm9udGVuZHMtZG9jcy82V0s3THduQjFLOGJOSnlha1R0Nm5GTVRRY1hyIiwicHJldmlld1VybCI6InNob3B3YXJlLWZyb250ZW5kcy1kb2NzLWdpdC1yZWZhY3Rvci05OWM0OGMtc2hvcHdhcmUtZnJvbnRlbmRzLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiUEVORElORyJ9LHsibmFtZSI6ImZyb250ZW5kcy1kZW1vIiwicm9vdERpcmVjdG9yeSI6InRlbXBsYXRlcy92dWUtZGVtby1zdG9yZSIsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9zaG9wd2FyZS1mcm9udGVuZHMvZnJvbnRlbmRzLWRlbW8vOXNyN3ZuZkZ1blQzbkV5VVR0WkFIZnczSjI5aSIsInByZXZpZXdVcmwiOiJmcm9udGVuZHMtZGVtby1naXQtcmVmYWN0b3ItZXJyb3ItaGFuLTRhYjNlOS1zaG9wd2FyZS1mcm9udGVuZHMudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJQRU5ESU5HIn1dfQ==
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Updated (UTC) |
| :--- | :----- | :------ | :------ |
| **frontends-demo** | 🔄 Building ([Inspect](https://vercel.com/shopware-frontends/frontends-demo/9sr7vnfFunT3nEyUTtZAHfw3J29i)) | [Visit Preview](https://frontends-demo-git-refactor-error-han-4ab3e9-shopware-frontends.vercel.app) | Jun 6, 2024 11:51am |
| **shopware-frontends-docs** | 🔄 Building ([Inspect](https://vercel.com/shopware-frontends/shopware-frontends-docs/6WK7LwnB1K8bNJyakTt6nFMTQcXr)) | [Visit Preview](https://shopware-frontends-docs-git-refactor-99c48c-shopware-frontends.vercel.app) | Jun 6, 2024 11:51am |



codspeed-hq[bot]: ## [CodSpeed Performance Report](https://codspeed.io/shopware/frontends/branches/refactor/error-handling-for-useCastomerPassword)

### Merging #978 will **not alter performance**

<sub>Comparing <code>refactor/error-handling-for-useCastomerPassword</code> (a2f8f99) with <code>main</code> (8510f65)</sub>



### Summary

`✅ 9` untouched benchmarks






Patch:
diff --git a/.changeset/wet-lions-decide.md b/.changeset/wet-lions-decide.md
new file mode 100644
index 00000000..b595b2df
--- /dev/null
+++ b/.changeset/wet-lions-decide.md
@@ -0,0 +1,33 @@
+---
+"@shopware-pwa/composables-next": major
+"vue-demo-store": minor
+---
+
+`useCustomerPassword` and `loadCustomerAddresses` inside `useAddress` are now throwing api errors on invocation. The `errors` object has been removed from the composable to make consistent error handling across the composables. This change is breaking and requires you to update your implementation of the composables.
+
+Example of error handling for resseting password:
+
+```typescript
+const {
+  resetPassword,
+  // errors --> removed from the API
+} = useCustomerPassword();
+
+const errors = ref([]);
+
+const invokeRecover = async (): Promise<void> => {
+  try {
+    errors.value = [];
+    const emailSent = await resetPassword(formData.value);
+
+    if (emailSent.success) {
+      // here we know that email was sent
+    }
+  } catch (error) {
+    console.error("[AccountRecoverPassword]", error);
+    if (error instanceof ApiClientError) {
+      errors.value = error.details?.errors || [];
+    }
+  }
+};
+```
diff --git a/examples/responsive-images/package.json b/examples/responsive-images/package.json
index b35b2eaa..fabeed05 100644
--- a/examples/responsive-images/package.json
+++ b/examples/responsive-images/package.json
@@ -16,7 +16,6 @@
     "vue": "3.4.27"
   },
   "devDependencies": {
-    "@shopware-pwa/types": "canary",
     "@types/node": "20.12.7",
     "@vitejs/plugin-vue": "5.0.5",
     "vite": "5.2.12",
diff --git a/packages/api-client-next/src/errorInterceptor.ts b/packages/api-client-next/src/errorInterceptor.ts
index 51993529..77c5f6a3 100644
--- a/packages/api-client-next/src/errorInterceptor.ts
+++ b/packages/api-client-next/src/errorInterceptor.ts
@@ -9,7 +9,7 @@ export type ApiError = {
     pointer?: string;
   };
   meta?: {
-    parameters?: Record<string, string>;
+    parameters?: Record<string, string> | [];
   };
 };
 
diff --git a/packages/composables/src/index.ts b/packages/composables/src/index.ts
index 65463e5f..5b157b1d 100644
--- a/packages/composables/src/index.ts
+++ b/packages/composables/src/index.ts
@@ -97,5 +97,8 @@ export function resolveCmsComponent(content: CmsSection | CmsBlock | CmsSlot) {
 export function getDefaultApiParams(): {
   [composableName: string]: unknown;
 } {
+  console.error(
+    "[@shopware-pwa/composables] `getDefaultApiParams` is deprecated and will be removed in the next major release.",
+  );
   return {};
 }
diff --git a/packages/composables/src/useAddress.ts b/packages/composables/src/useAddress.ts
index 3eed9955..2f71c341 100644
--- a/packages/composables/src/useAddress.ts
+++ b/packages/composables/src/useAddress.ts
@@ -82,6 +82,7 @@ export function useAddress(): UseAddressReturn {
           _storeCustomerAddresses.value = [];
         }
       }
+      throw error;
     }
   }
 
diff --git a/packages/composables/src/useCartItem.ts b/packages/composables/src/useCartItem.ts
index b4b8b35a..ff8c777c 100644
--- a/packages/composables/src/useCartItem.ts
+++ b/packages/composables/src/useCartItem.ts
@@ -156,6 +156,9 @@ export function useCartItem(
    * @deprecated Method is not used anymore and the case should be solved on project level instead due to performance reasons.
    */
   async function getProductItemSeoUrlData() {
+    console.warn(
+      "[useCartItem][getProductItemSeoUrlData] is deprecated and will be removed in the next major release.",
+    );
     if (!cartItem.value.referencedId) {
       return;
     }
diff --git a/packages/composables/src/useCustomerPassword.ts b/packages/composables/src/useCustomerPassword.ts
index 0918147e..2208d36b 100644
--- a/packages/composables/src/useCustomerPassword.ts
+++ b/packages/composables/src/useCustomerPassword.ts
@@ -1,27 +1,23 @@
-import { reactive } from "vue";
-import type { UnwrapRef } from "vue";
 import { useShopwareContext } from "#imports";
 import type { operations } from "#shopware";
-import { ApiClientError } from "@shopware/api-client";
-import type { ApiError } from "@shopware/api-client";
 
 export type UseCustomerPasswordReturn = {
-  errors: UnwrapRef<{
-    resetPassword: ApiError[];
-    updatePassword: ApiError[];
-  }>;
   /**
    * Change customer's current password
    */
   updatePassword(
     updatePasswordData: operations["changePassword post /account/change-password"]["body"],
-  ): Promise<boolean>;
+  ): Promise<
+    operations["changePassword post /account/change-password"]["response"]
+  >;
   /**
    * Reset customer's password
    */
   resetPassword(
     resetPasswordData: operations["sendRecoveryMail post /account/recovery-password"]["body"],
-  ): Promise<boolean>;
+  ): Promise<
+    operations["sendRecoveryMail post /account/recovery-password"]["response"]
+  >;
 };
 
 /**
@@ -32,51 +28,30 @@ export type UseCustomerPasswordReturn = {
 export function useCustomerPassword(): UseCustomerPasswordReturn {
   const { apiClient } = useShopwareContext();
 
-  const errors: UnwrapRef<{
-    resetPassword: ApiError[];
-    updatePassword: ApiError[];
-  }> = reactive({
-    resetPassword: [],
-    updatePassword: [],
-  });
-
   async function updatePassword(
     updatePasswordData: operations["changePassword post /account/change-password"]["body"],
   ) {
-    try {
-      errors.updatePassword = [];
-      await apiClient.invoke("changePassword post /account/change-password", {
+    const response = await apiClient.invoke(
+      "changePassword post /account/change-password",
+      {
         body: updatePasswordData,
-      });
-    } catch (e) {
-      if (e instanceof ApiClientError) {
-        errors.updatePassword = e.details;
-      }
-      return false;
-    }
-    return true;
+      },
+    );
+    return response.data;
   }
 
   async function resetPassword(
     resetPasswordData: operations["sendRecoveryMail post /account/recovery-password"]["body"],
   ) {
-    try {
-      await apiClient.invoke(
-        "sendRecoveryMail post /account/recovery-password",
-        { body: resetPasswordData },
-      );
-    } catch (e) {
-      if (e instanceof ApiClientError) {
-        errors.resetPassword = e.details;
-      }
-      return false;
-    }
-    return true;
+    const response = await apiClient.invoke(
+      "sendRecoveryMail post /account/recovery-password",
+      { body: resetPasswordData },
+    );
+    return response.data;
   }
 
   return {
     updatePassword,
     resetPassword,
-    errors,
   };
 }
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3b904eed..aa505548 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -570,9 +570,6 @@ importers:
         specifier: 3.4.27
         version: 3.4.27(typescript@5.4.5)
     devDependencies:
-      '@shopware-pwa/types':
-        specifier: canary
-        version: link:../../packages/types
       '@types/node':
         specifier: ^20
         version: 20.11.17
@@ -24941,7 +24938,7 @@ snapshots:
       browserslist: 4.23.0
       chrome-trace-event: 1.0.3
       enhanced-resolve: 5.16.0
-      es-module-lexer: 1.4.1
+      es-module-lexer: 1.5.3
       eslint-scope: 5.1.1
       events: 3.3.0
       glob-to-regexp: 0.4.1
diff --git a/templates/vue-demo-store/components/account/AccountChangePassword.vue b/templates/vue-demo-store/components/account/AccountChangePassword.vue
index 47262951..79d2365b 100644
--- a/templates/vue-demo-store/components/account/AccountChangePassword.vue
+++ b/templates/vue-demo-store/components/account/AccountChangePassword.vue
@@ -1,17 +1,19 @@
 <script setup lang="ts">
 import { useVuelidate } from "@vuelidate/core";
 import { customValidators } from "@/i18n/utils/i18n-validators";
+import { ApiClientError } from "@shopware/api-client";
 
 const { required, minLength, sameAs } = customValidators();
 const emits = defineEmits<{
   (e: "success"): void;
 }>();
 
-const { updatePassword, errors } = useCustomerPassword();
+const { updatePassword } = useCustomerPassword();
 const { user, refreshUser } = useUser();
 
+const errors = ref([]);
 const userErrorMessages = computed(() =>
-  errors.updatePassword?.map(({ detail }) => detail).toString(),
+  errors.value?.map(({ detail }) => detail).toString(),
 );
 
 const isSuccess = ref(false);
@@ -48,6 +50,7 @@ const $v = useVuelidate(rules, state);
 const invokeChange = async (): Promise<void> => {
   loadingData.value = true;
   try {
+    errors.value = [];
     const isFormCorrect = await $v.value.$validate();
 
     if (isFormCorrect) {
@@ -71,8 +74,11 @@ const invokeChange = async (): Promise<void> => {
     } else {
       return;
     }
-  } catch (err) {
-    console.error("error change password", err);
+  } catch (error) {
+    console.error("[AccountChangePassword]", error);
+    if (error instanceof ApiClientError) {
+      errors.value = error.details?.errors || [];
+    }
   } finally {
     loadingData.value = false;
   }
@@ -103,13 +109,6 @@ const invokeChange = async (): Promise<void> => {
       data-testid="account-change-password-form"
       @submit.prevent="invokeChange"
     >
-      <div
-        v-if="userErrorMessages.length"
-        class="text-red-600 focus:ring-primary border-secondary-300 rounded"
-      >
-        {{ userErrorMessages }}
-      </div>
-
       <div class="mt-4 space-y-4 lg:mt-5 md:space-y-5">
         <div>
           <label
@@ -197,6 +196,15 @@ const invokeChange = async (): Promise<void> => {
           {{ $t("changePassword.form.changePassword") }}
         </button>
       </div>
+
+      <div class="min-h-30px">
+        <div
+          v-if="userErrorMessages?.length"
+          class="text-red-600 text-sm focus:ring-primary border-secondary-300 rounded"
+        >
+          {{ userErrorMessages }}
+        </div>
+      </div>
     </form>
   </div>
 </template>
diff --git a/templates/vue-demo-store/components/account/AccountMenu.vue b/templates/vue-demo-store/components/account/AccountMenu.vue
index 2fc07d6f..7991d60a 100644
--- a/templates/vue-demo-store/components/account/AccountMenu.vue
+++ b/templates/vue-demo-store/components/account/AccountMenu.vue
@@ -7,8 +7,11 @@ const { formatLink } = useInternationalization(localePath);
 const isAccountMenuOpen = ref(false);
 
 async function invokeLogout() {
-  await logout();
-  isAccountMenuOpen.value = false;
+  try {
+    await logout();
+  } finally {
+    isAccountMenuOpen.value = false;
+  }
 }
 </script>
 <template>
@@ -28,7 +31,18 @@ async function invokeLogout() {
           <AccountLoginForm
             @close="loginModalController.close"
             @success="loginModalController.close"
-          />
+          >
+            <div class="flex items-center justify-end">
+              <div class="text-sm">
+                <NuxtLink
+                  :to="formatLink(`/account/recover`)"
+                  class="font-medium text-indigo-600 hover:text-indigo-500"
+                >
+                  {{ $t("recoveryPassword.forgotPassword") }}
+                </NuxtLink>
+              </div>
+            </div>
+          </AccountLoginForm>
         </SharedModal>
         <div v-if="isLoggedIn">
           <div
diff --git a/templates/vue-demo-store/components/account/AccountRecoverPassword.vue b/templates/vue-demo-store/components/account/AccountRecoverPassword.vue
index 6f8c325d..9b2b7216 100644
--- a/templates/vue-demo-store/components/account/AccountRecoverPassword.vue
+++ b/templates/vue-demo-store/components/account/AccountRecoverPassword.vue
@@ -1,32 +1,39 @@
 <script setup lang="ts">
+import { ApiClientError } from "@shopware/api-client";
+
 const emits = defineEmits<{
   (e: "success"): void;
 }>();
 
-const { resetPassword, errors } = useCustomerPassword();
-const { apiInstance } = useShopwareContext();
+const { resetPassword } = useCustomerPassword();
 const isSuccess = ref(false);
 
+const config = useRuntimeConfig();
+
 const formData = ref({
   email: "",
-  storefrontUrl:
-    (apiInstance as { config: { endpoint: string } }).config.endpoint || "",
+  storefrontUrl: config.public.shopware.devStorefrontUrl!,
 });
 
+const errors = ref([]);
 const recoverPasswordErrors = computed(() =>
-  errors.resetPassword?.map(({ detail }) => detail).toString(),
+  errors.value?.map(({ detail }) => detail).toString(),
 );
 
 const invokeRecover = async (): Promise<void> => {
   try {
+    errors.value = [];
     const emailSent = await resetPassword(formData.value);
 
-    if (emailSent) {
+    if (emailSent.success) {
       isSuccess.value = true;
       emits("success");
     }
   } catch (error) {
-    console.error("error resend email", error);
+    console.error("[AccountRecoverPassword]", error);
+    if (error instanceof ApiClientError) {
+      errors.value = error.details?.errors || [];
+    }
   }
 };
 </script>
@@ -41,7 +48,7 @@ const invokeRecover = async (): Promise<void> => {
           {{ $t("recoveryPassword.header") }}
         </h2>
       </div>
-      <form v-if="!isSuccess" class="space-y-6" @submit.prevent="invokeRecover">
+      <form v-if="!isSuccess" class="space-y-3" @submit.prevent="invokeRecover">
         <div class="rounded-md shadow-sm -space-y-px">
           <div>
             <label for="email-address" class="sr-only">{{
@@ -61,15 +68,12 @@ const invokeRecover = async (): Promise<void> => {
         </div>
 
         <slot name="error">
-          <div
-            v-if="recoverPasswordErrors.length"
-            class="flex items-center justify-between"
-          >
-            <div class="flex items-center">
-              <div
-                class="send-email-errors text-red-600 border-secondary-300 rounded"
-              >
-                {{ recoverPasswordErrors }}
+          <div class="min-h-20px">
+            <div v-if="recoverPasswordErrors?.length" class="flex">
+              <div class="text-sm">
+                <div class="text-red-600 border-secondary-300 rounded">
+                  {{ recoverPasswordErrors }}
+                </div>
               </div>
             </div>
           </div>
diff --git a/templates/vue-demo-store/pages/checkout/index.vue b/templates/vue-demo-store/pages/checkout/index.vue
index 00eb2950..34d8e696 100644
--- a/templates/vue-demo-store/pages/checkout/index.vue
+++ b/templates/vue-demo-store/pages/checkout/index.vue
@@ -252,8 +252,11 @@ const invokeSubmit = async () => {
   }
 };
 async function invokeLogout() {
-  await logout();
-  await push("/");
+  try {
+    await logout();
+  } finally {
+    await push("/");
+  }
 }
 
 const loginModalController = useModal();
diff --git a/templates/vue-demo-store/pages/login.vue b/templates/vue-demo-store/pages/login.vue
index 34d962cf..390a8e31 100644
--- a/templates/vue-demo-store/pages/login.vue
+++ b/templates/vue-demo-store/pages/login.vue
@@ -1,6 +1,6 @@
 <script setup lang="ts">
 const { push } = useRouter();
-const { logout, isLoggedIn } = useUser();
+const { isLoggedIn } = useUser();
 
 const redirectAfterLogin = (path = "/account") => push(path);
 const localePath = useLocalePath();
@@ -10,8 +10,6 @@ onBeforeMount(async () => {
   if (process.client && isLoggedIn.value) {
     // redirect to account page if user is logged in
     navigateTo({ path: "/account" });
-  } else {
-    await logout(); // if you do a hard reload on the login page, you will be logged out
   }
 });
 


Test patch:
diff --git a/packages/composables/src/useCustomerPassword.test.ts b/packages/composables/src/useCustomerPassword.test.ts
index 24bd0c69..363d38f8 100644
--- a/packages/composables/src/useCustomerPassword.test.ts
+++ b/packages/composables/src/useCustomerPassword.test.ts
@@ -6,7 +6,11 @@ import type { FetchResponse } from "ofetch";
 
 describe("useCustomerPassword", () => {
   it("reset password", async () => {
-    const { vm, injections } = await useSetup(useCustomerPassword);
+    const { vm, injections } = await useSetup(useCustomerPassword, {
+      apiClient: {
+        invoke: vi.fn().mockResolvedValue({ data: {} }),
+      },
+    });
 
     await vm.resetPassword({
       email: "test@test.test",
@@ -29,22 +33,78 @@ describe("useCustomerPassword", () => {
       apiClient: {
         invoke: vi.fn().mockImplementation(() => {
           throw new ApiClientError({
-            details: [{ title: "test" }],
+            _data: {
+              errors: [
+                {
+                  code: "VIOLATION::INVALID_FORMAT_ERROR",
+                  status: "400",
+                  title: "Constraint violation error",
+                  detail: "This value is not a valid email address.",
+                  source: {
+                    pointer: "/email",
+                  },
+                  meta: {
+                    parameters: {
+                      "{{ value }}": "\u0022werwerewr@ddddddd\u0022",
+                    },
+                  },
+                },
+                {
+                  code: "VIOLATION::IS_BLANK_ERROR",
+                  status: "400",
+                  title: "Constraint violation error",
+                  detail: "This value should not be blank.",
+                  source: {
+                    pointer: "/storefrontUrl",
+                  },
+                  meta: {
+                    parameters: {
+                      "{{ value }}": "\u0022\u0022",
+                    },
+                  },
+                },
+                {
+                  code: "VIOLATION::NO_SUCH_CHOICE_ERROR",
+                  status: "400",
+                  title: "Constraint violation error",
+                  detail: "The value you selected is not a valid choice.",
+                  source: {
+                    pointer: "/storefrontUrl",
+                  },
+                  meta: {
+                    parameters: {
+                      "{{ value }}": "\u0022\u0022",
+                      "{{ choices }}":
+                        "\u0022https://demo-frontends.shopware.store/de-De\u0022, \u0022https://demo-frontends.shopware.store/de\u0022, \u0022https://demo-frontends.shopware.store\u0022, \u0022https://demo-frontends.shopware.store/testde\u0022, \u0022https://demo-frontends.shopware.store/pl-PL\u0022, \u0022https://frontends-demo.vercel.app\u0022",
+                    },
+                  },
+                },
+              ],
+            },
           } as unknown as FetchResponse<{ errors: Array<{ title: string }> }>);
         }),
       },
     });
 
-    await vm.resetPassword({
-      email: "test@test.test",
-      storefrontUrl: "http://test.test",
-    });
-    expect(vm.errors.resetPassword.length).not.toBe(0);
-    expect(vm.errors.resetPassword).not.toBeNull();
+    await expect(
+      vm.resetPassword({
+        email: "test@test.test",
+        storefrontUrl: "http://test.test",
+      }),
+    ).rejects.toThrowErrorMatchingInlineSnapshot(`
+      [ApiClientError: Failed request
+       - [Constraint violation error][/email] This value is not a valid email address.
+       - [Constraint violation error][/storefrontUrl] This value should not be blank.
+       - [Constraint violation error][/storefrontUrl] The value you selected is not a valid choice.]
+    `);
   });
 
   it("update password", async () => {
-    const { vm, injections } = await useSetup(useCustomerPassword);
+    const { vm, injections } = await useSetup(useCustomerPassword, {
+      apiClient: {
+        invoke: vi.fn().mockResolvedValue({ data: {} }),
+      },
+    });
 
     await vm.updatePassword({
       newPassword: "test",
@@ -69,20 +129,36 @@ describe("useCustomerPassword", () => {
       apiClient: {
         invoke: vi.fn().mockImplementation(() => {
           throw new ApiClientError({
-            details: [{ title: "test" }],
-          } as unknown as FetchResponse<{
-            errors: Array<{ title: string }>;
-          }>);
+            _data: {
+              errors: [
+                {
+                  code: "VIOLATION::CUSTOMER_PASSWORD_NOT_CORRECT",
+                  status: "400",
+                  title: "Constraint violation error",
+                  detail: "Your password is wrong",
+                  source: {
+                    pointer: "/password",
+                  },
+                  meta: {
+                    parameters: [],
+                  },
+                },
+              ],
+            },
+          } as unknown as FetchResponse<{ errors: Array<{ title: string }> }>);
         }),
       },
     });
 
-    await vm.updatePassword({
-      newPassword: "test",
-      newPasswordConfirm: "test",
-      password: "test",
-    });
-    expect(vm.errors.updatePassword.length).not.toBe(0);
-    expect(vm.errors.updatePassword).not.toBeNull();
+    await expect(
+      vm.updatePassword({
+        newPassword: "test",
+        newPasswordConfirm: "test",
+        password: "test",
+      }),
+    ).rejects.toThrowErrorMatchingInlineSnapshot(`
+      [ApiClientError: Failed request
+       - [Constraint violation error][/password] Your password is wrong]
+    `);
   });
 });