From 3ff01d186d3eda6de5c7033d16d454ed75d05768 Mon Sep 17 00:00:00 2001
From: Nima Shkouhfar <nima.shokofar@gmail.com>
Date: Sat, 19 Oct 2024 19:10:32 -0400
Subject: [PATCH] account page test coverage improved

---
 stories/account/pages/Account.stories.tsx     | 102 ++++++++++++
 .../account/pages/Applications.stories.tsx    | 148 ++++++++++++++++++
 .../pages/FederatedIdentity.stories.tsx       |  58 +++++++
 stories/account/pages/Log.stories.tsx         | 104 ++++++++++++
 stories/account/pages/Password.stories.tsx    |  76 +++++++++
 stories/account/pages/Sessions.stories.tsx    |  94 +++++++++++
 stories/account/pages/Totp.stories.tsx        |  61 ++++++++
 7 files changed, 643 insertions(+)

diff --git a/stories/account/pages/Account.stories.tsx b/stories/account/pages/Account.stories.tsx
index 04e654d2..4eac998c 100644
--- a/stories/account/pages/Account.stories.tsx
+++ b/stories/account/pages/Account.stories.tsx
@@ -16,3 +16,105 @@ type Story = StoryObj<typeof meta>;
 export const Default: Story = {
     render: () => <KcPageStory />
 };
+
+/**
+ * UsernameNotEditable:
+ * - Purpose: Test the scenario where the username field is not editable.
+ * - Scenario: The component renders, but the username field is disabled.
+ * - Key Aspect: Ensures that the `editUsernameAllowed` condition is respected and the username field is read-only.
+ */
+export const UsernameNotEditable: Story = {
+    render: () => (
+        <KcPageStory
+            kcContext={{
+                account: {
+                    username: "john_doe",
+                    email: "john.doe@gmail.com",
+                    firstName: "John",
+                    lastName: "Doe"
+                },
+                realm: {
+                    registrationEmailAsUsername: false,
+                    editUsernameAllowed: false
+                },
+                referrer: {
+                    url: "/home"
+                },
+                url: {
+                    accountUrl: "/account"
+                },
+                messagesPerField: {
+                    printIfExists: () => ""
+                },
+                stateChecker: "state-checker"
+            }}
+        />
+    )
+};
+
+/**
+ * WithValidationErrors:
+ * - Purpose: Test the form when there are validation errors.
+ * - Scenario: The component renders with error messages for invalid input in the fields.
+ * - Key Aspect: Ensures that error messages are properly displayed and the user can correct their inputs.
+ */
+export const WithValidationErrors: Story = {
+    render: () => (
+        <KcPageStory
+            kcContext={{
+                account: {
+                    username: "john_doe",
+                    email: "",
+                    firstName: "",
+                    lastName: "Doe"
+                },
+                realm: {
+                    registrationEmailAsUsername: false,
+                    editUsernameAllowed: true
+                },
+                referrer: {
+                    url: "/home"
+                },
+                url: {
+                    accountUrl: "/account"
+                },
+                messagesPerField: {
+                    printIfExists: field => (field === "email" || field === "firstName" ? "has-error" : "")
+                },
+                stateChecker: "state-checker"
+            }}
+        />
+    )
+};
+/**
+ * EmailAsUsername:
+ * - Purpose: Test the form where email is used as the username.
+ * - Scenario: The component renders without a separate username field, and the email field is treated as the username.
+ * - Key Aspect: Ensures the form functions correctly when `registrationEmailAsUsername` is enabled.
+ */
+export const EmailAsUsername: Story = {
+    render: () => (
+        <KcPageStory
+            kcContext={{
+                account: {
+                    email: "john.doe@gmail.com",
+                    firstName: "John",
+                    lastName: "Doe"
+                },
+                realm: {
+                    registrationEmailAsUsername: true
+                },
+                referrer: {
+                    url: "/home"
+                },
+                url: {
+                    accountUrl: "/account"
+                },
+                messagesPerField: {
+                    printIfExists: () => ""
+                },
+                stateChecker: "state-checker"
+            }}
+        />
+    )
+};
diff --git a/stories/account/pages/Applications.stories.tsx b/stories/account/pages/Applications.stories.tsx
index c531c03d..e0b85893 100644
--- a/stories/account/pages/Applications.stories.tsx
+++ b/stories/account/pages/Applications.stories.tsx
@@ -78,3 +78,151 @@ export const Default: Story = {
         />
     )
 };
+
+// No Available Roles or Grants Scenario
+export const NoAvailableRolesOrGrants: Story = {
+    render: () => (
+        <KcPageStory
+            kcContext={{
+                pageId: "applications.ftl",
+                applications: {
+                    applications: [
+                        {
+                            realmRolesAvailable: [],
+                            resourceRolesAvailable: {},
+                            additionalGrants: [],
+                            clientScopesGranted: [],
+                            effectiveUrl: "#",
+                            client: {
+                                clientId: "application1",
+                                name: "Application 1",
+                                consentRequired: true
+                            }
+                        }
+                    ]
+                }
+            }}
+        />
+    )
+};
+
+// Consent Not Required Scenario
+export const ConsentNotRequired: Story = {
+    render: () => (
+        <KcPageStory
+            kcContext={{
+                pageId: "applications.ftl",
+                applications: {
+                    applications: [
+                        {
+                            realmRolesAvailable: [],
+                            resourceRolesAvailable: {},
+                            additionalGrants: [],
+                            clientScopesGranted: [],
+                            effectiveUrl: "#",
+                            client: {
+                                clientId: "application1",
+                                name: "Application 1",
+                                consentRequired: false // No consent required
+                            }
+                        }
+                    ]
+                }
+            }}
+        />
+    )
+};
+
+// No Roles Available but Consent Required Scenario
+export const NoRolesButConsentRequired: Story = {
+    render: () => (
+        <KcPageStory
+            kcContext={{
+                pageId: "applications.ftl",
+                applications: {
+                    applications: [
+                        {
+                            realmRolesAvailable: [],
+                            resourceRolesAvailable: {},
+                            additionalGrants: [],
+                            clientScopesGranted: ["scope1", "scope2"], // Consent required but no roles
+                            effectiveUrl: "#",
+                            client: {
+                                clientId: "application1",
+                                name: "Application 1",
+                                consentRequired: true
+                            }
+                        }
+                    ]
+                }
+            }}
+        />
+    )
+};
+
+// Only Resource Roles Available Scenario
+export const OnlyResourceRolesAvailable: Story = {
+    render: () => (
+        <KcPageStory
+            kcContext={{
+                pageId: "applications.ftl",
+                applications: {
+                    applications: [
+                        {
+                            realmRolesAvailable: [], // No realm roles
+                            resourceRolesAvailable: {
+                                resource1: [
+                                    {
+                                        roleName: "Resource Role Name 1",
+                                        roleDescription: "Resource role 1 description",
+                                        clientName: "Client Name 1",
+                                        clientId: "client1"
+                                    }
+                                ]
+                            },
+                            additionalGrants: [],
+                            clientScopesGranted: [],
+                            effectiveUrl: "#",
+                            client: {
+                                clientId: "application1",
+                                name: "Application 1",
+                                consentRequired: true
+                            }
+                        }
+                    ]
+                }
+            }}
+        />
+    )
+};
+
+// No Additional Grants Scenario
+export const NoAdditionalGrants: Story = {
+    render: () => (
+        <KcPageStory
+            kcContext={{
+                pageId: "applications.ftl",
+                applications: {
+                    applications: [
+                        {
+                            realmRolesAvailable: [
+                                {
+                                    name: "Realm Role Name 1"
+                                }
+                            ],
+                            resourceRolesAvailable: {},
+                            additionalGrants: [], // No additional grants
+                            clientScopesGranted: [],
+                            effectiveUrl: "#",
+                            client: {
+                                clientId: "application1",
+                                name: "Application 1",
+                                consentRequired: true
+                            }
+                        }
+                    ]
+                }
+            }}
+        />
+    )
+};
diff --git a/stories/account/pages/FederatedIdentity.stories.tsx b/stories/account/pages/FederatedIdentity.stories.tsx
index ccb29405..86c556fc 100644
--- a/stories/account/pages/FederatedIdentity.stories.tsx
+++ b/stories/account/pages/FederatedIdentity.stories.tsx
@@ -36,3 +36,61 @@ export const NotConnected: Story = {
         />
     )
 };
+
+/**
+ * RemoveLinkNotPossible:
+ * - Federated identities are connected, but the user cannot remove them due to restrictions.
+ */
+export const RemoveLinkNotPossible: Story = {
+    render: () => (
+        <KcPageStory
+            kcContext={{
+                pageId: "federatedIdentity.ftl",
+                federatedIdentity: {
+                    identities: [
+                        {
+                            providerId: "google",
+                            displayName: "Google",
+                            userName: "john.doe@gmail.com",
+                            connected: true
+                        }
+                    ],
+                    removeLinkPossible: false
+                },
+                stateChecker: "1234",
+                url: {
+                    socialUrl: "/social"
+                }
+            }}
+        />
+    )
+};
+
+/**
+ * AddLinkForUnconnectedIdentity:
+ * - The user has an identity that is not connected and can add it.
+ */
+export const AddLinkForUnconnectedIdentity: Story = {
+    render: () => (
+        <KcPageStory
+            kcContext={{
+                pageId: "federatedIdentity.ftl",
+                federatedIdentity: {
+                    identities: [
+                        {
+                            providerId: "github",
+                            displayName: "GitHub",
+                            userName: "",
+                            connected: false
+                        }
+                    ],
+                    removeLinkPossible: true
+                },
+                stateChecker: "1234",
+                url: {
+                    socialUrl: "/social"
+                }
+            }}
+        />
+    )
+};
diff --git a/stories/account/pages/Log.stories.tsx b/stories/account/pages/Log.stories.tsx
index 6b9ba1b4..754f19fc 100644
--- a/stories/account/pages/Log.stories.tsx
+++ b/stories/account/pages/Log.stories.tsx
@@ -355,3 +355,107 @@ export const Default: Story = {
         />
     )
 };
+export const LogsMissingDetails: Story = {
+    render: () => (
+        <KcPageStory
+            kcContext={{
+                pageId: "log.ftl",
+                log: {
+                    events: [
+                        {
+                            date: "2024-04-26T12:29:08Z",
+                            ipAddress: "127.0.0.1",
+                            client: "",
+                            details: [],
+                            event: "login"
+                        }
+                    ]
+                }
+            }}
+        />
+    )
+};
+export const SingleLogEntry: Story = {
+    render: () => (
+        <KcPageStory
+            kcContext={{
+                pageId: "log.ftl",
+                log: {
+                    events: [
+                        {
+                            date: "2024-04-26T12:29:08Z",
+                            ipAddress: "127.0.0.1",
+                            client: "keycloakify-frontend",
+                            details: [
+                                { key: "auth_method", value: "openid-connect" },
+                                { key: "username", value: "john.doe" }
+                            ],
+                            event: "login"
+                        }
+                    ]
+                }
+            }}
+        />
+    )
+};
+export const LogsWithLongDetails: Story = {
+    render: () => (
+        <KcPageStory
+            kcContext={{
+                pageId: "log.ftl",
+                log: {
+                    events: [
+                        {
+                            date: "2024-04-26T12:29:08Z",
+                            ipAddress: "127.0.0.1",
+                            client: "keycloakify-frontend",
+                            details: [
+                                { key: "auth_method", value: "openid-connect" },
+                                { key: "username", value: "john.doe" },
+                                { key: "session_duration", value: "2 hours 30 minutes 45 seconds" },
+                                { key: "location", value: "Windsor, Ontario, Canada" },
+                                { key: "user_agent", value: "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" }
+                            ],
+                            event: "login"
+                        }
+                    ]
+                }
+            }}
+        />
+    )
+};
+export const EmptyClientField: Story = {
+    render: () => (
+        <KcPageStory
+            kcContext={{
+                pageId: "log.ftl",
+                log: {
+                    events: [
+                        {
+                            date: "2024-04-26T12:29:08Z",
+                            ipAddress: "127.0.0.1",
+                            client: "", // Empty client field
+                            details: [
+                                { key: "auth_method", value: "openid-connect" },
+                                { key: "username", value: "john.doe" }
+                            ],
+                            event: "login"
+                        }
+                    ]
+                }
+            }}
+        />
+    )
+};
+export const NoLogsAvailable: Story = {
+    render: () => (
+        <KcPageStory
+            kcContext={{
+                pageId: "log.ftl",
+                log: {
+                    events: [] // No log events
+                }
+            }}
+        />
+    )
+};
diff --git a/stories/account/pages/Password.stories.tsx b/stories/account/pages/Password.stories.tsx
index b9632bd9..7cd7efbf 100644
--- a/stories/account/pages/Password.stories.tsx
+++ b/stories/account/pages/Password.stories.tsx
@@ -26,3 +26,79 @@ export const WithMessage: Story = {
         />
     )
 };
+/**
+ * FirstTimePasswordSetup:
+ * - Purpose: Tests the page when no password is set (e.g., first login).
+ * - Scenario: This renders the form without the current password field.
+ * - Key Aspect: Ensures the page only displays fields for setting a new password.
+ */
+export const FirstTimePasswordSetup: Story = {
+    render: () => (
+        <KcPageStory
+            kcContext={{
+                account: {
+                    username: "john_doe"
+                },
+                password: {
+                    passwordSet: false
+                },
+                url: {
+                    passwordUrl: "/password"
+                },
+                stateChecker: "state-checker"
+            }}
+        />
+    )
+};
+
+/**
+ * IncorrectCurrentPassword:
+ * - Purpose: Simulates validation error when the current password is incorrect.
+ * - Scenario: This renders the page with an error message indicating the current password is incorrect.
+ * - Key Aspect: Validates that an error message is correctly displayed for the current password input.
+ */
+export const IncorrectCurrentPassword: Story = {
+    render: () => (
+        <KcPageStory
+            kcContext={{
+                message: { type: "error", summary: "Incorrect current password." },
+                account: {
+                    username: "john_doe"
+                },
+                password: {
+                    passwordSet: true
+                },
+                url: {
+                    passwordUrl: "/password"
+                },
+                stateChecker: "state-checker"
+            }}
+        />
+    )
+};
+
+/**
+ * SubmissionSuccessWithRedirect:
+ * - Purpose: Simulates a successful form submission with a redirect or success message.
+ * - Scenario: After successfully changing the password, a success message and redirect behavior are triggered.
+ * - Key Aspect: Verifies the handling of successful submissions.
+ */
+export const SubmissionSuccessWithRedirect: Story = {
+    render: () => (
+        <KcPageStory
+            kcContext={{
+                message: { type: "success", summary: "Password successfully changed." },
+                account: {
+                    username: "john_doe"
+                },
+                password: {
+                    passwordSet: true
+                },
+                url: {
+                    passwordUrl: "/password"
+                },
+                stateChecker: "state-checker"
+            }}
+        />
+    )
+};
diff --git a/stories/account/pages/Sessions.stories.tsx b/stories/account/pages/Sessions.stories.tsx
index 434622f5..8e86b344 100644
--- a/stories/account/pages/Sessions.stories.tsx
+++ b/stories/account/pages/Sessions.stories.tsx
@@ -57,3 +57,97 @@ export const WithError: Story = {
         />
     )
 };
+/**
+ * No active sessions scenario:
+ * - Simulates the scenario where no sessions are active for the user.
+ */
+export const NoActiveSessions: Story = {
+    render: () => (
+        <KcPageStory
+            kcContext={{
+                sessions: {
+                    sessions: []
+                },
+                stateChecker: "randomStateCheckerValue"
+            }}
+        />
+    )
+};
+
+/**
+ * Single session scenario:
+ * - Displays only one active session with session details.
+ */
+export const SingleSession: Story = {
+    render: () => (
+        <KcPageStory
+            kcContext={{
+                sessions: {
+                    sessions: [
+                        {
+                            expires: "2024-04-26T18:14:19Z",
+                            clients: ["account"],
+                            ipAddress: "172.20.0.1",
+                            started: "2024-04-26T08:14:19Z",
+                            lastAccess: "2024-04-26T08:30:54Z",
+                            id: "single-session-id"
+                        }
+                    ]
+                },
+                stateChecker: "anotherStateChecker"
+            }}
+        />
+    )
+};
+
+/**
+ * Multiple clients per session scenario:
+ * - Displays sessions where each session has multiple associated clients.
+ */
+export const MultipleClientsSession: Story = {
+    render: () => (
+        <KcPageStory
+            kcContext={{
+                sessions: {
+                    sessions: [
+                        {
+                            expires: "2024-04-26T18:14:19Z",
+                            clients: ["account", "admin-console", "another-client"],
+                            ipAddress: "172.20.0.1",
+                            started: "2024-04-26T08:14:19Z",
+                            lastAccess: "2024-04-26T08:30:54Z",
+                            id: "multiple-clients-session"
+                        }
+                    ]
+                },
+                stateChecker: "multiClientsStateChecker"
+            }}
+        />
+    )
+};
+
+/**
+ * Session without client details scenario:
+ * - Simulates a session where no client information is provided.
+ */
+export const SessionWithoutClients: Story = {
+    render: () => (
+        <KcPageStory
+            kcContext={{
+                sessions: {
+                    sessions: [
+                        {
+                            expires: "2024-04-26T18:14:19Z",
+                            clients: [], // No clients information
+                            ipAddress: "172.20.0.1",
+                            started: "2024-04-26T08:14:19Z",
+                            lastAccess: "2024-04-26T08:30:54Z",
+                            id: "no-clients-session"
+                        }
+                    ]
+                },
+                stateChecker: "noClientsStateChecker"
+            }}
+        />
+    )
+};
diff --git a/stories/account/pages/Totp.stories.tsx b/stories/account/pages/Totp.stories.tsx
index 1ad1f027..84e060f9 100644
--- a/stories/account/pages/Totp.stories.tsx
+++ b/stories/account/pages/Totp.stories.tsx
@@ -180,3 +180,64 @@ export const MoreThanOneTotpProviders: Story = {
         />
     )
 };
+
+// TOTP Enabled but No Existing OTP Credentials
+export const TotpEnabledNoOtpCredentials: Story = {
+    render: () => (
+        <KcPageStory
+            kcContext={{
+                totp: {
+                    enabled: true,
+                    totpSecretEncoded: "HE4W MSTC OBKU CY2M",
+                    otpCredentials: [] // No OTP Credentials
+                },
+                stateChecker: "stateChecker123",
+                url: {
+                    totpUrl: "http://localhost:8080/realms/myrealm/account/totp"
+                }
+            }}
+        />
+    )
+};
+
+// Manual Mode TOTP without Scanning
+export const ManualModeTotp: Story = {
+    render: () => (
+        <KcPageStory
+            kcContext={{
+                mode: "manual", // Manual mode
+                totp: {
+                    enabled: false,
+                    totpSecretEncoded: "HE4W MSTC OBKU CY2M",
+                    otpCredentials: []
+                },
+                stateChecker: "stateChecker123",
+                url: {
+                    totpUrl: "http://localhost:8080/realms/myrealm/account/totp"
+                }
+            }}
+        />
+    )
+};
+
+// Multiple OTP Devices Scenario
+export const MultipleOtpDevices: Story = {
+    render: () => (
+        <KcPageStory
+            kcContext={{
+                totp: {
+                    enabled: true,
+                    totpSecretEncoded: "G55E MZKC JFUD",
+                    otpCredentials: [
+                        { id: "1", userLabel: "Phone 1" },
+                        { id: "2", userLabel: "Tablet" }
+                    ]
+                },
+                stateChecker: "stateChecker123",
+                url: {
+                    totpUrl: "http://localhost:8080/realms/myrealm/account/totp"
+                }
+            }}
+        />
+    )
+};