Repository: canonical/juju-dashboard
Pull request #1918: feat: remove `AuthMethod`
Author: login: andogq
Labels: name: Review: QA +1
name: Review: Code +1 (with changes)
Created: 2025-04-03T06:01:03Z | Merged: 2025-04-04T01:13:42Z

## Done

- remove references to `AuthMethod`

- change references of `WindowConfig` to `Config`

- alter handful of tests to behave without `config.authMethod`

## QA

- Pull code
- Run `dotrun clean && dotrun serve`
- Open http://0.0.0.0:8036/
- Make sure everything behaves

## Details

WD-20710

> [!note]
> Builds on #1916, so there's an extra commit included



Diff:
diff --git a/src/auth/Auth.test.ts b/src/auth/Auth.test.ts
index 6ddd00470..b64fc92ca 100644
--- a/src/auth/Auth.test.ts
+++ b/src/auth/Auth.test.ts
@@ -1,6 +1,4 @@
-import { AuthMethod } from "store/general/types";
-
-import { Auth } from "./Auth";
+import { Auth, AuthMethod } from ".";
 
 describe("Auth base class", () => {
   beforeEach(() => {
diff --git a/src/auth/Auth.ts b/src/auth/Auth.ts
index 8f20fe29d..6aed5630c 100644
--- a/src/auth/Auth.ts
+++ b/src/auth/Auth.ts
@@ -4,10 +4,12 @@
 import type { ConnectOptions, Credentials } from "@canonical/jujulib";
 
 import type { ConnectionWithFacades } from "juju/types";
-import type { AuthMethod, Credential } from "store/general/types";
+import type { Credential } from "store/general/types";
 import type { AppDispatch } from "store/store";
 import { logger } from "utils/logger";
 
+import type { AuthMethod } from ".";
+
 export type ControllerData = {
   wsControllerURL: string;
   credentials?: Credential;
diff --git a/src/auth/CandidAuth.ts b/src/auth/CandidAuth.ts
index 19787cb98..0b4dc23e0 100644
--- a/src/auth/CandidAuth.ts
+++ b/src/auth/CandidAuth.ts
@@ -1,12 +1,13 @@
 import * as jujuApi from "juju/api";
 import type { ConnectionWithFacades } from "juju/types";
 import { thunks as appThunks } from "store/app";
-import { AuthMethod } from "store/general/types";
 import type { AppDispatch } from "store/store";
 
 import { Auth } from "./Auth";
 import { pollingMixin } from "./mixins";
 
+import { AuthMethod } from ".";
+
 export class CandidAuth extends pollingMixin(Auth) {
   constructor(dispatch: AppDispatch) {
     super(dispatch, AuthMethod.CANDID);
diff --git a/src/auth/LocalAuth.ts b/src/auth/LocalAuth.ts
index a82199823..1942b7edb 100644
--- a/src/auth/LocalAuth.ts
+++ b/src/auth/LocalAuth.ts
@@ -1,11 +1,12 @@
 import type { Credentials } from "@canonical/jujulib";
 
 import type { Credential } from "store/general/types";
-import { AuthMethod } from "store/general/types";
 import type { AppDispatch } from "store/store";
 
 import { Auth } from "./Auth";
 
+import { AuthMethod } from ".";
+
 export class LocalAuth extends Auth {
   constructor(dispatch: AppDispatch) {
     super(dispatch, AuthMethod.LOCAL);
diff --git a/src/auth/OIDCAuth.ts b/src/auth/OIDCAuth.ts
index 22e26fdbf..182c659ef 100644
--- a/src/auth/OIDCAuth.ts
+++ b/src/auth/OIDCAuth.ts
@@ -4,13 +4,14 @@ import { unwrapResult } from "@reduxjs/toolkit";
 import * as jimmListeners from "juju/jimm/listeners";
 import * as jimmThunks from "juju/jimm/thunks";
 import { actions as generalActions } from "store/general";
-import { AuthMethod } from "store/general/types";
 import { listenerMiddleware } from "store/listenerMiddleware";
 import type { AppDispatch } from "store/store";
 
 import { Auth, type ControllerData } from "./Auth";
 import { pollingMixin } from "./mixins";
 
+import { AuthMethod } from ".";
+
 export enum Label {
   WHOAMI = "Unable to check authentication status. You can attempt to log in anyway.",
 }
diff --git a/src/auth/index.ts b/src/auth/index.ts
index ae0e756fe..a5d8b623e 100644
--- a/src/auth/index.ts
+++ b/src/auth/index.ts
@@ -3,3 +3,4 @@ export { CandidAuth } from "./CandidAuth";
 export { LocalAuth } from "./LocalAuth";
 export { OIDCAuth } from "./OIDCAuth";
 export { initialiseAuthFromConfig } from "./utils";
+export { AuthMethod } from "./types";
diff --git a/src/auth/mixins/polling.test.ts b/src/auth/mixins/polling.test.ts
index f1e1c47a6..ae0f96a97 100644
--- a/src/auth/mixins/polling.test.ts
+++ b/src/auth/mixins/polling.test.ts
@@ -1,9 +1,8 @@
 import type { Mock, MockInstance } from "vitest";
 
 import { thunks as appThunks } from "store/app";
-import { AuthMethod } from "store/general/types";
 
-import { Auth } from "../Auth";
+import { Auth, AuthMethod } from "..";
 
 import { pollingMixin } from "./polling";
 
diff --git a/src/auth/types.ts b/src/auth/types.ts
new file mode 100644
index 000000000..c4b5de1fc
--- /dev/null
+++ b/src/auth/types.ts
@@ -0,0 +1,5 @@
+export enum AuthMethod {
+  CANDID = "candid",
+  LOCAL = "local",
+  OIDC = "oidc",
+}
diff --git a/src/auth/utils.ts b/src/auth/utils.ts
index 5144e8361..1f062a219 100644
--- a/src/auth/utils.ts
+++ b/src/auth/utils.ts
@@ -1,5 +1,5 @@
+import type { Config } from "store/general/types";
 import type { AppDispatch } from "store/store";
-import type { WindowConfig } from "types";
 
 import { CandidAuth, LocalAuth, OIDCAuth } from ".";
 
@@ -8,7 +8,7 @@ import { CandidAuth, LocalAuth, OIDCAuth } from ".";
  * implementation.
  */
 export function initialiseAuthFromConfig(
-  config: WindowConfig,
+  config: Config,
   dispatch: AppDispatch,
 ) {
   if (!config.isJuju) {
diff --git a/src/components/LogIn/LogIn.test.tsx b/src/components/LogIn/LogIn.test.tsx
index d5593d7da..c7983e30c 100644
--- a/src/components/LogIn/LogIn.test.tsx
+++ b/src/components/LogIn/LogIn.test.tsx
@@ -2,7 +2,6 @@ import { screen, within } from "@testing-library/react";
 import userEvent from "@testing-library/user-event";
 import { vi } from "vitest";
 
-import { AuthMethod } from "store/general/types";
 import { configFactory, generalStateFactory } from "testing/factories/general";
 import { rootStateFactory } from "testing/factories/root";
 import { renderComponent } from "testing/utils";
@@ -48,7 +47,8 @@ describe("LogIn", () => {
       general: generalStateFactory.withConfig().build({
         visitURLs: ["I am a url"],
         config: configFactory.build({
-          authMethod: AuthMethod.CANDID,
+          isJuju: true,
+          identityProviderURL: "/candid",
         }),
       }),
     });
@@ -64,9 +64,7 @@ describe("LogIn", () => {
   it("renders an OIDC login UI if the user is not logged in", () => {
     const state = rootStateFactory.build({
       general: generalStateFactory.withConfig().build({
-        config: configFactory.build({
-          authMethod: AuthMethod.OIDC,
-        }),
+        config: configFactory.build({ isJuju: false }),
       }),
     });
     renderComponent(<LogIn />, { state });
@@ -81,9 +79,7 @@ describe("LogIn", () => {
   it("renders a UserPass login UI if the user is not logged in", () => {
     const state = rootStateFactory.build({
       general: generalStateFactory.withConfig().build({
-        config: configFactory.build({
-          authMethod: AuthMethod.LOCAL,
-        }),
+        config: configFactory.build({ isJuju: true }),
       }),
     });
     renderComponent(<LogIn />, { state });
@@ -103,9 +99,6 @@ describe("LogIn", () => {
             "wss://controller.example.com": "Controller rejected request",
           },
         },
-        config: configFactory.build({
-          authMethod: AuthMethod.LOCAL,
-        }),
       }),
     });
     renderComponent(<LogIn />, { state });
@@ -120,9 +113,7 @@ describe("LogIn", () => {
         login: {
           errors: { "wss://controller.example.com": ErrorResponse.INVALID_TAG },
         },
-        config: configFactory.build({
-          authMethod: AuthMethod.LOCAL,
-        }),
+        config: configFactory.build(),
       }),
     });
     renderComponent(<LogIn />, { state });
@@ -137,9 +128,6 @@ describe("LogIn", () => {
             "wss://controller.example.com": ErrorResponse.INVALID_FIELD,
           },
         },
-        config: configFactory.build({
-          authMethod: AuthMethod.LOCAL,
-        }),
       }),
     });
     renderComponent(<LogIn />, { state });
@@ -149,9 +137,7 @@ describe("LogIn", () => {
   it("displays authentication request notifications", async () => {
     const state = rootStateFactory.build({
       general: generalStateFactory.withConfig().build({
-        config: configFactory.build({
-          authMethod: AuthMethod.CANDID,
-        }),
+        config: configFactory.build(),
         visitURLs: ["http://example.com/log-in"],
       }),
     });
@@ -170,7 +156,6 @@ describe("LogIn", () => {
       general: generalStateFactory.withConfig().build({
         config: configFactory.build({
           isJuju: true,
-          authMethod: AuthMethod.LOCAL,
         }),
         visitURLs: ["http://example.com/log-in"],
       }),
diff --git a/src/components/LogIn/LogIn.tsx b/src/components/LogIn/LogIn.tsx
index 58a0a0631..f9b601f9d 100644
--- a/src/components/LogIn/LogIn.tsx
+++ b/src/components/LogIn/LogIn.tsx
@@ -5,12 +5,12 @@ import reactHotToast from "react-hot-toast";
 import { Outlet } from "react-router";
 
 import FadeUpIn from "animations/FadeUpIn";
+import { Auth, AuthMethod } from "auth";
 import AuthenticationButton from "components/AuthenticationButton";
 import Logo from "components/Logo";
 import ToastCard from "components/ToastCard";
 import type { ToastInstance } from "components/ToastCard";
 import {
-  getConfig,
   getLoginError,
   getVisitURLs,
   getWSControllerURL,
@@ -18,7 +18,6 @@ import {
   getIsJuju,
   getLoginLoading,
 } from "store/general/selectors";
-import { AuthMethod } from "store/general/types";
 import { useAppSelector } from "store/store";
 
 import IdentityProviderForm from "./IdentityProviderForm";
@@ -28,7 +27,6 @@ import { ErrorResponse, Label, TestId } from "./types";
 
 export default function LogIn() {
   const viewedAuthRequests = useRef<string[]>([]);
-  const config = useAppSelector(getConfig);
   const isJuju = useAppSelector(getIsJuju);
   const wsControllerURL = useAppSelector(getWSControllerURL);
   const userIsLoggedIn = useAppSelector((state) =>
@@ -81,7 +79,7 @@ export default function LogIn() {
       </button>
     );
   } else {
-    switch (config?.authMethod) {
+    switch (Auth.instance.name) {
       case AuthMethod.CANDID:
         form = <IdentityProviderForm userIsLoggedIn={userIsLoggedIn} />;
         break;
diff --git a/src/components/Routes/Routes.test.tsx b/src/components/Routes/Routes.test.tsx
index 0f6a953c9..5f04181f5 100644
--- a/src/components/Routes/Routes.test.tsx
+++ b/src/components/Routes/Routes.test.tsx
@@ -1,6 +1,7 @@
 import { render, screen } from "@testing-library/react";
 import { Provider } from "react-redux";
 
+import { LocalAuth } from "auth";
 import { AdvancedSearchTestId } from "pages/AdvancedSearch";
 import { ControllersIndexTestId } from "pages/ControllersIndex/index";
 import { EntityDetailsTestId } from "pages/EntityDetails";
@@ -37,6 +38,7 @@ describe("Routes", () => {
         },
       }),
     });
+    new LocalAuth(vi.fn());
   });
 
   it("handles models", async () => {
diff --git a/src/index.tsx b/src/index.tsx
index f0665fbde..96b9c6719 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -10,8 +10,6 @@ import { Auth, initialiseAuthFromConfig } from "auth";
 import App from "components/App";
 import reduxStore from "store";
 import { actions as generalActions } from "store/general";
-import { AuthMethod } from "store/general/types";
-import type { WindowConfig } from "types";
 import { logger } from "utils/logger";
 
 import packageJSON from "../package.json";
@@ -32,16 +30,6 @@ if (import.meta.env.PROD && window.jujuDashboardConfig?.analyticsEnabled) {
   Sentry.setTag("dashboardVersion", appVersion);
 }
 
-const getAuthMethod = (config: WindowConfig) => {
-  if (!config.isJuju) {
-    return AuthMethod.OIDC;
-  }
-  if (config.identityProviderURL) {
-    return AuthMethod.CANDID;
-  }
-  return AuthMethod.LOCAL;
-};
-
 const addressRegex = new RegExp(/^ws[s]?:\/\/(\S+)\//);
 export const getControllerAPIEndpointErrors = (
   controllerAPIEndpoint?: string,
@@ -93,13 +81,7 @@ export const renderApp = () => {
 renderApp();
 
 function bootstrap() {
-  const windowConfig = window.jujuDashboardConfig;
-  const config = windowConfig
-    ? {
-        ...windowConfig,
-        authMethod: getAuthMethod(windowConfig),
-      }
-    : null;
+  const config = window.jujuDashboardConfig;
   const isProduction = import.meta.env.PROD;
   const logLevel: LogLevelDesc = isProduction
     ? logger.levels.SILENT
diff --git a/src/juju/api.test.ts b/src/juju/api.test.ts
index f75a886b5..c2426abda 100644
--- a/src/juju/api.test.ts
+++ b/src/juju/api.test.ts
@@ -6,7 +6,6 @@ import { vi } from "vitest";
 
 import { Auth, CandidAuth, LocalAuth, OIDCAuth } from "auth";
 import { actions as generalActions } from "store/general";
-import { AuthMethod } from "store/general/types";
 import { actions as jujuActions } from "store/juju";
 import type { RootState } from "store/store";
 import { rootStateFactory } from "testing/factories";
@@ -327,9 +326,6 @@ describe("Juju API", () => {
     });
 
     it("can log in with an external provider", async () => {
-      if (state.general.config) {
-        state.general.config.authMethod = AuthMethod.CANDID;
-      }
       const loginResponse = {
         conn: {
           facades: {
diff --git a/src/juju/jimm/api.test.ts b/src/juju/jimm/api.test.ts
index 710bfa645..a3b2b5f99 100644
--- a/src/juju/jimm/api.test.ts
+++ b/src/juju/jimm/api.test.ts
@@ -1,8 +1,8 @@
 import type { Connection } from "@canonical/jujulib";
 import { vi } from "vitest";
 
+import type { Config } from "store/general/types";
 import { relationshipTupleFactory } from "testing/factories/juju/juju";
-import type { WindowConfig } from "types";
 
 import {
   crossModelQuery,
@@ -20,7 +20,7 @@ describe("JIMM API", () => {
   it("generates correct endpoints", () => {
     window.jujuDashboardConfig = {
       controllerAPIEndpoint: "ws://example.com/api",
-    } as WindowConfig;
+    } as Config;
     const { login, logout, whoami } = endpoints();
     expect(login).toEqual("http://example.com/auth/login");
     expect(logout).toEqual("http://example.com/auth/logout");
@@ -30,7 +30,7 @@ describe("JIMM API", () => {
   it("generates correct endpoints for secure controller API", () => {
     window.jujuDashboardConfig = {
       controllerAPIEndpoint: "wss://example.com/api",
-    } as WindowConfig;
+    } as Config;
     const { login, logout, whoami } = endpoints();
     expect(login).toEqual("https://example.com/auth/login");
     expect(logout).toEqual("https://example.com/auth/logout");
diff --git a/src/juju/jimm/listeners.test.ts b/src/juju/jimm/listeners.test.ts
index dc2c18149..25fa2deaa 100644
--- a/src/juju/jimm/listeners.test.ts
+++ b/src/juju/jimm/listeners.test.ts
@@ -15,9 +15,9 @@ import { vi } from "vitest";
 
 import { thunks as appThunks } from "store/app";
 import { actions as generalActions } from "store/general";
+import type { Config } from "store/general/types";
 import type { RootState, AppDispatch } from "store/store";
 import { rootStateFactory } from "testing/factories";
-import type { WindowConfig } from "types";
 
 import { endpoints } from "./api";
 import {
@@ -58,7 +58,7 @@ describe("listeners", () => {
     fetchMock.resetMocks();
     window.jujuDashboardConfig = {
       controllerAPIEndpoint: "wss://controller.example.com",
-    } as WindowConfig;
+    } as Config;
     listenerMiddleware = createListenerMiddleware<RootState, AppDispatch>();
     const slice = createSlice({
       name: "root",
diff --git a/src/juju/jimm/thunks.test.ts b/src/juju/jimm/thunks.test.ts
index 6c10cec60..97f2c1282 100644
--- a/src/juju/jimm/thunks.test.ts
+++ b/src/juju/jimm/thunks.test.ts
@@ -1,7 +1,7 @@
 import { unwrapResult } from "@reduxjs/toolkit";
 import { vi } from "vitest";
 
-import type { WindowConfig } from "types";
+import type { Config } from "store/general/types";
 
 import { endpoints } from "./api";
 import { logout, whoami } from "./thunks";
@@ -11,7 +11,7 @@ describe("thunks", () => {
     fetchMock.resetMocks();
     window.jujuDashboardConfig = {
       controllerAPIEndpoint: "wss://example.com/api",
-    } as WindowConfig;
+    } as Config;
   });
 
   afterEach(() => {
diff --git a/src/store/app/thunks.test.ts b/src/store/app/thunks.test.ts
index fcc264daf..acea58afb 100644
--- a/src/store/app/thunks.test.ts
+++ b/src/store/app/thunks.test.ts
@@ -4,7 +4,7 @@ import { Auth, CandidAuth, OIDCAuth } from "auth";
 import { pollWhoamiStop } from "juju/jimm/listeners";
 import { actions as appActions } from "store/app";
 import { actions as generalActions } from "store/general";
-import { AuthMethod } from "store/general/types";
+import type { Config } from "store/general/types";
 import { actions as jujuActions } from "store/juju";
 import type { RootState } from "store/store";
 import { rootStateFactory } from "testing/factories";
@@ -17,7 +17,6 @@ import {
   controllerFactory,
   jujuStateFactory,
 } from "testing/factories/juju/juju";
-import type { WindowConfig } from "types";
 
 import { logOut, connectAndStartPolling } from "./thunks";
 
@@ -27,7 +26,7 @@ describe("thunks", () => {
   beforeEach(() => {
     window.jujuDashboardConfig = {
       controllerAPIEndpoint: "wss://example.com/api",
-    } as WindowConfig;
+    } as Config;
     fetchMock.resetMocks();
     state = rootStateFactory.build({
       general: generalStateFactory.build({
@@ -67,13 +66,7 @@ describe("thunks", () => {
   it("logOut", async () => {
     const action = logOut();
     const dispatch = vi.fn();
-    const getState = vi.fn(() =>
-      rootStateFactory.build({
-        general: generalStateFactory.build({
-          config: configFactory.build({ authMethod: AuthMethod.CANDID }),
-        }),
-      }),
-    );
+    const getState = vi.fn(() => rootStateFactory.build());
     const logoutSpy = vi.spyOn(CandidAuth.prototype, "logout");
     new CandidAuth(dispatch);
     await action(dispatch, getState, null);
@@ -93,13 +86,7 @@ describe("thunks", () => {
     fetchMock.mockResponseOnce(JSON.stringify({}), { status: 200 });
     const action = logOut();
     const dispatch = vi.fn();
-    const getState = vi.fn(() =>
-      rootStateFactory.build({
-        general: generalStateFactory.build({
-          config: configFactory.build({ authMethod: AuthMethod.OIDC }),
-        }),
-      }),
-    );
+    const getState = vi.fn(() => rootStateFactory.build());
     const logoutSpy = vi.spyOn(OIDCAuth.prototype, "logout");
     new OIDCAuth(dispatch);
     await action(dispatch, getState, null);
diff --git a/src/store/general/types.ts b/src/store/general/types.ts
index 6989b61bd..fcbb1c073 100644
--- a/src/store/general/types.ts
+++ b/src/store/general/types.ts
@@ -1,14 +1,7 @@
 import type { ConnectionInfo } from "@canonical/jujulib";
 
-export enum AuthMethod {
-  CANDID = "candid",
-  LOCAL = "local",
-  OIDC = "oidc",
-}
-
 export type Config = {
   analyticsEnabled: boolean;
-  authMethod: AuthMethod;
   baseAppURL: string;
   // Support for 2.9 configuration.
   baseControllerURL?: string | null;
diff --git a/src/testing/factories/general.ts b/src/testing/factories/general.ts
index 9ed1e5e2d..78b312e1c 100644
--- a/src/testing/factories/general.ts
+++ b/src/testing/factories/general.ts
@@ -7,11 +7,9 @@ import {
   type Credential,
   type GeneralState,
   type ControllerFeaturesState,
-  AuthMethod,
 } from "store/general/types";
 
 export const configFactory = Factory.define<Config>(() => ({
-  authMethod: AuthMethod.LOCAL,
   controllerAPIEndpoint: "wss://controller.example.com",
   baseAppURL: "/",
   identityProviderURL: "",
diff --git a/src/testing/utils.tsx b/src/testing/utils.tsx
index 9e533188c..9dcac5882 100644
--- a/src/testing/utils.tsx
+++ b/src/testing/utils.tsx
@@ -23,7 +23,6 @@ import generalReducer from "store/general";
 import jujuReducer from "store/juju";
 import type { RootState } from "store/store";
 import { rootStateFactory } from "testing/factories";
-import type { WindowConfig } from "types";
 
 import { configFactory } from "./factories/general";
 import queries from "./queries";
@@ -80,8 +79,7 @@ export const wrapComponent = (
       ? options.store
       : createStore(options?.state ?? rootStateFactory.build());
   const config = store.getState().general.config || configFactory.build();
-  // TODO: (WD-20710) Remove `WindowConfig` cast once `authMethod` is removed from config.
-  initialiseAuthFromConfig(config as WindowConfig, store.dispatch);
+  initialiseAuthFromConfig(config, store.dispatch);
   const router = createMemoryRouter(
     [
       {
diff --git a/src/types.ts b/src/types.ts
index 0e505642f..d175c9c16 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -4,11 +4,9 @@ import { isAction } from "redux";
 
 import type { Config } from "store/general/types";
 
-export type WindowConfig = Omit<Config, "authMethod">;
-
 declare global {
   interface Window {
-    jujuDashboardConfig?: WindowConfig;
+    jujuDashboardConfig?: Config;
   }
 }
 


Reviews:
user: login: huwshimi
body: A couple of tiny suggestions.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T23:59:07Z

Review comments:
diff_hunk: @@ -1,14 +1,20 @@
+import type { Config } from "store/general/types";
 import type { AppDispatch } from "store/store";
-import type { WindowConfig } from "types";
 
 import { CandidAuth, LocalAuth, OIDCAuth } from ".";
 
+export enum AuthMethod {
+  CANDID = "candid",
+  LOCAL = "local",
+  OIDC = "oidc",
+}
+
path: src/auth/utils.ts
user: login: huwshimi
body: I think this would be more consistent if it was in a `types.ts` file.
created_at: 2025-04-03T23:53:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -93,13 +81,7 @@ export const renderApp = () => {
 renderApp();
 
 function bootstrap() {
-  const windowConfig = window.jujuDashboardConfig;
-  const config = windowConfig
-    ? {
-        ...windowConfig,
-        authMethod: getAuthMethod(windowConfig),
-      }
-    : null;
+  const config = window.jujuDashboardConfig || null;
path: src/index.tsx
user: login: huwshimi
body: It looks like this would be handled fine if it was `undefined` so I think we can remove `|| null`.
created_at: 2025-04-03T23:56:47Z
author_association: CONTRIBUTOR

PR comments:
user: login: webteam-app
created_at: 2025-04-03T06:01:08Z
body: ### [<img src='https://assets.ubuntu.com/v1/6baef514-ubuntu-circle-of-friends-large.svg' height=32 width=32> Demo</img>](https://juju-dashboard-1918.demos.haus)
### [<img src='https://assets.ubuntu.com/v1/e512b0e2-jenkins.svg' height=32 width=32> Jenkins </img>](https://jenkins.canonical.com/webteam/job/start-demo/8888)
### [<img src='https://assets.ubuntu.com/v1/7144ec6d-logo-jaas-icon.svg' height=32 width=32> demos.haus </img>](https://demos.haus)

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-03T06:04:44Z
body: <h2>Coverage Report</h2>
<table> <thead> <tr> <th align="center">Status</th> <th align="left">Category</th> <th align="right">Percentage</th> <th align="right">Covered / Total</th> </tr> </thead> <tbody> <tr> <td align="center">🟢</td> <td align="left">Lines</td> <td align="right">98.02% (🎯 95%)</td> <td align="right">14564 / 14858</td> </tr> <tr> <td align="center">🟢</td> <td align="left">Statements</td> <td align="right">98.02% (🎯 95%)</td> <td align="right">14564 / 14858</td> </tr> <tr> <td align="center">🟢</td> <td align="left">Functions</td> <td align="right">98.05% (🎯 95%)</td> <td align="right">605 / 617</td> </tr> <tr> <td align="center">🟢</td> <td align="left">Branches</td> <td align="right">92.58% (🎯 90%)</td> <td align="right">3086 / 3333</td> </tr> </tbody> </table><details><summary>File Coverage</summary><table> <thead> <tr> <th align="left">File</th> <th align="right">Stmts</th> <th align="right">Branches</th> <th align="right">Functions</th> <th align="right">Lines</th> <th align="left">Uncovered Lines</th> </tr> </thead> <tbody> <tr> <td align="left" colspan="6"><b>Changed Files</b></td> </tr> <tr> <td align="left"><a href="https://github.com/canonical/juju-dashboard/blob/fa3ed5de5688906573d6f86d9351078d7d0ae023/src/index.tsx">src/index.tsx</a></td> <td align="right">93.04%</td> <td align="right">86.84%</td> <td align="right">100%</td> <td align="right">93.04%</td> <td align="left"><a href="https://github.com/canonical/juju-dashboard/blob/fa3ed5de5688906573d6f86d9351078d7d0ae023/src/index.tsx#L27-L31" class="text-red">27-31</a>, <a href="https://github.com/canonical/juju-dashboard/blob/fa3ed5de5688906573d6f86d9351078d7d0ae023/src/index.tsx#L87" class="text-red">87</a>, <a href="https://github.com/canonical/juju-dashboard/blob/fa3ed5de5688906573d6f86d9351078d7d0ae023/src/index.tsx#L133-L134" class="text-red">133-134</a></td> </tr> <tr> <td align="left"><a href="https://github.com/canonical/juju-dashboard/blob/fa3ed5de5688906573d6f86d9351078d7d0ae023/src/auth/Auth.ts">src/auth/Auth.ts</a></td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">85.71%</td> <td align="right">100%</td> <td align="left"></td> </tr> <tr> <td align="left"><a href="https://github.com/canonical/juju-dashboard/blob/fa3ed5de5688906573d6f86d9351078d7d0ae023/src/auth/CandidAuth.ts">src/auth/CandidAuth.ts</a></td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="left"></td> </tr> <tr> <td align="left"><a href="https://github.com/canonical/juju-dashboard/blob/fa3ed5de5688906573d6f86d9351078d7d0ae023/src/auth/LocalAuth.ts">src/auth/LocalAuth.ts</a></td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="left"></td> </tr> <tr> <td align="left"><a href="https://github.com/canonical/juju-dashboard/blob/fa3ed5de5688906573d6f86d9351078d7d0ae023/src/auth/OIDCAuth.ts">src/auth/OIDCAuth.ts</a></td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="left"></td> </tr> <tr> <td align="left"><a href="https://github.com/canonical/juju-dashboard/blob/fa3ed5de5688906573d6f86d9351078d7d0ae023/src/auth/utils.ts">src/auth/utils.ts</a></td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="left"></td> </tr> <tr> <td align="left"><a href="https://github.com/canonical/juju-dashboard/blob/fa3ed5de5688906573d6f86d9351078d7d0ae023/src/components/LogIn/LogIn.tsx">src/components/LogIn/LogIn.tsx</a></td> <td align="right">96.11%</td> <td align="right">95%</td> <td align="right">100%</td> <td align="right">96.11%</td> <td align="left"><a href="https://github.com/canonical/juju-dashboard/blob/fa3ed5de5688906573d6f86d9351078d7d0ae023/src/components/LogIn/LogIn.tsx#L76-L79" class="text-red">76-79</a></td> </tr> </tbody> </table></details>
<em>Generated in workflow <a href=https://github.com/canonical/juju-dashboard/actions/runs/14255688770>#482</a> for commit <a href="https://github.com/canonical/juju-dashboard/commit/fa3ed5de5688906573d6f86d9351078d7d0ae023">fa3ed5d</a> by the <a href="https://github.com/davelosert/vitest-coverage-report-action">Vitest Coverage Report Action</a></em>

<!-- vitest-coverage-report-marker-root -->
author_association: NONE