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" + } + }} + /> + ) +};