-
- {{ statusMessage }}
-
-
- Verify MFA
-
+
+
+
+
+
+
+ {{ statusMessage }}
+
+
+
+
+ Retry Verification
+
+
@@ -27,7 +41,7 @@
diff --git a/src/dispatch/static/dispatch/src/tests/Mfa.spec.js b/src/dispatch/static/dispatch/src/tests/Mfa.spec.js
new file mode 100644
index 000000000000..970305938dad
--- /dev/null
+++ b/src/dispatch/static/dispatch/src/tests/Mfa.spec.js
@@ -0,0 +1,176 @@
+import { mount, flushPromises } from "@vue/test-utils"
+import { expect, test, vi, beforeEach, afterEach } from "vitest"
+import { createVuetify } from "vuetify"
+import * as components from "vuetify/components"
+import * as directives from "vuetify/directives"
+import MfaVerification from "@/auth/mfa.vue"
+import authApi from "@/auth/api"
+
+vi.mock("vue-router", () => ({
+ useRoute: () => ({
+ query: {
+ challenge_id: "test-challenge",
+ project_id: "123",
+ action: "test-action",
+ },
+ }),
+}))
+
+vi.mock("@/auth/api", () => ({
+ default: {
+ verifyMfa: vi.fn(),
+ },
+}))
+
+const vuetify = createVuetify({
+ components,
+ directives,
+})
+
+global.ResizeObserver = require("resize-observer-polyfill")
+
+const windowCloseMock = vi.fn()
+const originalClose = window.close
+
+beforeEach(() => {
+ vi.useFakeTimers()
+ Object.defineProperty(window, "close", {
+ value: windowCloseMock,
+ writable: true,
+ })
+ vi.clearAllMocks()
+})
+
+afterEach(() => {
+ vi.useRealTimers()
+ Object.defineProperty(window, "close", {
+ value: originalClose,
+ writable: true,
+ })
+})
+
+test("mounts correctly and starts verification automatically", async () => {
+ const wrapper = mount(MfaVerification, {
+ global: {
+ plugins: [vuetify],
+ },
+ })
+
+ await flushPromises()
+
+ expect(wrapper.exists()).toBe(true)
+ expect(authApi.verifyMfa).toHaveBeenCalledWith({
+ challenge_id: "test-challenge",
+ project_id: 123,
+ action: "test-action",
+ })
+})
+
+test("shows loading state while verifying", async () => {
+ vi.mocked(authApi.verifyMfa).mockImplementationOnce(
+ () => new Promise(() => {}) // Never resolving promise
+ )
+
+ const wrapper = mount(MfaVerification, {
+ global: {
+ plugins: [vuetify],
+ },
+ })
+
+ await flushPromises()
+
+ const loadingSpinner = wrapper.findComponent({ name: "v-progress-circular" })
+ expect(loadingSpinner.exists()).toBe(true)
+ expect(loadingSpinner.isVisible()).toBe(true)
+})
+
+test("shows success message and closes window on approval", async () => {
+ vi.mocked(authApi.verifyMfa).mockResolvedValueOnce({
+ data: { status: "approved" },
+ })
+
+ const wrapper = mount(MfaVerification, {
+ global: {
+ plugins: [vuetify],
+ },
+ })
+
+ await flushPromises()
+
+ const alert = wrapper.findComponent({ name: "v-alert" })
+ expect(alert.exists()).toBe(true)
+ expect(alert.props("type")).toBe("success")
+ expect(alert.text()).toContain("MFA verification successful")
+
+ vi.advanceTimersByTime(5000)
+ expect(windowCloseMock).toHaveBeenCalled()
+})
+
+test("shows error message and retry button on denial", async () => {
+ vi.mocked(authApi.verifyMfa).mockResolvedValueOnce({
+ data: { status: "denied" },
+ })
+
+ const wrapper = mount(MfaVerification, {
+ global: {
+ plugins: [vuetify],
+ },
+ })
+
+ await flushPromises()
+
+ const alert = wrapper.findComponent({ name: "v-alert" })
+ expect(alert.exists()).toBe(true)
+ expect(alert.props("type")).toBe("error")
+ expect(alert.text()).toContain("MFA verification denied")
+
+ const retryButton = wrapper.findComponent({ name: "v-btn" })
+ expect(retryButton.exists()).toBe(true)
+ expect(retryButton.text()).toContain("Retry Verification")
+})
+
+test("retry button triggers new verification attempt", async () => {
+ const verifyMfaMock = vi
+ .mocked(authApi.verifyMfa)
+ .mockResolvedValueOnce({
+ data: { status: "denied" },
+ })
+ .mockResolvedValueOnce({
+ data: { status: "approved" },
+ })
+
+ const wrapper = mount(MfaVerification, {
+ global: {
+ plugins: [vuetify],
+ },
+ })
+
+ await flushPromises()
+
+ const retryButton = wrapper.findComponent({ name: "v-btn" })
+ await retryButton.trigger("click")
+
+ await flushPromises()
+
+ expect(verifyMfaMock).toHaveBeenCalledTimes(2)
+
+ const alert = wrapper.findComponent({ name: "v-alert" })
+ expect(alert.props("type")).toBe("success")
+})
+
+test("handles API errors gracefully", async () => {
+ vi.mocked(authApi.verifyMfa).mockRejectedValueOnce(new Error("API Error"))
+
+ const wrapper = mount(MfaVerification, {
+ global: {
+ plugins: [vuetify],
+ },
+ })
+
+ await flushPromises()
+
+ const alert = wrapper.findComponent({ name: "v-alert" })
+ expect(alert.exists()).toBe(true)
+ expect(alert.props("type")).toBe("error")
+ expect(alert.text()).toContain("MFA verification denied")
+})
From 0ee4db2707ded7ff51ae913e90046f187a6eaf80 Mon Sep 17 00:00:00 2001
From: David Whittaker <84562015+whitdog47@users.noreply.github.com>
Date: Fri, 8 Nov 2024 20:19:19 -0800
Subject: [PATCH 03/15] Use local event to prevent slow down (#5444)
---
.../dispatch/src/incident/EditEventDialog.vue | 27 +++++++++++++------
1 file changed, 19 insertions(+), 8 deletions(-)
diff --git a/src/dispatch/static/dispatch/src/incident/EditEventDialog.vue b/src/dispatch/static/dispatch/src/incident/EditEventDialog.vue
index e2887176777b..f1bb197f07c6 100644
--- a/src/dispatch/static/dispatch/src/incident/EditEventDialog.vue
+++ b/src/dispatch/static/dispatch/src/incident/EditEventDialog.vue
@@ -11,7 +11,7 @@