|
23 | 23 |
|
24 | 24 | import React from 'react'; |
25 | 25 | import { describe, it, expect, vi } from 'vitest'; |
26 | | -import { render, screen, fireEvent } from '@testing-library/react'; |
| 26 | +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; |
27 | 27 | import { AuthProvider } from '../AuthProvider'; |
28 | 28 | import { ForgotPasswordForm } from '../ForgotPasswordForm'; |
29 | 29 | import type { AuthClient, AuthPublicConfig } from '../types'; |
@@ -110,3 +110,87 @@ describe('ForgotPasswordForm — success message substitution (objectui#4135)', |
110 | 110 | expect(occurrences(description.textContent ?? '', 'control@example.com')).toBe(1); |
111 | 111 | }); |
112 | 112 | }); |
| 113 | + |
| 114 | +/** |
| 115 | + * objectstack#30366 — on a deployment that sends no mail the success screen |
| 116 | + * must not say a reset link was sent. The server reports this as |
| 117 | + * `features.emailDelivery` on `/auth/config`; only an explicit `false` swaps |
| 118 | + * the text, and the swap depends on the deployment alone, so a known and an |
| 119 | + * unknown address read the same (no account-existence oracle). |
| 120 | + */ |
| 121 | +describe('ForgotPasswordForm — success screen on a deployment that sends no mail (objectstack#30366)', () => { |
| 122 | + function renderWithConfig( |
| 123 | + getConfig: AuthClient['getConfig'], |
| 124 | + props: React.ComponentProps<typeof ForgotPasswordForm> = {}, |
| 125 | + ) { |
| 126 | + const client = createMockClient({ getConfig }); |
| 127 | + const view = render( |
| 128 | + <AuthProvider authUrl="/api/auth" client={client}> |
| 129 | + <ForgotPasswordForm {...props} /> |
| 130 | + </AuthProvider>, |
| 131 | + ); |
| 132 | + return { client, view }; |
| 133 | + } |
| 134 | + |
| 135 | + const noMail = () => vi.fn().mockResolvedValue({ features: { emailDelivery: false } } as AuthPublicConfig); |
| 136 | + |
| 137 | + it('says a reset link cannot be emailed and to ask an administrator when emailDelivery is false', async () => { |
| 138 | + const getConfig = noMail(); |
| 139 | + renderWithConfig(getConfig); |
| 140 | + await waitFor(() => expect(getConfig).toHaveBeenCalled()); |
| 141 | + await submit('someone@example.com'); |
| 142 | + |
| 143 | + expect(await screen.findByText("A reset link can't be emailed here")).toBeTruthy(); |
| 144 | + const description = screen.getByText(/doesn't send email/); |
| 145 | + expect(description.textContent).toMatch(/administrator/); |
| 146 | + expect(description.textContent).toMatch(/Setup → Users/); |
| 147 | + expect(description.textContent).not.toContain('someone@example.com'); |
| 148 | + expect(screen.queryByText('Check your email')).toBeNull(); |
| 149 | + expect(screen.queryByText(/We've sent a password reset link/)).toBeNull(); |
| 150 | + }); |
| 151 | + |
| 152 | + it('reads a known and an unknown address the same: the text follows the deployment, not the address', async () => { |
| 153 | + // The reset request answers both alike (the server's no-oracle rule); the |
| 154 | + // screen must add no difference of its own, address substitution included. |
| 155 | + const screens: string[] = []; |
| 156 | + for (const address of ['known@example.com', 'nobody-here@example.com']) { |
| 157 | + const getConfig = noMail(); |
| 158 | + const { client, view } = renderWithConfig(getConfig); |
| 159 | + await waitFor(() => expect(getConfig).toHaveBeenCalled()); |
| 160 | + await submit(address); |
| 161 | + await screen.findByText("A reset link can't be emailed here"); |
| 162 | + expect(client.forgotPassword).toHaveBeenCalledWith(address); |
| 163 | + screens.push(view.container.textContent ?? ''); |
| 164 | + view.unmount(); |
| 165 | + } |
| 166 | + expect(screens[0]).toBe(screens[1]); |
| 167 | + expect(screens[0]).not.toContain('@example.com'); |
| 168 | + }); |
| 169 | + |
| 170 | + it('uses caller-supplied translated labels for the no-delivery screen', async () => { |
| 171 | + const getConfig = noMail(); |
| 172 | + renderWithConfig(getConfig, { |
| 173 | + labels: { noDeliveryTitle: 'NO-MAIL TITLE', noDeliveryDescription: 'NO-MAIL BODY' }, |
| 174 | + }); |
| 175 | + await waitFor(() => expect(getConfig).toHaveBeenCalled()); |
| 176 | + await submit('someone@example.com'); |
| 177 | + |
| 178 | + expect(await screen.findByText('NO-MAIL TITLE')).toBeTruthy(); |
| 179 | + expect(screen.getByText('NO-MAIL BODY')).toBeTruthy(); |
| 180 | + }); |
| 181 | + |
| 182 | + it.each([ |
| 183 | + ['emailDelivery is true', () => vi.fn().mockResolvedValue({ features: { emailDelivery: true } } as AuthPublicConfig)], |
| 184 | + ['the server sends no emailDelivery key (older server)', () => vi.fn().mockResolvedValue({ features: {} } as AuthPublicConfig)], |
| 185 | + ['the config fetch fails', () => vi.fn().mockRejectedValue(new Error('offline'))], |
| 186 | + ])('keeps the "check your email" screen when %s', async (_label, makeGetConfig) => { |
| 187 | + const getConfig = makeGetConfig(); |
| 188 | + renderWithConfig(getConfig); |
| 189 | + await waitFor(() => expect(getConfig).toHaveBeenCalled()); |
| 190 | + await submit('someone@example.com'); |
| 191 | + |
| 192 | + expect(await screen.findByText('Check your email')).toBeTruthy(); |
| 193 | + expect(screen.getByText(/We've sent a password reset link to someone@example.com/)).toBeTruthy(); |
| 194 | + expect(screen.queryByText("A reset link can't be emailed here")).toBeNull(); |
| 195 | + }); |
| 196 | +}); |
0 commit comments