-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
169efe9
commit 7be0fbd
Showing
15 changed files
with
404 additions
and
37 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { FactoryProvider } from '@nestjs/common'; | ||
import { IEmailServiceToken } from '@api/modules/notifications/email/email-service.interface'; | ||
import { MockEmailService } from '../../../../test/utils/mocks/mock-email.service'; | ||
import { NodemailerEmailService } from '@api/modules/notifications/email/nodemailer.email.service'; | ||
import { ApiConfigService } from '@api/modules/config/app-config.service'; | ||
import { EventBus } from '@nestjs/cqrs'; | ||
|
||
export const EmailProviderFactory: FactoryProvider = { | ||
provide: IEmailServiceToken, | ||
useFactory: (configService: ApiConfigService, eventBus: EventBus) => { | ||
const env = configService.get('NODE_ENV'); | ||
return env === 'test' | ||
? new MockEmailService() | ||
: new NodemailerEmailService(eventBus, configService); | ||
}, | ||
inject: [ApiConfigService], | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,20 @@ | ||
import { IEmailServiceInterface } from '@api/modules/notifications/email/email-service.interface'; | ||
import { | ||
IEmailServiceInterface, | ||
SendMailDTO, | ||
} from '@api/modules/notifications/email/email-service.interface'; | ||
import { Logger } from '@nestjs/common'; | ||
|
||
export class MockEmailService implements IEmailServiceInterface { | ||
logger: Logger = new Logger(MockEmailService.name); | ||
|
||
sendMail = jest.fn(async (): Promise<void> => { | ||
this.logger.log('Mock Email sent'); | ||
return Promise.resolve(); | ||
}); | ||
sendMail = | ||
typeof jest !== 'undefined' | ||
? jest.fn(async (sendMailDTO: SendMailDTO): Promise<void> => { | ||
this.logger.log('Mock Email sent', this.constructor.name); | ||
return Promise.resolve(); | ||
}) | ||
: async (sendMailDTO: SendMailDTO): Promise<void> => { | ||
this.logger.log('Mock Email sent', this.constructor.name); | ||
return Promise.resolve(); | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import ConfirmEmailForm from "@/containers/auth/confirm-email/form"; | ||
|
||
export default function ConfirmEmailPage() { | ||
return <ConfirmEmailForm />; | ||
} |
139 changes: 139 additions & 0 deletions
139
client/src/containers/auth/confirm-email/form/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
"use client"; | ||
|
||
import { FC, FormEvent, useCallback, useRef } from "react"; | ||
|
||
import { useForm } from "react-hook-form"; | ||
|
||
import { useParams, useRouter, useSearchParams } from "next/navigation"; | ||
|
||
import { zodResolver } from "@hookform/resolvers/zod"; | ||
import { TOKEN_TYPE_ENUM } from "@shared/schemas/auth/token-type.schema"; | ||
import { RequestEmailUpdateSchema } from "@shared/schemas/users/request-email-update.schema"; | ||
import { useQuery } from "@tanstack/react-query"; | ||
import { z } from "zod"; | ||
|
||
import { client } from "@/lib/query-client"; | ||
import { queryKeys } from "@/lib/query-keys"; | ||
|
||
import { Button } from "@/components/ui/button"; | ||
import { | ||
Form, | ||
FormControl, | ||
FormField, | ||
FormItem, | ||
FormMessage, | ||
} from "@/components/ui/form"; | ||
import { Input } from "@/components/ui/input"; | ||
import { useApiResponseToast } from "@/components/ui/toast/use-api-response-toast"; | ||
|
||
const NewPasswordForm: FC = () => { | ||
const router = useRouter(); | ||
const params = useParams<{ token: string }>(); | ||
const searchParams = useSearchParams(); | ||
const newEmail = searchParams.get("newEmail"); | ||
|
||
const formRef = useRef<HTMLFormElement>(null); | ||
const form = useForm<z.infer<typeof RequestEmailUpdateSchema>>({ | ||
resolver: zodResolver(RequestEmailUpdateSchema), | ||
defaultValues: { | ||
newEmail: newEmail as NonNullable<typeof newEmail>, | ||
}, | ||
}); | ||
const { apiResponseToast, toast } = useApiResponseToast(); | ||
|
||
const { | ||
data: isValidToken, | ||
isFetching, | ||
isError, | ||
} = useQuery({ | ||
queryKey: queryKeys.auth.confirmEmailToken(params.token).queryKey, | ||
queryFn: () => { | ||
return client.auth.validateToken.query({ | ||
headers: { | ||
authorization: `Bearer ${params.token}`, | ||
}, | ||
query: { | ||
tokenType: TOKEN_TYPE_ENUM.EMAIL_CONFIRMATION, | ||
}, | ||
}); | ||
}, | ||
select: (data) => data.status === 200, | ||
}); | ||
|
||
const handleEmailConfirmation = useCallback( | ||
(evt: FormEvent<HTMLFormElement>) => { | ||
evt.preventDefault(); | ||
|
||
form.handleSubmit(async (formValues) => { | ||
try { | ||
const { status, body } = await client.auth.confirmEmail.mutation({ | ||
body: formValues, | ||
extraHeaders: { | ||
authorization: `Bearer ${params.token}`, | ||
}, | ||
}); | ||
apiResponseToast( | ||
{ status, body }, | ||
{ | ||
successMessage: "Email updated successfully.", | ||
}, | ||
); | ||
router.push("/auth/signin"); | ||
} catch (err) { | ||
toast({ | ||
variant: "destructive", | ||
description: "Something went wrong", | ||
}); | ||
} | ||
})(evt); | ||
}, | ||
[form, apiResponseToast, toast, params.token, router], | ||
); | ||
|
||
const isDisabled = isFetching || isError || !isValidToken; | ||
|
||
return ( | ||
<div className="space-y-8 rounded-2xl py-6"> | ||
<div className="space-y-4 px-6"> | ||
<h2 className="text-xl font-semibold">Confirm email</h2> | ||
{!isValidToken && ( | ||
<p className="text-sm text-destructive"> | ||
The token is invalid or has expired. | ||
</p> | ||
)} | ||
</div> | ||
<Form {...form}> | ||
<form | ||
ref={formRef} | ||
className="w-full space-y-8" | ||
onSubmit={handleEmailConfirmation} | ||
> | ||
<FormField | ||
control={form.control} | ||
name="newEmail" | ||
render={({ field }) => ( | ||
<FormItem> | ||
<FormControl> | ||
<Input type="hidden" {...field} /> | ||
</FormControl> | ||
<FormMessage /> | ||
</FormItem> | ||
)} | ||
/> | ||
<div className="space-y-2 px-6"> | ||
<Button | ||
variant="secondary" | ||
type="submit" | ||
className="w-full" | ||
disabled={isDisabled} | ||
> | ||
Confirm email | ||
</Button> | ||
</div> | ||
</form> | ||
</Form> | ||
</div> | ||
); | ||
}; | ||
|
||
export default NewPasswordForm; |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import { expect, Page, test } from "@playwright/test"; | ||
import { E2eTestManager } from "@shared/lib/e2e-test-manager"; | ||
import { User } from "@shared/entities/users/user.entity"; | ||
import { ROLES } from "@shared/entities/users/roles.enum"; | ||
|
||
let testManager: E2eTestManager; | ||
let page: Page; | ||
|
||
test.describe.configure({ mode: "serial" }); | ||
|
||
test.describe("Auth - Delete Account", () => { | ||
test.beforeAll(async ({ browser }) => { | ||
page = await browser.newPage(); | ||
testManager = await E2eTestManager.load(page); | ||
}); | ||
|
||
test.beforeEach(async () => { | ||
await testManager.clearDatabase(); | ||
}); | ||
|
||
test.afterEach(async () => { | ||
// await testManager.clearDatabase(); | ||
}); | ||
|
||
test.afterAll(async () => { | ||
await testManager.close(); | ||
}); | ||
|
||
test("an user deletes their account successfully", async () => { | ||
const user: Pick<User, "email" | "password" | "partnerName" | "role"> = { | ||
email: "[email protected]", | ||
password: "12345678", | ||
partnerName: "partner-test", | ||
role: ROLES.ADMIN, | ||
}; | ||
|
||
await testManager.mocks().createUser(user); | ||
await testManager.login(user as User); | ||
|
||
await page.waitForURL('/profile'); | ||
|
||
await page.getByRole('button', { name: 'Delete account' }).click(); | ||
await page.getByRole('button', { name: 'Delete account' }).click(); | ||
|
||
await page.waitForURL('/auth/signin'); | ||
|
||
await page.getByLabel("Email").fill(user.email); | ||
await page.locator('input[type="password"]').fill(user.password); | ||
await page.getByRole("button", { name: /log in/i }).click(); | ||
|
||
|
||
await expect(page.getByText('Invalid credentials')).toBeVisible(); | ||
}); | ||
}); |
Oops, something went wrong.