feat(api): s/jest/vitest/g (#61863)

Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
Co-authored-by: Shaun Hamilton <shauhami020@gmail.com>
This commit is contained in:
Mrugesh Mohapatra
2025-08-25 22:57:56 +05:30
committed by GitHub
parent fed489f092
commit 45c098d506
58 changed files with 1974 additions and 907 deletions

View File

@@ -1,3 +1,5 @@
import { expect } from 'vitest';
import { nanoidCharSet } from '../../utils/create-user';
const uuidRe = /^[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}$/;

View File

@@ -1,6 +1,14 @@
import {
describe,
test,
expect,
beforeAll,
beforeEach,
afterAll
} from 'vitest';
import Fastify, { FastifyInstance } from 'fastify';
import { checkCanConnectToDb, defaultUserEmail } from '../../jest.utils';
import { checkCanConnectToDb, defaultUserEmail } from '../../vitest.utils';
import { HOME_LOCATION } from '../utils/env';
import { devAuth } from '../plugins/auth-dev';
import prismaPlugin from '../db/prisma';
@@ -36,7 +44,7 @@ describe('dev login', () => {
});
describe('GET /signin', () => {
it('should create an account if one does not exist', async () => {
test('should create an account if one does not exist', async () => {
const before = await fastify.prisma.user.count({});
await fastify.inject({
method: 'GET',
@@ -49,7 +57,7 @@ describe('dev login', () => {
expect(after).toBe(before + 1);
});
it('should populate the user with the correct data', async () => {
test('should populate the user with the correct data', async () => {
await fastify.inject({
method: 'GET',
url: '/signin'
@@ -63,7 +71,7 @@ describe('dev login', () => {
expect(user.username).toBe(user.usernameDisplay);
});
it('should set the jwt_access_token cookie', async () => {
test('should set the jwt_access_token cookie', async () => {
const res = await fastify.inject({
method: 'GET',
url: '/signin'
@@ -78,9 +86,9 @@ describe('dev login', () => {
);
});
it.todo('should create a session');
test.todo('should create a session');
it('should redirect to the Referer (if it is a valid origin)', async () => {
test('should redirect to the Referer (if it is a valid origin)', async () => {
const res = await fastify.inject({
method: 'GET',
url: '/signin',
@@ -95,7 +103,7 @@ describe('dev login', () => {
);
});
it('should redirect to /valid-language/learn when signing in from /valid-language', async () => {
test('should redirect to /valid-language/learn when signing in from /valid-language', async () => {
const res = await fastify.inject({
method: 'GET',
url: '/signin',
@@ -110,7 +118,7 @@ describe('dev login', () => {
);
});
it('should handle referers with trailing slahes', async () => {
test('should handle referers with trailing slahes', async () => {
const res = await fastify.inject({
method: 'GET',
url: '/signin',
@@ -125,7 +133,7 @@ describe('dev login', () => {
);
});
it('should redirect to /learn by default', async () => {
test('should redirect to /learn by default', async () => {
const res = await fastify.inject({
method: 'GET',
url: '/signin'

View File

@@ -1,3 +1,4 @@
import { describe, test, expect, beforeEach, afterEach } from 'vitest';
import Fastify, { FastifyInstance } from 'fastify';
import jwt from 'jsonwebtoken';
@@ -30,7 +31,7 @@ describe('auth', () => {
// We won't need to keep doubly signing the cookie when we migrate the
// authentication, but for the MVP we have to be able to read the cookies
// set by the api-server. So, double signing:
it('should doubly sign the cookie', async () => {
test('should doubly sign the cookie', async () => {
const token = createAccessToken('test-id');
fastify.get('/test', async (req, reply) => {
reply.setAccessTokenCookie(token);
@@ -68,7 +69,7 @@ describe('auth', () => {
fastify.addHook('onRequest', fastify.authorize);
});
it('should deny if the access token is missing', async () => {
test('should deny if the access token is missing', async () => {
expect.assertions(4);
fastify.addHook('onRequest', (req, _reply, done) => {
@@ -89,7 +90,7 @@ describe('auth', () => {
expect(res.statusCode).toEqual(200);
});
it('should deny if the access token is not signed', async () => {
test('should deny if the access token is not signed', async () => {
expect.assertions(4);
fastify.addHook('onRequest', (req, _reply, done) => {
@@ -117,7 +118,7 @@ describe('auth', () => {
expect(res.statusCode).toEqual(200);
});
it('should deny if the access token is invalid', async () => {
test('should deny if the access token is invalid', async () => {
expect.assertions(4);
fastify.addHook('onRequest', (req, _reply, done) => {
@@ -146,7 +147,7 @@ describe('auth', () => {
expect(res.statusCode).toEqual(200);
});
it('should deny if the access token has expired', async () => {
test('should deny if the access token has expired', async () => {
expect.assertions(4);
fastify.addHook('onRequest', (req, _reply, done) => {
@@ -175,7 +176,7 @@ describe('auth', () => {
expect(res.statusCode).toEqual(200);
});
it('should deny if the user is not found', async () => {
test('should deny if the user is not found', async () => {
expect.assertions(4);
fastify.addHook('onRequest', (req, _reply, done) => {
@@ -207,7 +208,7 @@ describe('auth', () => {
expect(res.statusCode).toEqual(200);
});
it('should populate the request with the user if the token is valid', async () => {
test('should populate the request with the user if the token is valid', async () => {
const fakeUser = { id: '123', username: 'test-user' };
// @ts-expect-error prisma isn't defined, since we're not building the
// full application here.
@@ -235,7 +236,7 @@ describe('auth', () => {
});
describe('onRequest Hook', () => {
it('should update the jwt_access_token to httpOnly and secure', async () => {
test('should update the jwt_access_token to httpOnly and secure', async () => {
const rawValue = 'should-not-change';
fastify.get('/test', (req, reply) => {
reply.send({ ok: true });
@@ -260,7 +261,7 @@ describe('auth', () => {
expect(res.statusCode).toBe(200);
});
it('should do nothing if there is no jwt_access_token', async () => {
test('should do nothing if there is no jwt_access_token', async () => {
fastify.get('/test', (req, reply) => {
reply.send({ ok: true });
});

View File

@@ -19,7 +19,7 @@ declare module 'fastify' {
}
interface FastifyInstance {
authorize: (req: FastifyRequest, reply: FastifyReply) => void;
authorize: (req: FastifyRequest, reply: FastifyReply) => Promise<void>;
authorizeExamEnvironmentToken: (
req: FastifyRequest,
reply: FastifyReply
@@ -60,26 +60,26 @@ const auth: FastifyPluginCallback = (fastify, _options, done) => {
const setAccessDenied = (req: FastifyRequest, content: string) =>
(req.accessDeniedMessage = { type: 'info', content });
const handleAuth = async (req: FastifyRequest) => {
const handleAuth = async (req: FastifyRequest): Promise<void> => {
const tokenCookie = req.cookies.jwt_access_token;
if (!tokenCookie) return setAccessDenied(req, TOKEN_REQUIRED);
if (!tokenCookie) return void setAccessDenied(req, TOKEN_REQUIRED);
const unsignedToken = req.unsignCookie(tokenCookie);
if (!unsignedToken.valid) return setAccessDenied(req, TOKEN_REQUIRED);
if (!unsignedToken.valid) return void setAccessDenied(req, TOKEN_REQUIRED);
const jwtAccessToken = unsignedToken.value;
try {
jwt.verify(jwtAccessToken, JWT_SECRET);
} catch {
return setAccessDenied(req, TOKEN_INVALID);
return void setAccessDenied(req, TOKEN_INVALID);
}
const { accessToken } = jwt.decode(jwtAccessToken) as {
accessToken: Token;
};
if (isExpired(accessToken)) return setAccessDenied(req, TOKEN_EXPIRED);
if (isExpired(accessToken)) return void setAccessDenied(req, TOKEN_EXPIRED);
// We're using token.userId since it's possible for the user record to be
// malformed and for prisma to throw while trying to find the user.
fastify.Sentry?.setUser({
@@ -89,7 +89,7 @@ const auth: FastifyPluginCallback = (fastify, _options, done) => {
const user = await fastify.prisma.user.findUnique({
where: { id: accessToken.userId }
});
if (!user) return setAccessDenied(req, TOKEN_INVALID);
if (!user) return void setAccessDenied(req, TOKEN_INVALID);
req.user = user;
};

View File

@@ -1,4 +1,14 @@
const COOKIE_DOMAIN = 'test.com';
import {
describe,
test,
expect,
beforeAll,
afterAll,
beforeEach,
afterEach,
vi,
MockInstance
} from 'vitest';
import Fastify, { FastifyInstance } from 'fastify';
import { createUserInput } from '../utils/create-user';
@@ -11,10 +21,11 @@ import auth from './auth';
import bouncer from './bouncer';
import { newUser } from './__fixtures__/user';
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
jest.mock('../utils/env', () => ({
...jest.requireActual('../utils/env'),
COOKIE_DOMAIN
const COOKIE_DOMAIN = 'test.com';
vi.mock('../utils/env', async importOriginal => ({
...(await importOriginal<typeof import('../utils/env')>()),
COOKIE_DOMAIN: 'test.com'
}));
describe('auth0 plugin', () => {
@@ -36,7 +47,7 @@ describe('auth0 plugin', () => {
});
describe('GET /signin', () => {
it('should redirect to the auth0 login page', async () => {
test('should redirect to the auth0 login page', async () => {
const res = await fastify.inject({
method: 'GET',
url: '/signin'
@@ -48,7 +59,7 @@ describe('auth0 plugin', () => {
expect(res.statusCode).toBe(302);
});
it('sets a login-returnto cookie', async () => {
test('sets a login-returnto cookie', async () => {
const returnTo = 'http://localhost:3000/learn';
const res = await fastify.inject({
method: 'GET',
@@ -71,8 +82,8 @@ describe('auth0 plugin', () => {
describe('GET /auth/auth0/callback', () => {
const email = 'new@user.com';
let getAccessTokenFromAuthorizationCodeFlowSpy: jest.SpyInstance;
let userinfoSpy: jest.SpyInstance;
let getAccessTokenFromAuthorizationCodeFlowSpy: MockInstance;
let userinfoSpy: MockInstance;
const mockAuthSuccess = () => {
getAccessTokenFromAuthorizationCodeFlowSpy.mockResolvedValueOnce({
@@ -82,21 +93,21 @@ describe('auth0 plugin', () => {
};
beforeEach(() => {
getAccessTokenFromAuthorizationCodeFlowSpy = jest.spyOn(
getAccessTokenFromAuthorizationCodeFlowSpy = vi.spyOn(
fastify.auth0OAuth,
'getAccessTokenFromAuthorizationCodeFlow'
);
userinfoSpy = jest.spyOn(fastify.auth0OAuth, 'userinfo');
userinfoSpy = vi.spyOn(fastify.auth0OAuth, 'userinfo');
// @ts-expect-error - Only mocks part of the Sentry object.
fastify.Sentry = { captureException: () => '' };
});
afterEach(async () => {
jest.restoreAllMocks();
vi.restoreAllMocks();
await fastify.prisma.user.deleteMany({ where: { email } });
});
it('should redirect to the client if authentication fails', async () => {
test('should redirect to the client if authentication fails', async () => {
getAccessTokenFromAuthorizationCodeFlowSpy.mockRejectedValueOnce(
'any error'
);
@@ -112,7 +123,7 @@ describe('auth0 plugin', () => {
expect(res.statusCode).toBe(302);
});
it('should redirect to the client if the state is invalid', async () => {
test('should redirect to the client if the state is invalid', async () => {
const res = await fastify.inject({
method: 'GET',
url: '/auth/auth0/callback?state=invalid'
@@ -124,8 +135,8 @@ describe('auth0 plugin', () => {
expect(res.statusCode).toBe(302);
});
it('should log an error if the state is invalid', async () => {
jest.spyOn(fastify.log, 'error');
test('should log an error if the state is invalid', async () => {
vi.spyOn(fastify.log, 'error');
const res = await fastify.inject({
method: 'GET',
url: '/auth/auth0/callback?state=invalid'
@@ -137,8 +148,8 @@ describe('auth0 plugin', () => {
expect(res.statusCode).toBe(302);
});
it('should log expected Auth0 errors', async () => {
jest.spyOn(fastify.log, 'error');
test('should log expected Auth0 errors', async () => {
vi.spyOn(fastify.log, 'error');
const auth0Error = Error('Response Error: 403 Forbidden');
// @ts-expect-error - mocking a hapi/boom error
auth0Error.data = {
@@ -164,7 +175,7 @@ describe('auth0 plugin', () => {
expect(res.statusCode).toBe(302);
});
it('should not create a user if the state is invalid', async () => {
test('should not create a user if the state is invalid', async () => {
await fastify.inject({
method: 'GET',
url: '/auth/auth0/callback?state=invalid'
@@ -173,7 +184,7 @@ describe('auth0 plugin', () => {
expect(await fastify.prisma.user.count()).toBe(0);
});
it('should block requests with "access_denied" error', async () => {
test('should block requests with "access_denied" error', async () => {
const res = await fastify.inject({
method: 'GET',
url: '/auth/auth0/callback?error=access_denied&error_description=Access denied from your location'
@@ -193,7 +204,7 @@ describe('auth0 plugin', () => {
);
});
it('creates a user if the state is valid', async () => {
test('creates a user if the state is valid', async () => {
mockAuthSuccess();
await fastify.inject({
method: 'GET',
@@ -203,7 +214,7 @@ describe('auth0 plugin', () => {
expect(await fastify.prisma.user.count()).toBe(1);
});
it('handles userinfo errors', async () => {
test('handles userinfo errors', async () => {
getAccessTokenFromAuthorizationCodeFlowSpy.mockResolvedValueOnce({
token: 'any token'
});
@@ -224,7 +235,7 @@ describe('auth0 plugin', () => {
expect(await fastify.prisma.user.count()).toBe(0);
});
it('handles invalid userinfo responses', async () => {
test('handles invalid userinfo responses', async () => {
getAccessTokenFromAuthorizationCodeFlowSpy.mockResolvedValueOnce({
token: 'any token'
});
@@ -245,7 +256,7 @@ describe('auth0 plugin', () => {
expect(await fastify.prisma.user.count()).toBe(0);
});
it('redirects with the signin-success message on success', async () => {
test('redirects with the signin-success message on success', async () => {
mockAuthSuccess();
const res = await fastify.inject({
@@ -259,7 +270,7 @@ describe('auth0 plugin', () => {
expect(res.statusCode).toBe(302);
});
it('should set the jwt_access_token cookie', async () => {
test('should set the jwt_access_token cookie', async () => {
mockAuthSuccess();
const res = await fastify.inject({
@@ -272,7 +283,7 @@ describe('auth0 plugin', () => {
);
});
it('should use the login-returnto cookie if present and valid', async () => {
test('should use the login-returnto cookie if present and valid', async () => {
mockAuthSuccess();
await fastify.prisma.user.create({
data: { ...createUserInput(email), acceptedPrivacyTerms: true }
@@ -299,7 +310,7 @@ describe('auth0 plugin', () => {
);
});
it('should redirect home if the login-returnto cookie is invalid', async () => {
test('should redirect home if the login-returnto cookie is invalid', async () => {
mockAuthSuccess();
const returnTo = 'https://www.evilcodecamp.org/espanol/learn';
// /signin sets the cookie
@@ -321,7 +332,7 @@ describe('auth0 plugin', () => {
expect(res.headers.location).toMatch(HOME_LOCATION);
});
it('should redirect to email-sign-up if the user has not acceptedPrivacyTerms', async () => {
test('should redirect to email-sign-up if the user has not acceptedPrivacyTerms', async () => {
mockAuthSuccess();
// Using an italian path to make sure redirection works.
const italianReturnTo = 'https://www.freecodecamp.org/italian/settings';
@@ -341,7 +352,7 @@ describe('auth0 plugin', () => {
);
});
it('should populate the user with the correct data', async () => {
test('should populate the user with the correct data', async () => {
mockAuthSuccess();
await fastify.inject({

View File

@@ -1,6 +1,15 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/require-await */
import {
describe,
test,
expect,
beforeEach,
afterEach,
vi,
MockInstance
} from 'vitest';
import Fastify, { type FastifyInstance } from 'fastify';
import { type user } from '@prisma/client';
import { HOME_LOCATION } from '../utils/env';
import bouncer from './bouncer';
@@ -8,13 +17,13 @@ import auth from './auth';
import cookies from './cookies';
import redirectWithMessage, { formatMessage } from './redirect-with-message';
let authorizeSpy: jest.SpyInstance;
let authorizeSpy: MockInstance<FastifyInstance['authorize']>;
async function setupServer() {
const fastify = Fastify();
await fastify.register(cookies);
await fastify.register(auth);
authorizeSpy = jest.spyOn(fastify, 'authorize');
authorizeSpy = vi.spyOn(fastify, 'authorize');
await fastify.register(redirectWithMessage);
await fastify.register(bouncer);
@@ -40,14 +49,13 @@ describe('bouncer', () => {
fastify.addHook('onRequest', fastify.send401IfNoUser);
});
it('should return 401 if NO user is present', async () => {
test('should return 401 if NO user is present', async () => {
const message = {
type: 'danger',
type: 'info' as const,
content: 'Something undesirable occurred'
};
authorizeSpy.mockImplementationOnce((req, _reply, done) => {
authorizeSpy.mockImplementationOnce(async req => {
req.accessDeniedMessage = message;
done();
});
const res = await fastify.inject({
method: 'GET',
@@ -61,10 +69,9 @@ describe('bouncer', () => {
expect(res.statusCode).toEqual(401);
});
it('should not alter the response if a user is present', async () => {
authorizeSpy.mockImplementationOnce((req, _reply, done) => {
req.user = { id: '123' };
done();
test('should not alter the response if a user is present', async () => {
authorizeSpy.mockImplementationOnce(async req => {
req.user = { id: '123' } as user;
});
const res = await fastify.inject({
@@ -86,14 +93,13 @@ describe('bouncer', () => {
// TODO(Post-MVP): make the redirects consistent between redirectIfNoUser
// and redirectIfSignedIn. Either both should redirect to the referer or
// both should redirect to HOME_LOCATION.
it('should redirect to HOME_LOCATION if NO user is present', async () => {
test('should redirect to HOME_LOCATION if NO user is present', async () => {
const message = {
type: 'danger',
type: 'info' as const,
content: 'At the moment, content is ignored'
};
authorizeSpy.mockImplementationOnce((req, _reply, done) => {
authorizeSpy.mockImplementationOnce(async req => {
req.accessDeniedMessage = message;
done();
});
const res = await fastify.inject({
method: 'GET',
@@ -104,10 +110,9 @@ describe('bouncer', () => {
expect(res.statusCode).toEqual(302);
});
it('should not alter the response if a user is present', async () => {
authorizeSpy.mockImplementationOnce((req, _reply, done) => {
req.user = { id: '123' };
done();
test('should not alter the response if a user is present', async () => {
authorizeSpy.mockImplementationOnce(async req => {
req.user = { id: '123' } as user;
});
const res = await fastify.inject({
@@ -125,10 +130,9 @@ describe('bouncer', () => {
fastify.addHook('onRequest', fastify.redirectIfSignedIn);
});
it('should redirect to the referer if a user is present', async () => {
authorizeSpy.mockImplementationOnce((req, _reply, done) => {
req.user = { id: '123' };
done();
test('should redirect to the referer if a user is present', async () => {
authorizeSpy.mockImplementationOnce(async req => {
req.user = { id: '123' } as user;
});
const res = await fastify.inject({
method: 'GET',
@@ -144,14 +148,13 @@ describe('bouncer', () => {
expect(res.statusCode).toEqual(302);
});
it('should not alter the response if NO user is present', async () => {
test('should not alter the response if NO user is present', async () => {
const message = {
type: 'danger',
type: 'info' as const,
content: 'At the moment, content is ignored'
};
authorizeSpy.mockImplementationOnce((req, _reply, done) => {
authorizeSpy.mockImplementationOnce(async req => {
req.accessDeniedMessage = message;
done();
});
const res = await fastify.inject({
method: 'GET',

View File

@@ -1,13 +1,16 @@
import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest';
import Fastify, { FastifyInstance } from 'fastify';
import cookies, { type CookieSerializeOptions, sign } from './cookies';
import { cookieUpdate } from './cookie-update';
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
jest.mock('../utils/env', () => ({
...jest.requireActual('../utils/env'),
COOKIE_DOMAIN: 'www.example.com',
FREECODECAMP_NODE_ENV: 'not-development'
}));
vi.mock('../utils/env', async importOriginal => {
const actual = await importOriginal<typeof import('../utils/env')>();
return {
...actual,
COOKIE_DOMAIN: 'www.example.com',
FREECODECAMP_NODE_ENV: 'not-development'
};
});
describe('Cookie updates', () => {
let fastify: FastifyInstance;
@@ -36,7 +39,7 @@ describe('Cookie updates', () => {
await fastify.close();
});
it('should not set cookies that are not in the request', async () => {
test('should not set cookies that are not in the request', async () => {
await setup({});
const res = await fastify.inject({
@@ -50,7 +53,7 @@ describe('Cookie updates', () => {
expect(res.headers['set-cookie']).toBeUndefined();
});
it("should update the cookie's attributes without changing the value", async () => {
test("should update the cookie's attributes without changing the value", async () => {
await setup({ sameSite: 'strict' });
const signedCookie = sign('cookie_value');
const encodedCookie = encodeURIComponent(signedCookie);
@@ -70,7 +73,7 @@ describe('Cookie updates', () => {
expect(updatedCookie).toEqual(expect.stringContaining('SameSite=Strict'));
});
it('should unsign the cookie if required', async () => {
test('should unsign the cookie if required', async () => {
await setup({ signed: false });
const signedCookie = sign('cookie_value');
@@ -88,7 +91,7 @@ describe('Cookie updates', () => {
);
});
it('should respect the default cookie config if not overriden', async () => {
test('should respect the default cookie config if not overriden', async () => {
await setup({});
const res = await fastify.inject({

View File

@@ -1,15 +1,18 @@
import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest';
import Fastify, { type FastifyInstance } from 'fastify';
import fastifyCookie from '@fastify/cookie';
import { COOKIE_SECRET } from '../utils/env';
import cookies from './cookies';
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
jest.mock('../utils/env', () => ({
...jest.requireActual('../utils/env'),
COOKIE_DOMAIN: 'www.example.com',
FREECODECAMP_NODE_ENV: 'not-development'
}));
vi.mock('../utils/env', async importOriginal => {
const actual = await importOriginal<typeof import('../utils/env')>();
return {
...actual,
COOKIE_DOMAIN: 'www.example.com',
FREECODECAMP_NODE_ENV: 'not-development'
};
});
describe('cookies', () => {
let fastify: FastifyInstance;
@@ -23,7 +26,7 @@ describe('cookies', () => {
await fastify.close();
});
it('should prefix signed cookies with "s:" (url-encoded)', async () => {
test('should prefix signed cookies with "s:" (url-encoded)', async () => {
fastify.get('/test', async (req, reply) => {
void reply.setCookie('test', 'value', { signed: true });
return { ok: true };
@@ -37,7 +40,7 @@ describe('cookies', () => {
expect(res.headers['set-cookie']).toMatch(/test=s%3Avalue\.\w*/);
});
it('should be able to unsign cookies', async () => {
test('should be able to unsign cookies', async () => {
const signedCookie = `test=s%3A${fastifyCookie.sign('value', COOKIE_SECRET)}`;
fastify.get('/test', (req, reply) => {
void reply.send({ unsigned: req.unsignCookie(req.cookies.test!) });
@@ -56,7 +59,7 @@ describe('cookies', () => {
});
});
it('should reject cookies not prefixed with "s:"', async () => {
test('should reject cookies not prefixed with "s:"', async () => {
const signedCookie = `test=${fastifyCookie.sign('value', COOKIE_SECRET)}`;
fastify.get('/test', (req, reply) => {
void reply.send({ unsigned: req.unsignCookie(req.cookies.test!) });
@@ -75,7 +78,7 @@ describe('cookies', () => {
});
});
it('should have reasonable defaults', async () => {
test('should have reasonable defaults', async () => {
fastify.get('/test', async (req, reply) => {
void reply.setCookie('test', 'value');
return { ok: true };
@@ -103,7 +106,7 @@ describe('cookies', () => {
// TODO(Post-MVP): Clear all cookies rather than just three specific ones?
// Then it should be called something like clearAllCookies.
it('clearOurCookies should clear cookies that we set', async () => {
test('clearOurCookies should clear cookies that we set', async () => {
fastify.get('/test', async (req, reply) => {
void reply.clearOurCookies();
return { ok: true };

View File

@@ -1,3 +1,4 @@
import { describe, test, expect, beforeAll, afterAll, vi } from 'vitest';
import Fastify, { FastifyInstance, LogLevel } from 'fastify';
import cors from './cors';
@@ -21,9 +22,9 @@ describe('cors', () => {
await fastify.close();
});
it('should not log for /status/* routes', async () => {
test('should not log for /status/* routes', async () => {
const logger = fastify.log.child({ req: { url: '/status/ping' } });
const spies = LOG_LEVELS.map(level => jest.spyOn(logger, level));
const spies = LOG_LEVELS.map(level => vi.spyOn(logger, level));
await fastify.inject({
url: '/status/ping'
});
@@ -33,9 +34,9 @@ describe('cors', () => {
});
});
it('should not log if the origin is undefined', async () => {
test('should not log if the origin is undefined', async () => {
const logger = fastify.log.child({ req: { url: '/api/some-endpoint' } });
const spies = LOG_LEVELS.map(level => jest.spyOn(logger, level));
const spies = LOG_LEVELS.map(level => vi.spyOn(logger, level));
await fastify.inject({
url: '/api/some-endpoint'
});

View File

@@ -1,15 +1,18 @@
import { describe, test, expect, beforeEach, vi } from 'vitest';
import Fastify, { type FastifyInstance } from 'fastify';
import { COOKIE_DOMAIN } from '../utils/env';
import cookies from './cookies';
import csrf, { CSRF_COOKIE, CSRF_SECRET_COOKIE } from './csrf';
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
jest.mock('../utils/env', () => ({
...jest.requireActual('../utils/env'),
COOKIE_DOMAIN: 'www.example.com',
FREECODECAMP_NODE_ENV: 'production'
}));
vi.mock('../utils/env', async importOriginal => {
const actual = await importOriginal<typeof import('../utils/env')>();
return {
...actual,
COOKIE_DOMAIN: 'www.example.com',
FREECODECAMP_NODE_ENV: 'production'
};
});
async function setupServer() {
const fastify = Fastify({ logger: true, disableRequestLogging: true });
@@ -29,7 +32,7 @@ describe('CSRF protection', () => {
beforeEach(async () => {
fastify = await setupServer();
});
it('should receive a new CSRF token with the expected properties', async () => {
test('should receive a new CSRF token with the expected properties', async () => {
const response = await fastify.inject({
method: 'GET',
url: '/'
@@ -53,7 +56,7 @@ describe('CSRF protection', () => {
});
});
it('should return 403 if the _csrf secret is missing', async () => {
test('should return 403 if the _csrf secret is missing', async () => {
const response = await fastify.inject({
method: 'GET',
url: '/'
@@ -64,7 +67,7 @@ describe('CSRF protection', () => {
// check it here.
});
it('should return 403 if the csrf_token is invalid', async () => {
test('should return 403 if the csrf_token is invalid', async () => {
const response = await fastify.inject({
method: 'GET',
url: '/',
@@ -76,7 +79,7 @@ describe('CSRF protection', () => {
expect(response.statusCode).toEqual(403);
});
it('should allow the request if the csrf_token is valid', async () => {
test('should allow the request if the csrf_token is valid', async () => {
const csrfResponse = await fastify.inject({
method: 'GET',
url: '/'

View File

@@ -1,22 +1,30 @@
const SENTRY_DSN = 'https://anything@goes/123';
import {
describe,
test,
expect,
beforeEach,
afterEach,
beforeAll,
afterAll,
vi
} from 'vitest';
import Fastify, { FastifyError, type FastifyInstance } from 'fastify';
import accepts from '@fastify/accepts';
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
import '../instrument';
import errorHandling from './error-handling';
import redirectWithMessage, { formatMessage } from './redirect-with-message';
jest.mock('../utils/env', () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
vi.mock('../utils/env', async importOriginal => {
const actual = await importOriginal<typeof import('../utils/env')>();
return {
...jest.requireActual('../utils/env'),
SENTRY_DSN
...actual,
SENTRY_DSN: 'https://anything@goes/123'
};
});
import '../instrument';
import errorHandling from './error-handling';
import redirectWithMessage, { formatMessage } from './redirect-with-message';
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
describe('errorHandling', () => {
@@ -55,10 +63,10 @@ describe('errorHandling', () => {
afterEach(async () => {
await fastify.close();
jest.clearAllMocks();
vi.clearAllMocks();
});
it('should redirect to the referer if the request does not Accept json', async () => {
test('should redirect to the referer if the request does not Accept json', async () => {
const res = await fastify.inject({
method: 'GET',
url: '/test',
@@ -71,7 +79,7 @@ describe('errorHandling', () => {
expect(res.statusCode).toEqual(302);
});
it('should add a generic flash message if it is a server error (i.e. 500+)', async () => {
test('should add a generic flash message if it is a server error (i.e. 500+)', async () => {
const res = await fastify.inject({
method: 'GET',
url: '/test',
@@ -90,7 +98,7 @@ describe('errorHandling', () => {
);
});
it('should return a json response if the request does Accept json', async () => {
test('should return a json response if the request does Accept json', async () => {
const res = await fastify.inject({
method: 'GET',
url: '/test',
@@ -107,7 +115,7 @@ describe('errorHandling', () => {
});
});
it('should redirect if the request prefers text/html to json', async () => {
test('should redirect if the request prefers text/html to json', async () => {
const res = await fastify.inject({
method: 'GET',
url: '/test',
@@ -121,7 +129,7 @@ describe('errorHandling', () => {
expect(res.statusCode).toEqual(302);
});
it('should respect the error status code', async () => {
test('should respect the error status code', async () => {
const res = await fastify.inject({
method: 'GET',
url: '/test-bad-request'
@@ -130,7 +138,7 @@ describe('errorHandling', () => {
expect(res.statusCode).toEqual(400);
});
it('should return the error message if the status is not 500 ', async () => {
test('should return the error message if the status is not 500 ', async () => {
const res = await fastify.inject({
method: 'GET',
url: '/test-bad-request'
@@ -142,7 +150,7 @@ describe('errorHandling', () => {
});
});
it('should convert CSRF errors to a generic error message', async () => {
test('should convert CSRF errors to a generic error message', async () => {
const resToken = await fastify.inject({
method: 'GET',
url: '/test-csrf-token'
@@ -162,8 +170,8 @@ describe('errorHandling', () => {
});
});
it('should call fastify.log.error when an unhandled error occurs', async () => {
const logSpy = jest.spyOn(fastify.log, 'error');
test('should call fastify.log.error when an unhandled error occurs', async () => {
const logSpy = vi.spyOn(fastify.log, 'error');
await fastify.inject({
method: 'GET',
@@ -171,13 +179,15 @@ describe('errorHandling', () => {
});
expect(logSpy).toHaveBeenCalledWith(
Error('a very bad thing happened'),
expect.objectContaining({
message: 'a very bad thing happened'
}),
'Error in request'
);
});
it('should call fastify.log.warn when a bad request error occurs', async () => {
const logSpy = jest.spyOn(fastify.log, 'warn');
test('should call fastify.log.warn when a bad request error occurs', async () => {
const logSpy = vi.spyOn(fastify.log, 'warn');
await fastify.inject({
method: 'GET',
@@ -185,14 +195,16 @@ describe('errorHandling', () => {
});
expect(logSpy).toHaveBeenCalledWith(
Error('a very bad thing happened'),
expect.objectContaining({
message: 'a very bad thing happened'
}),
'CSRF error in request'
);
});
it('should NOT log when a CSRF error is thrown', async () => {
const errorLogSpy = jest.spyOn(fastify.log, 'error');
const warnLogSpy = jest.spyOn(fastify.log, 'warn');
test('should NOT log when a CSRF error is thrown', async () => {
const errorLogSpy = vi.spyOn(fastify.log, 'error');
const warnLogSpy = vi.spyOn(fastify.log, 'warn');
await fastify.inject({
method: 'GET',
@@ -239,7 +251,7 @@ describe('errorHandling', () => {
});
});
it('should capture the error with Sentry', async () => {
test.skip('should capture the error with Sentry', async () => {
const receivedRequest = createRequestListener();
await fastify.inject({
@@ -247,10 +259,10 @@ describe('errorHandling', () => {
url: '/test'
});
expect(await Promise.race([receivedRequest, delay(1000)])).toBe(true);
expect(await Promise.race([receivedRequest, delay(2000)])).toBe(true);
});
it('should NOT capture CSRF token errors with Sentry', async () => {
test('should NOT capture CSRF token errors with Sentry', async () => {
const receivedRequest = createRequestListener();
await fastify.inject({
@@ -261,7 +273,7 @@ describe('errorHandling', () => {
expect(await Promise.race([receivedRequest, delay(200)])).toBeUndefined();
});
it('should NOT capture CSRF secret errors with Sentry', async () => {
test('should NOT capture CSRF secret errors with Sentry', async () => {
const receivedRequest = createRequestListener();
await fastify.inject({
@@ -272,7 +284,7 @@ describe('errorHandling', () => {
expect(await Promise.race([receivedRequest, delay(200)])).toBeUndefined();
});
it('should NOT capture bad requests with Sentry', async () => {
test('should NOT capture bad requests with Sentry', async () => {
const receivedRequest = createRequestListener();
await fastify.inject({

View File

@@ -1,14 +1,17 @@
import { describe, test, expect, beforeAll, afterAll, vi } from 'vitest';
import Fastify, { type FastifyInstance } from 'fastify';
import growthBook from './growth-book';
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
jest.mock('../utils/env', () => ({
...jest.requireActual('../utils/env'),
// We're only interested in the production behaviour
FREECODECAMP_NODE_ENV: 'production'
}));
vi.mock('../utils/env', async importOriginal => {
const actual = await importOriginal<typeof import('../utils/env')>();
return {
...actual,
// We're only interested in the production behaviour
FREECODECAMP_NODE_ENV: 'production'
};
});
const captureException = jest.fn();
const captureException = vi.fn();
describe('growth-book', () => {
let fastify: FastifyInstance;
@@ -22,8 +25,8 @@ describe('growth-book', () => {
await fastify.close();
});
it('should log the error if the GrowthBook initialization fails', async () => {
const spy = jest.spyOn(fastify.log, 'error');
test('should log the error if the GrowthBook initialization fails', async () => {
const spy = vi.spyOn(fastify.log, 'error');
await fastify.register(growthBook, {
apiHost: 'invalid-url',

View File

@@ -1,11 +1,12 @@
import { describe, test, expect, vi } from 'vitest';
import Fastify from 'fastify';
import mailer from './mailer';
describe('mailer', () => {
it('should send an email via the provider', async () => {
test('should send an email via the provider', async () => {
const fastify = Fastify();
const send = jest.fn();
const send = vi.fn();
await fastify.register(mailer, { provider: { send } });
const data = {

View File

@@ -1,3 +1,4 @@
import { describe, beforeEach, afterEach, it, expect } from 'vitest';
import Fastify, { type FastifyInstance } from 'fastify';
import accepts from '@fastify/accepts';

View File

@@ -1,3 +1,4 @@
import { describe, test, expect, beforeEach } from 'vitest';
import Fastify, { FastifyInstance } from 'fastify';
import qs from 'query-string';
@@ -14,7 +15,7 @@ const isString = (value: unknown): value is string => {
};
describe('redirectWithMessage plugin', () => {
it('should decorate reply object with redirectWithMessage method', async () => {
test('should decorate reply object with redirectWithMessage method', async () => {
expect.assertions(3);
const fastify = await setupServer();
@@ -39,7 +40,7 @@ describe('redirectWithMessage plugin', () => {
fastify = await setupServer();
});
it('should redirect to the first argument', async () => {
test('should redirect to the first argument', async () => {
fastify.get('/', (_req, reply) => {
return reply.redirectWithMessage('/target', {
type: 'info',
@@ -55,7 +56,7 @@ describe('redirectWithMessage plugin', () => {
expect(res.statusCode).toEqual(302);
});
it('should convert the second argument into a query string', async () => {
test('should convert the second argument into a query string', async () => {
fastify.get('/', (_req, reply) => {
return reply.redirectWithMessage('/target', {
type: 'info',
@@ -70,7 +71,7 @@ describe('redirectWithMessage plugin', () => {
expect(res.headers.location).toMatch(/^\/target\?messages=info/);
});
it('should encode the message twice when creating the query string', async () => {
test('should encode the message twice when creating the query string', async () => {
const expectedMessage = { danger: ['foo bar'] };
fastify.get('/', (_req, reply) => {