alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
228 lines (193 loc) • 6.53 kB
text/typescript
import { Alepha } from "alepha";
import { DateTimeProvider } from "alepha/datetime";
import { AlephaOrmPostgres } from "alepha/orm/postgres";
import { describe, it } from "vitest";
import {
AlephaApiVerification,
VerificationController,
VerificationParameters,
VerificationService,
} from "../index.ts";
const createTest = async () => {
const alepha = Alepha.create()
.with(AlephaOrmPostgres)
.with(AlephaApiVerification);
const parameters = alepha.inject(VerificationParameters).get("link");
const controller = alepha.inject(VerificationController);
const service = alepha.inject(VerificationService);
const dateTimeProvider = alepha.inject(DateTimeProvider);
const target = "test@example.com";
await alepha.start();
return {
alepha,
service,
parameters,
controller,
dateTimeProvider,
target,
};
};
describe("Link Verification", () => {
it("should verify link with UUID token correctly", async ({ expect }) => {
const { parameters, controller, target, service } = await createTest();
const request = await service.createVerification({ type: "link", target });
expect(request.codeExpiration).toEqual(parameters.codeExpiration);
expect(request.verificationCooldown).toEqual(
parameters.verificationCooldown,
);
expect(request.maxVerificationAttempts).toEqual(parameters.maxAttempts);
expect(request.token).toBeTruthy();
// UUID format validation
const token = request.token;
expect(token).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,
);
expect(
await controller.validateVerificationCode({
params: {
type: "link",
},
body: {
target,
token,
},
}),
).toEqual({
ok: true,
});
expect(
await controller.validateVerificationCode({
params: {
type: "link",
},
body: {
target,
token,
},
}),
).toEqual({
ok: true,
alreadyVerified: true,
});
});
it("should handle invalid token", async ({ expect }) => {
const { controller, target, service } = await createTest();
await service.createVerification({ type: "link", target });
await expect(() =>
controller.validateVerificationCode({
params: {
type: "link",
},
body: {
target,
token: "550e8400-e29b-41d4-a716-446655440000",
},
}),
).rejects.toThrowError("Invalid verification code");
});
it("should handle max attempts", async ({ expect }) => {
const { parameters, controller, target, service } = await createTest();
await service.createVerification({ type: "link", target });
for (let i = 0; i < parameters.maxAttempts; i++) {
await controller
.validateVerificationCode({
params: {
type: "link",
},
body: {
target,
token: "550e8400-e29b-41d4-a716-446655440000",
},
})
.catch(() => null);
}
await expect(() =>
controller.validateVerificationCode({
params: {
type: "link",
},
body: {
target,
token: "550e8400-e29b-41d4-a716-446655440000",
},
}),
).rejects.toThrowError("Maximum number of attempts reached");
});
it("should handle cooldown", async ({ expect }) => {
const { dateTimeProvider, parameters, target, service } =
await createTest();
await service.createVerification({ type: "link", target });
await expect(() =>
service.createVerification({ type: "link", target }),
).rejects.toThrowError("Verification is on cooldown for ");
await dateTimeProvider.travel(
parameters.verificationCooldown + 1,
"seconds",
);
const response = await service.createVerification({ type: "link", target });
expect(response.codeExpiration).toEqual(parameters.codeExpiration);
expect(response.verificationCooldown).toEqual(
parameters.verificationCooldown,
);
expect(response.maxVerificationAttempts).toEqual(parameters.maxAttempts);
expect(response.token).toBeTruthy();
});
it("should respect rate limit per day", async ({ expect }) => {
const { parameters, dateTimeProvider, target, service } =
await createTest();
// Anchor test time at noon to keep all `limitPerDay` inserts inside the
// same calendar day — running near midnight (real wall-clock) used to
// make the cooldown travel cross day boundaries and reset the window.
const now = dateTimeProvider.now();
const noon = now.startOf("day").add(12, "hours");
if (noon.diff(now) > 0) {
await dateTimeProvider.travel(noon.diff(now), "milliseconds");
} else {
await dateTimeProvider.travel(
noon.add(1, "day").diff(now),
"milliseconds",
);
}
for (let i = 0; i < parameters.limitPerDay; i++) {
await service.createVerification({ type: "link", target });
await dateTimeProvider.travel(
parameters.verificationCooldown + 1,
"seconds",
);
}
await expect(() =>
service.createVerification({ type: "link", target }),
).rejects.toThrowError(
`Maximum number of verification requests per day reached (${parameters.limitPerDay})`,
);
});
it("should handle token expiration", async ({ expect }) => {
const { parameters, controller, dateTimeProvider, target, service } =
await createTest();
const response = await service.createVerification({ type: "link", target });
const token = response.token;
// Travel past expiration
await dateTimeProvider.travel(parameters.codeExpiration + 1, "seconds");
await expect(() =>
controller.validateVerificationCode({
params: {
type: "link",
},
body: {
target,
token,
},
}),
).rejects.toThrowError("Verification code has expired");
});
it("should return token in response", async ({ expect }) => {
const { parameters, target, service } = await createTest();
const response = await service.createVerification({ type: "link", target });
expect(response.token).toBeTruthy();
expect(response.codeExpiration).toBe(parameters.codeExpiration);
// Verify token is a UUID
expect(response.token).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,
);
});
});