mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-03-27 02:01:02 -04:00
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:
committed by
GitHub
parent
fed489f092
commit
45c098d506
@@ -8,9 +8,7 @@ import {
|
||||
ExamEnvironmentQuestionSet
|
||||
} from '@prisma/client';
|
||||
import { ObjectId } from 'mongodb';
|
||||
// import { defaultUserId } from '../jest.utils';
|
||||
import { examEnvironmentPostExamAttempt } from '../src/exam-environment/schemas';
|
||||
// import { generateExam } from '../src/exam-environment/utils/exam';
|
||||
|
||||
export const oid = () => new ObjectId().toString();
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { expect } from 'vitest';
|
||||
|
||||
export const examChallengeId = '647e22d18acb466c97ccbef8';
|
||||
|
||||
export const examJson = {
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { Config } from 'jest';
|
||||
|
||||
const config: Config = {
|
||||
verbose: true,
|
||||
testRegex: '\\.test\\.ts$',
|
||||
transform: {
|
||||
'^.+\\.ts$': ['ts-jest', { isolatedModules: true }]
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -45,13 +45,14 @@
|
||||
"@types/nodemailer": "6.4.14",
|
||||
"@types/supertest": "2.0.16",
|
||||
"@types/validator": "13.11.2",
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"dotenv-cli": "7.3.0",
|
||||
"jest": "29.7.0",
|
||||
"jsdom": "^26.1.0",
|
||||
"msw": "^2.7.0",
|
||||
"prisma": "5.5.2",
|
||||
"supertest": "6.3.3",
|
||||
"ts-jest": "29.1.2",
|
||||
"tsx": "4.19.1"
|
||||
"tsx": "4.19.1",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18",
|
||||
@@ -71,8 +72,9 @@
|
||||
"clean": "rm -rf dist",
|
||||
"develop": "tsx watch --clear-screen=false src/server.ts",
|
||||
"start": "FREECODECAMP_NODE_ENV=production node dist/server.js",
|
||||
"test": "jest --force-exit",
|
||||
"test-with-logging": "FCC_ENABLE_TEST_LOGGING=true pnpm run test",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:ui": "vitest --ui",
|
||||
"prisma": "dotenv -e ../.env prisma",
|
||||
"postinstall": "prisma generate",
|
||||
"exam-env:generate": "tsx tools/exam-environment/generate/index.ts",
|
||||
|
||||
@@ -80,8 +80,11 @@ ajv.addFormat('objectid', {
|
||||
validate: (str: string) => isObjectID(str)
|
||||
});
|
||||
|
||||
export const buildOptions = {
|
||||
loggerInstance: process.env.NODE_ENV === 'test' ? undefined : getLogger(),
|
||||
export const buildOptions: FastifyHttpOptions<
|
||||
RawServerDefault,
|
||||
FastifyBaseLogger
|
||||
> = {
|
||||
loggerInstance: getLogger(),
|
||||
genReqId: () => randomBytes(8).toString('hex'),
|
||||
disableRequestLogging: true
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { addDays } from 'date-fns';
|
||||
|
||||
import { setupServer, superRequest } from '../../../jest.utils';
|
||||
import { setupServer, superRequest } from '../../../vitest.utils';
|
||||
import { getNowUsCentral, getUtcMidnight } from '../utils/helpers';
|
||||
|
||||
function dateToDateParam(date: Date): string {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { defaultUserEmail, setupServer } from '../../jest.utils';
|
||||
import { describe, it, expect, beforeEach, afterAll } from 'vitest';
|
||||
import { defaultUserEmail, setupServer } from '../../vitest.utils';
|
||||
import { createUserInput } from '../utils/create-user';
|
||||
|
||||
describe('prisma client extensions', () => {
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
beforeAll,
|
||||
afterAll,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
vi
|
||||
} from 'vitest';
|
||||
import { ExamEnvironmentExamModerationStatus } from '@prisma/client';
|
||||
import { Static } from '@fastify/type-provider-typebox';
|
||||
import jwt from 'jsonwebtoken';
|
||||
@@ -7,7 +17,7 @@ import {
|
||||
defaultUserId,
|
||||
devLogin,
|
||||
setupServer
|
||||
} from '../../../jest.utils';
|
||||
} from '../../../vitest.utils';
|
||||
import {
|
||||
examEnvironmentPostExamAttempt,
|
||||
examEnvironmentPostExamGeneratedExam
|
||||
@@ -16,10 +26,10 @@ import * as mock from '../../../__mocks__/exam-environment-exam';
|
||||
import { constructUserExam } from '../utils/exam-environment';
|
||||
import { JWT_SECRET } from '../../utils/env';
|
||||
|
||||
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'),
|
||||
...actual,
|
||||
FCC_ENABLE_EXAM_ENVIRONMENT: 'true',
|
||||
DEPLOYMENT_ENV: 'org'
|
||||
};
|
||||
@@ -520,7 +530,7 @@ describe('/exam-environment/', () => {
|
||||
});
|
||||
|
||||
it('should unwind (delete) the exam attempt if the user exam cannot be constructed', async () => {
|
||||
const _mockConstructUserExam = jest
|
||||
const _mockConstructUserExam = vi
|
||||
.spyOn(await import('../utils/exam-environment'), 'constructUserExam')
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error('Test error');
|
||||
@@ -550,7 +560,7 @@ describe('/exam-environment/', () => {
|
||||
|
||||
it('should return the user exam with the exam attempt', async () => {
|
||||
// Mock Math.random for `shuffleArray` to be equivalent between `/generated-exam` and `constructUserExam`
|
||||
jest.spyOn(Math, 'random').mockReturnValue(0.123456789);
|
||||
vi.spyOn(Math, 'random').mockReturnValue(0.123456789);
|
||||
const body: Static<typeof examEnvironmentPostExamGeneratedExam.body> = {
|
||||
examId: mock.examId
|
||||
};
|
||||
@@ -863,7 +873,7 @@ describe('/exam-environment/', () => {
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
xit('TODO: (once serialization is serializable) should return 400 if no attempt id is given', async () => {
|
||||
it.skip('TODO: (once serialization is serializable) should return 400 if no attempt id is given', async () => {
|
||||
const res = await superGet('/exam-environment/exam/attempt/').set(
|
||||
'exam-environment-authorization-token',
|
||||
examEnvironmentAuthorizationToken
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
|
||||
import {
|
||||
ExamEnvironmentAnswer,
|
||||
ExamEnvironmentQuestionType
|
||||
@@ -10,7 +11,7 @@ import {
|
||||
oid
|
||||
} from '../../../__mocks__/exam-environment-exam';
|
||||
import * as schemas from '../schemas';
|
||||
import { setupServer } from '../../../jest.utils';
|
||||
import { setupServer } from '../../../vitest.utils';
|
||||
import {
|
||||
checkAttemptAgainstGeneratedExam,
|
||||
checkPrerequisites,
|
||||
@@ -28,9 +29,9 @@ import {
|
||||
// generate a valid exam.
|
||||
// Another option is to call `generateExam` hundreds of times in a loop test :shrug:
|
||||
describe('Exam Environment mocked Math.random', () => {
|
||||
let spy: jest.SpyInstance;
|
||||
let spy: ReturnType<typeof vi.spyOn>;
|
||||
beforeAll(() => {
|
||||
spy = jest.spyOn(Math, 'random').mockReturnValue(0.123456789);
|
||||
spy = vi.spyOn(Math, 'random').mockReturnValue(0.123456789);
|
||||
});
|
||||
afterAll(() => {
|
||||
spy.mockRestore();
|
||||
|
||||
@@ -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}$/;
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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'
|
||||
});
|
||||
|
||||
@@ -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: '/'
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { describe, beforeEach, afterEach, it, expect } from 'vitest';
|
||||
import Fastify, { type FastifyInstance } from 'fastify';
|
||||
import accepts from '@fastify/accepts';
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { describe, test, expect, beforeAll, afterEach, vi } from 'vitest';
|
||||
import Fastify, { FastifyInstance } from 'fastify';
|
||||
|
||||
import db from '../../db/prisma';
|
||||
import { createUserInput } from '../../utils/create-user';
|
||||
import { checkCanConnectToDb } from '../../../jest.utils';
|
||||
import { checkCanConnectToDb } from '../../../vitest.utils';
|
||||
import { findOrCreateUser } from './auth-helpers';
|
||||
|
||||
const captureException = jest.fn();
|
||||
const captureException = vi.fn();
|
||||
|
||||
async function setupServer() {
|
||||
const fastify = Fastify();
|
||||
@@ -26,10 +27,10 @@ describe('findOrCreateUser', () => {
|
||||
afterEach(async () => {
|
||||
await fastify.prisma.user.deleteMany({ where: { email } });
|
||||
await fastify.close();
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should send a message to Sentry if there are multiple users with the same email', async () => {
|
||||
test('should send a message to Sentry if there are multiple users with the same email', async () => {
|
||||
const user1 = await fastify.prisma.user.create({
|
||||
data: createUserInput(email)
|
||||
});
|
||||
@@ -47,7 +48,7 @@ describe('findOrCreateUser', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should NOT send a message if there is only one user with the email', async () => {
|
||||
test('should NOT send a message if there is only one user with the email', async () => {
|
||||
await fastify.prisma.user.create({ data: createUserInput(email) });
|
||||
|
||||
await findOrCreateUser(fastify, email);
|
||||
@@ -55,7 +56,7 @@ describe('findOrCreateUser', () => {
|
||||
expect(captureException).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should NOT send a message if there are no users with the email', async () => {
|
||||
test('should NOT send a message if there are no users with the email', async () => {
|
||||
await findOrCreateUser(fastify, email);
|
||||
|
||||
expect(captureException).not.toHaveBeenCalled();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import { getFallbackFullStackDate } from './certificate-utils';
|
||||
|
||||
const fullStackChallenges = [
|
||||
@@ -29,17 +30,17 @@ const fullStackChallenges = [
|
||||
|
||||
describe('helper functions', () => {
|
||||
describe('getFallbackFullStackDate', () => {
|
||||
it('should return the date of the latest completed challenge', () => {
|
||||
test('should return the date of the latest completed challenge', () => {
|
||||
expect(getFallbackFullStackDate(fullStackChallenges, 123)).toBe(
|
||||
1685210952511
|
||||
);
|
||||
});
|
||||
|
||||
it('should fall back to completedDate if no certifications are provided', () => {
|
||||
test('should fall back to completedDate if no certifications are provided', () => {
|
||||
expect(getFallbackFullStackDate([], 123)).toBe(123);
|
||||
});
|
||||
|
||||
it('should fall back to completedDate if none of the certifications have been completed', () => {
|
||||
test('should fall back to completedDate if none of the certifications have been completed', () => {
|
||||
expect(
|
||||
getFallbackFullStackDate([{ completedDate: 567, id: 'abc' }], 123)
|
||||
).toBe(123);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { describe, test, expect, afterEach, vi } from 'vitest';
|
||||
import type {
|
||||
PartiallyCompletedChallenge,
|
||||
CompletedChallenge
|
||||
} from '@prisma/client';
|
||||
|
||||
import { createFetchMock } from '../../../jest.utils';
|
||||
import { createFetchMock } from '../../../vitest.utils';
|
||||
import {
|
||||
canSubmitCodeRoadCertProject,
|
||||
verifyTrophyWithMicrosoft
|
||||
@@ -32,7 +33,7 @@ const completedChallenges: CompletedChallenge[] = [
|
||||
|
||||
describe('Challenge Helpers', () => {
|
||||
describe('canSubmitCodeRoadCertProject', () => {
|
||||
it('returns true if the user has completed the required challenges or partially completed them', () => {
|
||||
test('returns true if the user has completed the required challenges or partially completed them', () => {
|
||||
expect(
|
||||
canSubmitCodeRoadCertProject(id, {
|
||||
partiallyCompletedChallenges,
|
||||
@@ -55,7 +56,7 @@ describe('Challenge Helpers', () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false if the user has not completed the required challenges', () => {
|
||||
test('returns false if the user has not completed the required challenges', () => {
|
||||
expect(
|
||||
canSubmitCodeRoadCertProject(id, {
|
||||
partiallyCompletedChallenges: [],
|
||||
@@ -64,7 +65,7 @@ describe('Challenge Helpers', () => {
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false if the id is undefined', () => {
|
||||
test('returns false if the id is undefined', () => {
|
||||
expect(
|
||||
canSubmitCodeRoadCertProject(undefined, {
|
||||
partiallyCompletedChallenges,
|
||||
@@ -81,11 +82,11 @@ describe('Challenge Helpers', () => {
|
||||
const verifyData = { msUsername, msTrophyId };
|
||||
const achievementsUrl = `https://learn.microsoft.com/api/achievements/user/${userId}`;
|
||||
|
||||
afterEach(() => jest.clearAllMocks());
|
||||
afterEach(() => vi.clearAllMocks());
|
||||
|
||||
test("handles failure to reach Microsoft's profile api", async () => {
|
||||
const notOk = createFetchMock({ ok: false });
|
||||
jest.spyOn(globalThis, 'fetch').mockImplementation(notOk);
|
||||
vi.spyOn(globalThis, 'fetch').mockImplementation(notOk);
|
||||
|
||||
const verification = await verifyTrophyWithMicrosoft(verifyData);
|
||||
|
||||
@@ -101,8 +102,7 @@ describe('Challenge Helpers', () => {
|
||||
test("handles failure to reach Microsoft's achievements api", async () => {
|
||||
const fetchProfile = createFetchMock({ body: { userId } });
|
||||
const fetchAchievements = createFetchMock({ ok: false });
|
||||
jest
|
||||
.spyOn(globalThis, 'fetch')
|
||||
vi.spyOn(globalThis, 'fetch')
|
||||
.mockImplementationOnce(fetchProfile)
|
||||
.mockImplementationOnce(fetchAchievements);
|
||||
|
||||
@@ -117,8 +117,7 @@ describe('Challenge Helpers', () => {
|
||||
test('handles the case where the user has no achievements', async () => {
|
||||
const fetchProfile = createFetchMock({ body: { userId } });
|
||||
const fetchAchievements = createFetchMock({ body: { achievements: [] } });
|
||||
jest
|
||||
.spyOn(globalThis, 'fetch')
|
||||
vi.spyOn(globalThis, 'fetch')
|
||||
.mockImplementationOnce(fetchProfile)
|
||||
.mockImplementationOnce(fetchAchievements);
|
||||
|
||||
@@ -135,8 +134,7 @@ describe('Challenge Helpers', () => {
|
||||
const fetchAchievements = createFetchMock({
|
||||
body: { achievements: [{ typeId: 'fake-id' }] }
|
||||
});
|
||||
jest
|
||||
.spyOn(globalThis, 'fetch')
|
||||
vi.spyOn(globalThis, 'fetch')
|
||||
.mockImplementationOnce(fetchProfile)
|
||||
.mockImplementationOnce(fetchAchievements);
|
||||
|
||||
@@ -156,8 +154,7 @@ describe('Challenge Helpers', () => {
|
||||
const fetchAchievements = createFetchMock({
|
||||
body: { achievements: [{ typeId: msTrophyId }] }
|
||||
});
|
||||
jest
|
||||
.spyOn(globalThis, 'fetch')
|
||||
vi.spyOn(globalThis, 'fetch')
|
||||
.mockImplementationOnce(fetchProfile)
|
||||
.mockImplementationOnce(fetchAchievements);
|
||||
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
import {
|
||||
describe,
|
||||
test,
|
||||
expect,
|
||||
beforeAll,
|
||||
afterEach,
|
||||
beforeEach,
|
||||
vi
|
||||
} from 'vitest';
|
||||
import { Certification } from '../../../../shared/config/certification-settings';
|
||||
import {
|
||||
defaultUserEmail,
|
||||
@@ -5,7 +14,7 @@ import {
|
||||
devLogin,
|
||||
setupServer,
|
||||
superRequest
|
||||
} from '../../../jest.utils';
|
||||
} from '../../../vitest.utils';
|
||||
|
||||
describe('certificate routes', () => {
|
||||
setupServer();
|
||||
@@ -18,7 +27,7 @@ describe('certificate routes', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('PUT /certificate/verify', () => {
|
||||
@@ -80,14 +89,15 @@ describe('certificate routes', () => {
|
||||
// TODO: Revisit this test after deciding if we need/want to fetch the
|
||||
// entire user during authorization or just the user id.
|
||||
test.skip('should return 500 if user not found in db', async () => {
|
||||
jest
|
||||
.spyOn(fastifyTestInstance.prisma.user, 'findUnique')
|
||||
.mockImplementation(
|
||||
() =>
|
||||
Promise.resolve(null) as ReturnType<
|
||||
typeof fastifyTestInstance.prisma.user.findUnique
|
||||
>
|
||||
);
|
||||
vi.spyOn(
|
||||
fastifyTestInstance.prisma.user,
|
||||
'findUnique'
|
||||
).mockImplementation(
|
||||
() =>
|
||||
Promise.resolve(null) as ReturnType<
|
||||
typeof fastifyTestInstance.prisma.user.findUnique
|
||||
>
|
||||
);
|
||||
const response = await superRequest('/certificate/verify', {
|
||||
method: 'PUT',
|
||||
setCookies
|
||||
@@ -203,38 +213,6 @@ describe('certificate routes', () => {
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
||||
test('should return 500 if db update fails', async () => {
|
||||
await fastifyTestInstance.prisma.user.updateMany({
|
||||
where: { email: defaultUserEmail },
|
||||
data: {
|
||||
completedChallenges: [
|
||||
{ id: 'bd7158d8c442eddfaeb5bd18', completedDate: 123456789 },
|
||||
{ id: '587d78af367417b2b2512b03', completedDate: 123456789 },
|
||||
{ id: '587d78af367417b2b2512b04', completedDate: 123456789 },
|
||||
{ id: '587d78b0367417b2b2512b05', completedDate: 123456789 },
|
||||
{ id: 'bd7158d8c242eddfaeb5bd13', completedDate: 123456789 }
|
||||
]
|
||||
}
|
||||
});
|
||||
jest
|
||||
.spyOn(fastifyTestInstance.prisma.user, 'update')
|
||||
.mockImplementation(() => {
|
||||
throw new Error('test');
|
||||
});
|
||||
const response = await superRequest('/certificate/verify', {
|
||||
method: 'PUT',
|
||||
setCookies
|
||||
}).send({
|
||||
certSlug: Certification.RespWebDesign
|
||||
});
|
||||
|
||||
expect(response.body).toStrictEqual({
|
||||
message: 'flash.generic-error',
|
||||
type: 'danger'
|
||||
});
|
||||
expect(response.status).toBe(500);
|
||||
});
|
||||
|
||||
// Note: Email does not actually send (work) in development, but status should still be 200.
|
||||
test('should send the certified email, if all current certifications are met', async () => {
|
||||
await fastifyTestInstance.prisma.user.updateMany({
|
||||
@@ -263,7 +241,7 @@ describe('certificate routes', () => {
|
||||
}
|
||||
});
|
||||
|
||||
const spy = jest.spyOn(fastifyTestInstance, 'sendEmail');
|
||||
const spy = vi.spyOn(fastifyTestInstance, 'sendEmail');
|
||||
|
||||
const response = await superRequest('/certificate/verify', {
|
||||
method: 'PUT',
|
||||
@@ -427,6 +405,43 @@ describe('certificate routes', () => {
|
||||
expect(response.status).toBe(400);
|
||||
}
|
||||
});
|
||||
|
||||
// This has to be the last test since vi.mockRestore replaces the original
|
||||
// function with undefined when restoring a prisma function (for some
|
||||
// reason)
|
||||
test('should return 500 if db update fails', async () => {
|
||||
await fastifyTestInstance.prisma.user.updateMany({
|
||||
where: { email: defaultUserEmail },
|
||||
data: {
|
||||
completedChallenges: [
|
||||
{ id: 'bd7158d8c442eddfaeb5bd18', completedDate: 123456789 },
|
||||
{ id: '587d78af367417b2b2512b03', completedDate: 123456789 },
|
||||
{ id: '587d78af367417b2b2512b04', completedDate: 123456789 },
|
||||
{ id: '587d78b0367417b2b2512b05', completedDate: 123456789 },
|
||||
{ id: 'bd7158d8c242eddfaeb5bd13', completedDate: 123456789 }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
vi.spyOn(fastifyTestInstance.prisma.user, 'update').mockImplementation(
|
||||
() => {
|
||||
throw new Error('test');
|
||||
}
|
||||
);
|
||||
|
||||
const response = await superRequest('/certificate/verify', {
|
||||
method: 'PUT',
|
||||
setCookies
|
||||
}).send({
|
||||
certSlug: Certification.RespWebDesign
|
||||
});
|
||||
|
||||
expect(response.body).toStrictEqual({
|
||||
message: 'flash.generic-error',
|
||||
type: 'danger'
|
||||
});
|
||||
expect(response.status).toBe(500);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,26 @@
|
||||
// Yes, putting this above the imports is a hack to get around the fact that
|
||||
// jest.mock() must be called at the top level of the file.
|
||||
const mockVerifyTrophyWithMicrosoft = jest.fn();
|
||||
import {
|
||||
describe,
|
||||
test,
|
||||
expect,
|
||||
beforeAll,
|
||||
afterEach,
|
||||
beforeEach,
|
||||
afterAll,
|
||||
vi
|
||||
} from 'vitest';
|
||||
|
||||
vi.mock('../helpers/challenge-helpers', async () => {
|
||||
const originalModule = await vi.importActual<
|
||||
typeof import('../helpers/challenge-helpers')
|
||||
>('../helpers/challenge-helpers');
|
||||
|
||||
return {
|
||||
__esModule: true,
|
||||
...originalModule,
|
||||
verifyTrophyWithMicrosoft: vi.fn()
|
||||
};
|
||||
});
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { omit } from 'lodash';
|
||||
@@ -18,7 +38,7 @@ import {
|
||||
defaultUserEmail,
|
||||
createSuperRequest,
|
||||
defaultUsername
|
||||
} from '../../../jest.utils';
|
||||
} from '../../../vitest.utils';
|
||||
import {
|
||||
completedExamChallengeOneCorrect,
|
||||
completedExamChallengeTwoCorrect,
|
||||
@@ -36,22 +56,13 @@ import {
|
||||
} from '../../../__mocks__/exam';
|
||||
import { Answer } from '../../utils/exam-types';
|
||||
import type { getSessionUser } from '../../schemas/user/get-session-user';
|
||||
import { verifyTrophyWithMicrosoft } from '../helpers/challenge-helpers';
|
||||
|
||||
const mockVerifyTrophyWithMicrosoft = vi.mocked(verifyTrophyWithMicrosoft);
|
||||
|
||||
const EXISTING_COMPLETED_DATE = new Date('2024-11-08').getTime();
|
||||
const DATE_NOW = Date.now();
|
||||
|
||||
jest.mock('../helpers/challenge-helpers', () => {
|
||||
const originalModule = jest.requireActual<
|
||||
typeof import('../helpers/challenge-helpers')
|
||||
>('../helpers/challenge-helpers');
|
||||
|
||||
return {
|
||||
__esModule: true,
|
||||
...originalModule,
|
||||
verifyTrophyWithMicrosoft: mockVerifyTrophyWithMicrosoft
|
||||
};
|
||||
});
|
||||
|
||||
const isValidChallengeCompletionErrorMsg = {
|
||||
type: 'error',
|
||||
message: 'That does not appear to be a valid challenge submission.'
|
||||
@@ -275,28 +286,6 @@ describe('challengeRoutes', () => {
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
|
||||
test('Should return an error response if something goes wrong', async () => {
|
||||
jest
|
||||
.spyOn(fastifyTestInstance.prisma.userToken, 'findUnique')
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error('Database error');
|
||||
});
|
||||
const tokenResponse = await superPost('/user/user-token');
|
||||
const token = (tokenResponse.body as { userToken: string }).userToken;
|
||||
|
||||
const response = await superPost('/coderoad-challenge-completed')
|
||||
.set('coderoad-user-token', token)
|
||||
.send({
|
||||
tutorialId: 'freeCodeCamp/learn-celestial-bodies-database:v1.0.0'
|
||||
});
|
||||
|
||||
expect(response.body).toEqual({
|
||||
msg: 'An error occurred trying to submit the challenge',
|
||||
type: 'error'
|
||||
});
|
||||
expect(response.status).toBe(500);
|
||||
});
|
||||
|
||||
test('Should complete project with code 200', async () => {
|
||||
const tokenResponse = await superPost('/user/user-token');
|
||||
expect(tokenResponse.body).toHaveProperty('userToken');
|
||||
@@ -313,17 +302,45 @@ describe('challengeRoutes', () => {
|
||||
const user = await fastifyTestInstance.prisma.user.findFirst({
|
||||
where: { email: 'foo@bar.com' }
|
||||
});
|
||||
|
||||
const projectCompleted = user?.partiallyCompletedChallenges.some(
|
||||
project => {
|
||||
return project.id === '5f1a4ef5d5d6b5ab580fc6ae';
|
||||
}
|
||||
);
|
||||
|
||||
expect(response.body).toEqual({
|
||||
msg: 'Successfully submitted challenge',
|
||||
type: 'success'
|
||||
});
|
||||
expect(projectCompleted).toBe(true);
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
|
||||
// This has to be the last test since vi.mockRestore replaces the original
|
||||
// function with undefined when restoring a prisma function (for some
|
||||
// reason)
|
||||
test('Should return an error response if something goes wrong', async () => {
|
||||
vi.spyOn(
|
||||
fastifyTestInstance.prisma.userToken,
|
||||
'findUnique'
|
||||
).mockImplementationOnce(() => {
|
||||
throw new Error('Database error');
|
||||
});
|
||||
const tokenResponse = await superPost('/user/user-token');
|
||||
const token = (tokenResponse.body as { userToken: string }).userToken;
|
||||
|
||||
const response = await superPost('/coderoad-challenge-completed')
|
||||
.set('coderoad-user-token', token)
|
||||
.send({
|
||||
tutorialId: 'freeCodeCamp/learn-celestial-bodies-database:v1.0.0'
|
||||
});
|
||||
|
||||
expect(response.body).toEqual({
|
||||
msg: 'An error occurred trying to submit the challenge',
|
||||
type: 'error'
|
||||
});
|
||||
expect(response.status).toBe(500);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await fastifyTestInstance.prisma.user.updateMany({
|
||||
where: { email: 'foo@bar.com' },
|
||||
@@ -336,7 +353,7 @@ describe('challengeRoutes', () => {
|
||||
});
|
||||
describe('/project-completed', () => {
|
||||
describe('validation', () => {
|
||||
it('POST rejects requests without ids', async () => {
|
||||
test('POST rejects requests without ids', async () => {
|
||||
const response = await superPost('/project-completed').send({});
|
||||
|
||||
expect(response.body).toStrictEqual(
|
||||
@@ -345,7 +362,7 @@ describe('challengeRoutes', () => {
|
||||
expect(response.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('POST rejects requests without valid ObjectIDs', async () => {
|
||||
test('POST rejects requests without valid ObjectIDs', async () => {
|
||||
const response = await superPost(
|
||||
'/project-completed'
|
||||
// This is a departure from api-server, which does not require a
|
||||
@@ -359,7 +376,7 @@ describe('challengeRoutes', () => {
|
||||
expect(response.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('POST rejects requests with invalid challengeTypes', async () => {
|
||||
test('POST rejects requests with invalid challengeTypes', async () => {
|
||||
const response = await superPost('/project-completed').send({
|
||||
id: id1,
|
||||
challengeType: 'not-a-valid-challenge-type',
|
||||
@@ -378,7 +395,7 @@ describe('challengeRoutes', () => {
|
||||
expect(response.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('POST rejects requests without solutions', async () => {
|
||||
test('POST rejects requests without solutions', async () => {
|
||||
const response = await superPost('/project-completed').send({
|
||||
id: id1,
|
||||
challengeType: 3
|
||||
@@ -392,7 +409,7 @@ describe('challengeRoutes', () => {
|
||||
expect(response.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('POST rejects requests with solutions that are not urls', async () => {
|
||||
test('POST rejects requests with solutions that are not urls', async () => {
|
||||
const response = await superPost('/project-completed').send({
|
||||
id: id1,
|
||||
challengeType: 3,
|
||||
@@ -405,7 +422,7 @@ describe('challengeRoutes', () => {
|
||||
expect(response.statusCode).toBe(403);
|
||||
});
|
||||
|
||||
it('POST rejects backendProject requests without URL githubLinks', async () => {
|
||||
test('POST rejects backendProject requests without URL githubLinks', async () => {
|
||||
const response = await superPost('/project-completed').send({
|
||||
id: id1,
|
||||
challengeType: challengeTypes.backEndProject,
|
||||
@@ -431,7 +448,7 @@ describe('challengeRoutes', () => {
|
||||
expect(response_2.statusCode).toBe(403);
|
||||
});
|
||||
|
||||
it('POST rejects CodeRoad/CodeAlly projects when the user has not completed the required challenges', async () => {
|
||||
test('POST rejects CodeRoad/CodeAlly projects when the user has not completed the required challenges', async () => {
|
||||
const response = await superPost('/project-completed').send({
|
||||
id: id1, // not a codeally challenge id, but does not matter
|
||||
challengeType: 13, // this does matter, however, since there's special logic for that challenge type
|
||||
@@ -455,7 +472,10 @@ describe('challengeRoutes', () => {
|
||||
await fastifyTestInstance.prisma.user.updateMany({
|
||||
where: { email: 'foo@bar.com' },
|
||||
data: {
|
||||
partiallyCompletedChallenges: [{ id: id1, completedDate: 1 }]
|
||||
partiallyCompletedChallenges: [{ id: id1, completedDate: 1 }],
|
||||
completedChallenges: [],
|
||||
savedChallenges: [],
|
||||
progressTimestamps: []
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -472,7 +492,7 @@ describe('challengeRoutes', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('POST accepts CodeRoad/CodeAlly projects when the user has completed the required challenges', async () => {
|
||||
test('POST accepts CodeRoad/CodeAlly projects when the user has completed the required challenges', async () => {
|
||||
const now = Date.now();
|
||||
const response =
|
||||
await superPost('/project-completed').send(codeallyProject);
|
||||
@@ -506,7 +526,7 @@ describe('challengeRoutes', () => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
});
|
||||
|
||||
it('POST accepts backend projects', async () => {
|
||||
test('POST accepts backend projects', async () => {
|
||||
const now = Date.now();
|
||||
|
||||
const response =
|
||||
@@ -541,7 +561,7 @@ describe('challengeRoutes', () => {
|
||||
expect(response.statusCode).toBe(200);
|
||||
});
|
||||
|
||||
it('POST correctly handles multiple requests', async () => {
|
||||
test('POST correctly handles multiple requests', async () => {
|
||||
const resOriginal =
|
||||
await superPost('/project-completed').send(codeallyProject);
|
||||
|
||||
@@ -1392,8 +1412,8 @@ describe('challengeRoutes', () => {
|
||||
// that, the details do not matter, since whatever
|
||||
// verifyTrophyWithMicrosoft returns will be returned by the route.
|
||||
const verifyError = {
|
||||
type: 'error',
|
||||
message: 'flash.ms.profile.err',
|
||||
type: 'error' as const,
|
||||
message: 'flash.ms.profile.err' as const,
|
||||
variables: {
|
||||
msUsername
|
||||
}
|
||||
@@ -1467,7 +1487,7 @@ describe('challengeRoutes', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('POST correctly handles multiple requests', async () => {
|
||||
test('POST correctly handles multiple requests', async () => {
|
||||
mockVerifyTrophyWithMicrosoft.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
type: 'success',
|
||||
@@ -1972,14 +1992,14 @@ describe('challengeRoutes', () => {
|
||||
|
||||
describe('handling', () => {
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers({
|
||||
doNotFake: ['nextTick']
|
||||
vi.useFakeTimers({
|
||||
// toFake: ['Date']
|
||||
});
|
||||
jest.setSystemTime(DATE_NOW);
|
||||
vi.setSystemTime(DATE_NOW);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { describe, test, expect, beforeEach, vi } from 'vitest';
|
||||
import {
|
||||
createSuperRequest,
|
||||
devLogin,
|
||||
setupServer,
|
||||
defaultUserEmail,
|
||||
defaultUserId
|
||||
} from '../../../jest.utils';
|
||||
} from '../../../vitest.utils';
|
||||
import { createUserInput } from '../../utils/create-user';
|
||||
|
||||
const testEWalletEmail = 'baz@bar.com';
|
||||
@@ -74,14 +75,14 @@ const createStripePaymentIntentReqBody = {
|
||||
token: { id: 'tok_123' },
|
||||
...sharedDonationReqBody
|
||||
};
|
||||
const mockSubCreate = jest.fn();
|
||||
const mockAttachPaymentMethod = jest.fn(() =>
|
||||
const mockSubCreate = vi.fn();
|
||||
const mockAttachPaymentMethod = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
id: 'pm_1MqLiJLkdIwHu7ixUEgbFdYF',
|
||||
object: 'payment_method'
|
||||
})
|
||||
);
|
||||
const mockCustomerCreate = jest.fn(() =>
|
||||
const mockCustomerCreate = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
id: testCustomerId,
|
||||
name: 'Jest_User',
|
||||
@@ -105,11 +106,11 @@ const mockSubRetrieveObj = {
|
||||
customer: testCustomerId,
|
||||
status: 'active'
|
||||
};
|
||||
const mockSubRetrieve = jest.fn(() => Promise.resolve(mockSubRetrieveObj));
|
||||
const mockCheckoutSessionCreate = jest.fn(() =>
|
||||
const mockSubRetrieve = vi.fn(() => Promise.resolve(mockSubRetrieveObj));
|
||||
const mockCheckoutSessionCreate = vi.fn(() =>
|
||||
Promise.resolve({ id: 'checkout_session_id' })
|
||||
);
|
||||
const mockCustomerUpdate = jest.fn();
|
||||
const mockCustomerUpdate = vi.fn();
|
||||
const generateMockSubCreate = (status: string) => () =>
|
||||
Promise.resolve({
|
||||
id: testSubscriptionId,
|
||||
@@ -123,27 +124,29 @@ const generateMockSubCreate = (status: string) => () =>
|
||||
const defaultError = () =>
|
||||
Promise.reject(new Error('Stripe encountered an error'));
|
||||
|
||||
jest.mock('stripe', () => {
|
||||
return jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
customers: {
|
||||
create: mockCustomerCreate,
|
||||
update: mockCustomerUpdate
|
||||
},
|
||||
paymentMethods: {
|
||||
attach: mockAttachPaymentMethod
|
||||
},
|
||||
subscriptions: {
|
||||
create: mockSubCreate,
|
||||
retrieve: mockSubRetrieve
|
||||
},
|
||||
checkout: {
|
||||
sessions: {
|
||||
create: mockCheckoutSessionCreate
|
||||
vi.mock('stripe', () => {
|
||||
return {
|
||||
default: vi.fn().mockImplementation(() => {
|
||||
return {
|
||||
customers: {
|
||||
create: mockCustomerCreate,
|
||||
update: mockCustomerUpdate
|
||||
},
|
||||
paymentMethods: {
|
||||
attach: mockAttachPaymentMethod
|
||||
},
|
||||
subscriptions: {
|
||||
create: mockSubCreate,
|
||||
retrieve: mockSubRetrieve
|
||||
},
|
||||
checkout: {
|
||||
sessions: {
|
||||
create: mockCheckoutSessionCreate
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
describe('Donate', () => {
|
||||
@@ -199,7 +202,7 @@ describe('Donate', () => {
|
||||
});
|
||||
|
||||
describe('POST /donate/charge-stripe-card', () => {
|
||||
it('should return 200 and update the user', async () => {
|
||||
test('should return 200 and update the user', async () => {
|
||||
mockSubCreate.mockImplementationOnce(
|
||||
generateMockSubCreate('we only care about specific error cases')
|
||||
);
|
||||
@@ -211,7 +214,7 @@ describe('Donate', () => {
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
|
||||
it('should return 402 with client_secret if subscription status requires source action', async () => {
|
||||
test('should return 402 with client_secret if subscription status requires source action', async () => {
|
||||
mockSubCreate.mockImplementationOnce(
|
||||
generateMockSubCreate('requires_source_action')
|
||||
);
|
||||
@@ -229,7 +232,7 @@ describe('Donate', () => {
|
||||
expect(response.status).toBe(402);
|
||||
});
|
||||
|
||||
it('should return 402 if subscription status requires source', async () => {
|
||||
test('should return 402 if subscription status requires source', async () => {
|
||||
mockSubCreate.mockImplementationOnce(
|
||||
generateMockSubCreate('requires_source')
|
||||
);
|
||||
@@ -246,7 +249,7 @@ describe('Donate', () => {
|
||||
expect(response.status).toBe(402);
|
||||
});
|
||||
|
||||
it('should return 400 if the user is already donating', async () => {
|
||||
test('should return 400 if the user is already donating', async () => {
|
||||
mockSubCreate.mockImplementationOnce(
|
||||
generateMockSubCreate('still does not matter')
|
||||
);
|
||||
@@ -269,7 +272,7 @@ describe('Donate', () => {
|
||||
expect(failResponse.status).toBe(400);
|
||||
});
|
||||
|
||||
it('should return 403 if the user has no email', async () => {
|
||||
test('should return 403 if the user has no email', async () => {
|
||||
await fastifyTestInstance.prisma.user.updateMany({
|
||||
where: { email: userWithProgress.email },
|
||||
data: { email: null }
|
||||
@@ -286,7 +289,7 @@ describe('Donate', () => {
|
||||
expect(response.status).toBe(403);
|
||||
});
|
||||
|
||||
it('should return 500 if Stripe encountes an error', async () => {
|
||||
test('should return 500 if Stripe encountes an error', async () => {
|
||||
mockSubCreate.mockImplementationOnce(defaultError);
|
||||
const response = await superPost('/donate/charge-stripe-card').send(
|
||||
chargeStripeCardReqBody
|
||||
@@ -298,7 +301,7 @@ describe('Donate', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 400 if user has not completed challenges', async () => {
|
||||
test('should return 400 if user has not completed challenges', async () => {
|
||||
await fastifyTestInstance.prisma.user.updateMany({
|
||||
where: { email: userWithProgress.email },
|
||||
data: userWithoutProgress
|
||||
@@ -318,7 +321,7 @@ describe('Donate', () => {
|
||||
});
|
||||
|
||||
describe('POST /donate/add-donation', () => {
|
||||
it('should return 200 and update the user', async () => {
|
||||
test('should return 200 and update the user', async () => {
|
||||
const response = await superPost('/donate/add-donation').send({
|
||||
anything: true,
|
||||
itIs: 'ignored'
|
||||
@@ -333,7 +336,7 @@ describe('Donate', () => {
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
|
||||
it('should return 400 if the user is already donating', async () => {
|
||||
test('should return 400 if the user is already donating', async () => {
|
||||
const successResponse = await superPost('/donate/add-donation').send(
|
||||
{}
|
||||
);
|
||||
@@ -344,7 +347,7 @@ describe('Donate', () => {
|
||||
});
|
||||
|
||||
describe('PUT /donate/update-stripe-card', () => {
|
||||
it('should return 200 and return session id', async () => {
|
||||
test('should return 200 and return session id', async () => {
|
||||
await fastifyTestInstance.prisma.donation.create({
|
||||
data: donationMock
|
||||
});
|
||||
@@ -366,7 +369,7 @@ describe('Donate', () => {
|
||||
expect(response.body).toEqual({ sessionId: 'checkout_session_id' });
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
it('should return 500 if there is no donation record', async () => {
|
||||
test('should return 500 if there is no donation record', async () => {
|
||||
const response = await superPut('/donate/update-stripe-card').send({});
|
||||
expect(response.body).toEqual({
|
||||
message: 'flash.generic-error',
|
||||
@@ -377,7 +380,7 @@ describe('Donate', () => {
|
||||
});
|
||||
|
||||
describe('POST /donate/create-stripe-payment-intent', () => {
|
||||
it('should return 200 and call stripe api properly', async () => {
|
||||
test('should return 200 and call stripe api properly', async () => {
|
||||
mockSubCreate.mockImplementationOnce(
|
||||
generateMockSubCreate('no-errors')
|
||||
);
|
||||
@@ -391,7 +394,7 @@ describe('Donate', () => {
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
|
||||
it('should return 400 when email format is wrong', async () => {
|
||||
test('should return 400 when email format is wrong', async () => {
|
||||
const response = await superPost(
|
||||
'/donate/create-stripe-payment-intent'
|
||||
).send({
|
||||
@@ -404,7 +407,7 @@ describe('Donate', () => {
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
||||
it('should return 400 if amount is incorrect', async () => {
|
||||
test('should return 400 if amount is incorrect', async () => {
|
||||
const response = await superPost(
|
||||
'/donate/create-stripe-payment-intent'
|
||||
).send({
|
||||
@@ -417,7 +420,7 @@ describe('Donate', () => {
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
||||
it('should return 500 if Stripe encounters an error', async () => {
|
||||
test('should return 500 if Stripe encounters an error', async () => {
|
||||
mockSubCreate.mockImplementationOnce(defaultError);
|
||||
const response = await superPost(
|
||||
'/donate/create-stripe-payment-intent'
|
||||
@@ -430,7 +433,7 @@ describe('Donate', () => {
|
||||
});
|
||||
|
||||
describe('POST /donate/charge-stripe', () => {
|
||||
it('should return 200 and call stripe api properly', async () => {
|
||||
test('should return 200 and call stripe api properly', async () => {
|
||||
mockSubCreate.mockImplementationOnce(
|
||||
generateMockSubCreate('no-errors')
|
||||
);
|
||||
@@ -442,7 +445,7 @@ describe('Donate', () => {
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
|
||||
it('should return 500 when if product id is wrong', async () => {
|
||||
test('should return 500 when if product id is wrong', async () => {
|
||||
mockSubRetrieve.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
...mockSubRetrieveObj,
|
||||
@@ -469,7 +472,7 @@ describe('Donate', () => {
|
||||
expect(response.status).toBe(500);
|
||||
});
|
||||
|
||||
it('should return 500 if subsciption is not active', async () => {
|
||||
test('should return 500 if subsciption is not active', async () => {
|
||||
mockSubRetrieve.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
...mockSubRetrieveObj,
|
||||
@@ -486,7 +489,7 @@ describe('Donate', () => {
|
||||
expect(response.status).toBe(500);
|
||||
});
|
||||
|
||||
it('should return 500 if timestamp is old', async () => {
|
||||
test('should return 500 if timestamp is old', async () => {
|
||||
mockSubRetrieve.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
...mockSubRetrieveObj,
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import {
|
||||
describe,
|
||||
test,
|
||||
expect,
|
||||
beforeAll,
|
||||
afterEach,
|
||||
beforeEach,
|
||||
vi,
|
||||
MockInstance
|
||||
} from 'vitest';
|
||||
import {
|
||||
devLogin,
|
||||
setupServer,
|
||||
@@ -6,7 +16,7 @@ import {
|
||||
createSuperRequest,
|
||||
defaultUserId,
|
||||
defaultUserEmail
|
||||
} from '../../../jest.utils';
|
||||
} from '../../../vitest.utils';
|
||||
import { formatMessage } from '../../plugins/redirect-with-message';
|
||||
import { createUserInput } from '../../utils/create-user';
|
||||
import { API_LOCATION, HOME_LOCATION } from '../../utils/env';
|
||||
@@ -159,7 +169,7 @@ describe('settingRoutes', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject requests without params', async () => {
|
||||
test('should reject requests without params', async () => {
|
||||
const resNoParams = await superGet('/confirm-email');
|
||||
|
||||
expect(resNoParams.headers.location).toBe(
|
||||
@@ -168,7 +178,7 @@ describe('settingRoutes', () => {
|
||||
expect(resNoParams.status).toBe(302);
|
||||
});
|
||||
|
||||
it('should reject requests which have an invalid token param', async () => {
|
||||
test('should reject requests which have an invalid token param', async () => {
|
||||
const res = await superGet(
|
||||
// token should be 64 characters long
|
||||
`/confirm-email?email=${encodedEmail}&token=tooshort`
|
||||
@@ -180,7 +190,7 @@ describe('settingRoutes', () => {
|
||||
expect(res.status).toBe(302);
|
||||
});
|
||||
|
||||
it('should reject requests which have an invalid email param', async () => {
|
||||
test('should reject requests which have an invalid email param', async () => {
|
||||
const res = await superGet(
|
||||
`/confirm-email?email=${notEmail}&token=${validToken}`
|
||||
);
|
||||
@@ -191,7 +201,7 @@ describe('settingRoutes', () => {
|
||||
expect(res.status).toBe(302);
|
||||
});
|
||||
|
||||
it('should reject requests when the auth token is not in the database', async () => {
|
||||
test('should reject requests when the auth token is not in the database', async () => {
|
||||
const res = await superGet(
|
||||
`/confirm-email?email=${encodedEmail}&token=${validButMissingToken}`
|
||||
);
|
||||
@@ -202,7 +212,7 @@ describe('settingRoutes', () => {
|
||||
expect(res.status).toBe(302);
|
||||
});
|
||||
|
||||
it('should reject requests when the auth token exists, but the user does not', async () => {
|
||||
test('should reject requests when the auth token exists, but the user does not', async () => {
|
||||
const res = await superGet(
|
||||
`/confirm-email?email=${encodedEmail}&token=${validButMissingToken}`
|
||||
);
|
||||
@@ -215,11 +225,11 @@ describe('settingRoutes', () => {
|
||||
|
||||
// TODO(Post-MVP): there's no need to keep the auth token around if,
|
||||
// somehow, the user is missing
|
||||
it.todo(
|
||||
test.todo(
|
||||
'should delete the auth token if there is no user associated with it'
|
||||
);
|
||||
|
||||
it('should reject requests when the email param is different from user.newEmail', async () => {
|
||||
test('should reject requests when the email param is different from user.newEmail', async () => {
|
||||
await fastifyTestInstance.prisma.user.update({
|
||||
where: { id: defaultUserId },
|
||||
data: { newEmail: 'an@oth.er' }
|
||||
@@ -235,7 +245,7 @@ describe('settingRoutes', () => {
|
||||
expect(res.status).toBe(302);
|
||||
});
|
||||
|
||||
it('should reject requests if the auth token has expired', async () => {
|
||||
test('should reject requests if the auth token has expired', async () => {
|
||||
const res = await superGet(
|
||||
`/confirm-email?email=${encodedEmail}&token=${expiredToken}`
|
||||
);
|
||||
@@ -251,7 +261,7 @@ describe('settingRoutes', () => {
|
||||
expect(res.status).toBe(302);
|
||||
});
|
||||
|
||||
it('should update the user email', async () => {
|
||||
test('should update the user email', async () => {
|
||||
const res = await superGet(
|
||||
`/confirm-email?email=${encodedEmail}&token=${validToken}`
|
||||
);
|
||||
@@ -265,7 +275,7 @@ describe('settingRoutes', () => {
|
||||
expect(user.email).toBe(newEmail);
|
||||
});
|
||||
|
||||
it('should clean up the user record', async () => {
|
||||
test('should clean up the user record', async () => {
|
||||
await superGet(
|
||||
`/confirm-email?email=${encodedEmail}&token=${validToken}`
|
||||
);
|
||||
@@ -280,7 +290,7 @@ describe('settingRoutes', () => {
|
||||
expect(user.emailAuthLinkTTL).toBeNull();
|
||||
});
|
||||
|
||||
it('should remove the auth token on success', async () => {
|
||||
test('should remove the auth token on success', async () => {
|
||||
await superGet(
|
||||
`/confirm-email?email=${encodedEmail}&token=${validToken}`
|
||||
);
|
||||
@@ -346,7 +356,7 @@ describe('settingRoutes', () => {
|
||||
});
|
||||
|
||||
describe('/update-my-email', () => {
|
||||
let sendEmailSpy: jest.SpyInstance;
|
||||
let sendEmailSpy: MockInstance;
|
||||
beforeEach(async () => {
|
||||
await fastifyTestInstance.prisma.user.updateMany({
|
||||
where: { email: developerUserEmail },
|
||||
@@ -358,13 +368,13 @@ describe('settingRoutes', () => {
|
||||
}
|
||||
});
|
||||
|
||||
sendEmailSpy = jest
|
||||
sendEmailSpy = vi
|
||||
.spyOn(fastifyTestInstance, 'sendEmail')
|
||||
.mockImplementationOnce(jest.fn());
|
||||
.mockImplementationOnce(vi.fn());
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
await fastifyTestInstance.prisma.authToken.deleteMany({
|
||||
where: { userId: defaultUserId }
|
||||
});
|
||||
@@ -503,16 +513,53 @@ Please wait 5 minutes to resend an authentication link.`
|
||||
});
|
||||
});
|
||||
|
||||
test('PUT creates an auth token record for the requesting user', async () => {
|
||||
// Reset user state to avoid rate limiting from previous tests
|
||||
await fastifyTestInstance.prisma.user.update({
|
||||
where: { id: defaultUserId },
|
||||
data: {
|
||||
emailAuthLinkTTL: null,
|
||||
newEmail: null
|
||||
}
|
||||
});
|
||||
|
||||
const noToken = await fastifyTestInstance.prisma.authToken.findFirst({
|
||||
where: { userId: defaultUserId }
|
||||
});
|
||||
expect(noToken).toBeNull();
|
||||
|
||||
await superPut('/update-my-email').send({
|
||||
email: unusedEmailTwo
|
||||
});
|
||||
|
||||
const token = await fastifyTestInstance.prisma.authToken.findFirst({
|
||||
where: { userId: defaultUserId }
|
||||
});
|
||||
|
||||
expect(token).toEqual({
|
||||
ttl: 15 * 60 * 1000,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
created: expect.any(Date),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
id: expect.any(String),
|
||||
userId: defaultUserId
|
||||
});
|
||||
});
|
||||
|
||||
// This has to be the last test since vi.mockRestore replaces the original
|
||||
// function with undefined when restoring a prisma function (for some
|
||||
// reason)
|
||||
test('PUT sends an email to the new email address', async () => {
|
||||
jest
|
||||
.spyOn(fastifyTestInstance.prisma.authToken, 'create')
|
||||
.mockImplementationOnce(() =>
|
||||
// @ts-expect-error This is a mock implementation, all we're
|
||||
// interested in is the id.
|
||||
Promise.resolve({
|
||||
id: '123'
|
||||
})
|
||||
);
|
||||
vi.spyOn(
|
||||
fastifyTestInstance.prisma.authToken,
|
||||
'create'
|
||||
).mockImplementationOnce(() =>
|
||||
// @ts-expect-error This is a mock implementation, all we're
|
||||
// interested in is the id.
|
||||
Promise.resolve({
|
||||
id: '123'
|
||||
})
|
||||
);
|
||||
await superPut('/update-my-email').send({
|
||||
email: unusedEmailOne
|
||||
});
|
||||
@@ -533,30 +580,6 @@ Happy coding!
|
||||
`
|
||||
});
|
||||
});
|
||||
|
||||
test('PUT creates an auth token record for the requesting user', async () => {
|
||||
const noToken = await fastifyTestInstance.prisma.authToken.findFirst({
|
||||
where: { userId: defaultUserId }
|
||||
});
|
||||
expect(noToken).toBeNull();
|
||||
|
||||
await superPut('/update-my-email').send({
|
||||
email: unusedEmailOne
|
||||
});
|
||||
|
||||
const token = await fastifyTestInstance.prisma.authToken.findFirst({
|
||||
where: { userId: defaultUserId }
|
||||
});
|
||||
|
||||
expect(token).toEqual({
|
||||
ttl: 15 * 60 * 1000,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
created: expect.any(Date),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
id: expect.any(String),
|
||||
userId: defaultUserId
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/update-my-theme', () => {
|
||||
@@ -1051,7 +1074,7 @@ Happy coding!
|
||||
});
|
||||
|
||||
describe('/confirm-email', () => {
|
||||
it('redirects to the HOME_LOCATION with flash message', async () => {
|
||||
test('redirects to the HOME_LOCATION with flash message', async () => {
|
||||
const res = await superRequest('/confirm-email', {
|
||||
method: 'GET'
|
||||
}).set('Referer', 'https://who.knows/');
|
||||
@@ -1104,7 +1127,7 @@ Happy coding!
|
||||
describe('getWaitMessage', () => {
|
||||
const sec = 1000;
|
||||
const min = 60 * 1000;
|
||||
it.each([
|
||||
test.each([
|
||||
{
|
||||
sentAt: new Date(0),
|
||||
now: new Date(0),
|
||||
@@ -1137,10 +1160,10 @@ describe('getWaitMessage', () => {
|
||||
}
|
||||
);
|
||||
|
||||
it('returns null when sentAt is null', () => {
|
||||
test('returns null when sentAt is null', () => {
|
||||
expect(getWaitMessage({ sentAt: null, now: new Date(0) })).toBeNull();
|
||||
});
|
||||
it('uses the current time when now is not provided', () => {
|
||||
test('uses the current time when now is not provided', () => {
|
||||
expect(getWaitMessage({ sentAt: new Date() })).toEqual(
|
||||
'Please wait 5 minutes to resend an authentication link.'
|
||||
);
|
||||
@@ -1148,14 +1171,14 @@ describe('getWaitMessage', () => {
|
||||
});
|
||||
|
||||
describe('validateSocialUrl', () => {
|
||||
it.each(['githubProfile', 'linkedin', 'twitter'] as const)(
|
||||
test.each(['githubProfile', 'linkedin', 'twitter'] as const)(
|
||||
'accepts empty strings for %s',
|
||||
social => {
|
||||
expect(validateSocialUrl('', social)).toBe(true);
|
||||
}
|
||||
);
|
||||
|
||||
it.each([
|
||||
test.each([
|
||||
['githubProfile', 'https://something.com/user'],
|
||||
['linkedin', 'https://www.x.com/in/username'],
|
||||
['twitter', 'https://www.toomanyexes.com/username']
|
||||
@@ -1163,7 +1186,7 @@ describe('validateSocialUrl', () => {
|
||||
expect(validateSocialUrl(url, social)).toBe(false);
|
||||
});
|
||||
|
||||
it.each([
|
||||
test.each([
|
||||
['githubProfile', 'https://something.github.com/user'],
|
||||
['linkedin', 'https://www.linkedin.com/in/username'],
|
||||
['twitter', 'https://twitter.com/username'],
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import {
|
||||
describe,
|
||||
test,
|
||||
expect,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
beforeAll,
|
||||
afterAll,
|
||||
vi,
|
||||
MockInstance
|
||||
} from 'vitest';
|
||||
import jwt, { JwtPayload } from 'jsonwebtoken';
|
||||
import { DailyCodingChallengeLanguage, type Prisma } from '@prisma/client';
|
||||
import { ObjectId } from 'mongodb';
|
||||
@@ -16,7 +27,7 @@ import {
|
||||
createSuperRequest,
|
||||
defaultUsername,
|
||||
resetDefaultUser
|
||||
} from '../../../jest.utils';
|
||||
} from '../../../vitest.utils';
|
||||
import { JWT_SECRET } from '../../utils/env';
|
||||
import {
|
||||
clearEnvExam,
|
||||
@@ -26,13 +37,13 @@ import {
|
||||
} from '../../../__mocks__/exam-environment-exam';
|
||||
import { getMsTranscriptApiUrl } from './user';
|
||||
|
||||
const mockedFetch = jest.fn();
|
||||
jest.spyOn(globalThis, 'fetch').mockImplementation(mockedFetch);
|
||||
const mockedFetch = vi.fn();
|
||||
vi.spyOn(globalThis, 'fetch').mockImplementation(mockedFetch);
|
||||
|
||||
let mockDeploymentEnv = 'dev';
|
||||
jest.mock('../../utils/env', () => {
|
||||
const actualEnv = jest.requireActual('../../utils/env');
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
vi.mock('../../utils/env', async () => {
|
||||
const actualEnv =
|
||||
await vi.importActual<typeof import('../../utils/env')>('../../utils/env');
|
||||
return {
|
||||
...actualEnv,
|
||||
get DEPLOYMENT_ENV() {
|
||||
@@ -514,12 +525,14 @@ describe('userRoutes', () => {
|
||||
});
|
||||
|
||||
test('logs if it is asked to delete a non-existent user', async () => {
|
||||
const spy = jest.spyOn(fastifyTestInstance.log, 'warn');
|
||||
const spy = vi.spyOn(fastifyTestInstance.log, 'warn');
|
||||
|
||||
// Note: this could be flaky since the log is generated if the two
|
||||
// requests are concurrent. If they're sequential the second request
|
||||
// will be not be authed and hence not log anything.
|
||||
const deletePromises = Array.from({ length: 2 }, () =>
|
||||
superPost('/account/delete')
|
||||
);
|
||||
|
||||
await Promise.all(deletePromises);
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
@@ -873,16 +886,16 @@ describe('userRoutes', () => {
|
||||
});
|
||||
|
||||
describe('/user/report-user', () => {
|
||||
let sendEmailSpy: jest.SpyInstance;
|
||||
let sendEmailSpy: MockInstance;
|
||||
beforeEach(() => {
|
||||
sendEmailSpy = jest
|
||||
sendEmailSpy = vi
|
||||
.spyOn(fastifyTestInstance, 'sendEmail')
|
||||
.mockImplementation(jest.fn());
|
||||
.mockImplementation(vi.fn());
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await resetDefaultUser();
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('POST returns 400 for empty username', async () => {
|
||||
@@ -1046,7 +1059,7 @@ Thanks and regards,
|
||||
});
|
||||
});
|
||||
|
||||
it('handles missing transcript urls', async () => {
|
||||
test('handles missing transcript urls', async () => {
|
||||
const response = await superPost('/user/ms-username');
|
||||
|
||||
expect(response.body).toStrictEqual({
|
||||
@@ -1056,7 +1069,7 @@ Thanks and regards,
|
||||
expect(response.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('handles invalid transcript urls', async () => {
|
||||
test('handles invalid transcript urls', async () => {
|
||||
const response = await superPost('/user/ms-username').send({
|
||||
msTranscriptUrl: 'https://www.example.com'
|
||||
});
|
||||
@@ -1068,7 +1081,7 @@ Thanks and regards,
|
||||
expect(response.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('handles the case that MS does not return a username', async () => {
|
||||
test('handles the case that MS does not return a username', async () => {
|
||||
mockedFetch.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
@@ -1088,7 +1101,7 @@ Thanks and regards,
|
||||
expect(response.statusCode).toBe(500);
|
||||
});
|
||||
|
||||
it('handles duplicate Microsoft usernames', async () => {
|
||||
test('handles duplicate Microsoft usernames', async () => {
|
||||
mockedFetch.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
@@ -1120,7 +1133,7 @@ Thanks and regards,
|
||||
expect(response.statusCode).toBe(403);
|
||||
});
|
||||
|
||||
it('returns the username on success', async () => {
|
||||
test('returns the username on success', async () => {
|
||||
const msUsername = 'ms-user';
|
||||
mockedFetch.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
@@ -1142,7 +1155,7 @@ Thanks and regards,
|
||||
expect(response.statusCode).toBe(200);
|
||||
});
|
||||
|
||||
it('creates a record of the linked account', async () => {
|
||||
test('creates a record of the linked account', async () => {
|
||||
const msUsername = 'super-user';
|
||||
mockedFetch.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
@@ -1172,7 +1185,7 @@ Thanks and regards,
|
||||
});
|
||||
});
|
||||
|
||||
it('removes any other accounts linked to the same user', async () => {
|
||||
test('removes any other accounts linked to the same user', async () => {
|
||||
const msUsernameOne = 'super-user';
|
||||
const msUsernameTwo = 'super-user-2';
|
||||
mockedFetch
|
||||
@@ -1219,7 +1232,7 @@ Thanks and regards,
|
||||
expect(linkedAccounts[1]?.msUsername).toBe(msUsernameTwo);
|
||||
});
|
||||
|
||||
it('calls the Microsoft API with the correct url', async () => {
|
||||
test('calls the Microsoft API with the correct url', async () => {
|
||||
const msTranscriptUrl =
|
||||
'https://learn.microsoft.com/en-us/users/mot01/transcript/8u6awert43q1plo';
|
||||
|
||||
@@ -1440,21 +1453,21 @@ describe('Microsoft helpers', () => {
|
||||
const urlWithQueryParams = `${urlWithoutSlash}?foo=bar`;
|
||||
const urlWithQueryParamsAndSlash = `${urlWithSlash}?foo=bar`;
|
||||
|
||||
it('should extract the transcript id from the url', () => {
|
||||
test('should extract the transcript id from the url', () => {
|
||||
expect(getMsTranscriptApiUrl(urlWithoutSlash)).toEqual({
|
||||
error: null,
|
||||
data: expectedUrl
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle trailing slashes', () => {
|
||||
test('should handle trailing slashes', () => {
|
||||
expect(getMsTranscriptApiUrl(urlWithSlash)).toEqual({
|
||||
error: null,
|
||||
data: expectedUrl
|
||||
});
|
||||
});
|
||||
|
||||
it('should ignore query params', () => {
|
||||
test('should ignore query params', () => {
|
||||
expect(getMsTranscriptApiUrl(urlWithQueryParams)).toEqual({
|
||||
error: null,
|
||||
data: expectedUrl
|
||||
@@ -1465,7 +1478,7 @@ describe('Microsoft helpers', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an error for invalid URLs', () => {
|
||||
test('should return an error for invalid URLs', () => {
|
||||
const validBadUrl = 'https://www.example.com/invalid-url';
|
||||
expect(getMsTranscriptApiUrl(validBadUrl)).toEqual({
|
||||
error: expect.any(String),
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import { describe, it, expect, vi, beforeAll, beforeEach } from 'vitest';
|
||||
import {
|
||||
setupServer,
|
||||
superRequest,
|
||||
createSuperRequest
|
||||
} from '../../../jest.utils';
|
||||
} from '../../../vitest.utils';
|
||||
import { AUTH0_DOMAIN } from '../../utils/env';
|
||||
|
||||
const mockedFetch = jest.fn();
|
||||
jest.spyOn(globalThis, 'fetch').mockImplementation(mockedFetch);
|
||||
const mockedFetch = vi.fn();
|
||||
vi.spyOn(globalThis, 'fetch').mockImplementation(mockedFetch);
|
||||
|
||||
const newUserEmail = 'a.n.random@user.com';
|
||||
|
||||
@@ -25,10 +26,11 @@ const mockAuth0ValidEmail = () => ({
|
||||
json: () => ({ email: newUserEmail })
|
||||
});
|
||||
|
||||
jest.mock('../../utils/env', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
vi.mock('../../utils/env', async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import('../../utils/env')>('../../utils/env');
|
||||
return {
|
||||
...jest.requireActual('../../utils/env'),
|
||||
...actual,
|
||||
FCC_ENABLE_DEV_LOGIN_MODE: false
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
test,
|
||||
expect,
|
||||
beforeAll,
|
||||
beforeEach,
|
||||
afterAll,
|
||||
vi
|
||||
} from 'vitest';
|
||||
import {
|
||||
defaultUserEmail,
|
||||
defaultUserId,
|
||||
resetDefaultUser,
|
||||
setupServer,
|
||||
superRequest
|
||||
} from '../../../jest.utils';
|
||||
} from '../../../vitest.utils';
|
||||
import { getFallbackFullStackDate } from '../helpers/certificate-utils';
|
||||
|
||||
const DATE_NOW = Date.now();
|
||||
@@ -16,14 +26,12 @@ describe('certificate routes', () => {
|
||||
beforeAll(async () => {
|
||||
await resetDefaultUser();
|
||||
|
||||
jest.useFakeTimers({
|
||||
doNotFake: ['nextTick']
|
||||
});
|
||||
jest.setSystemTime(DATE_NOW);
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(DATE_NOW);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('GET /certificate/showCert/:username/:certSlug', () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import request from 'supertest';
|
||||
import { describe, test, expect } from 'vitest';
|
||||
|
||||
import { setupServer } from '../../../jest.utils';
|
||||
import { setupServer } from '../../../vitest.utils';
|
||||
import { endpoints } from './deprecated-endpoints';
|
||||
|
||||
describe('Deprecated endpoints', () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { setupServer, superRequest } from '../../../jest.utils';
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import { setupServer, superRequest } from '../../../vitest.utils';
|
||||
|
||||
import { unsubscribeEndpoints } from './deprecated-unsubscribe';
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { setupServer, superRequest } from '../../../jest.utils';
|
||||
import { describe, test, expect, beforeAll, vi } from 'vitest';
|
||||
import { setupServer, superRequest } from '../../../vitest.utils';
|
||||
|
||||
const testEWalletEmail = 'baz@bar.com';
|
||||
const testSubscriptionId = 'sub_test_id';
|
||||
@@ -19,14 +20,14 @@ const createStripePaymentIntentReqBody = {
|
||||
token: { id: 'tok_123' },
|
||||
...sharedDonationReqBody
|
||||
};
|
||||
const mockSubCreate = jest.fn();
|
||||
const mockAttachPaymentMethod = jest.fn(() =>
|
||||
const mockSubCreate = vi.fn();
|
||||
const mockAttachPaymentMethod = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
id: 'pm_1MqLiJLkdIwHu7ixUEgbFdYF',
|
||||
object: 'payment_method'
|
||||
})
|
||||
);
|
||||
const mockCustomerCreate = jest.fn(() =>
|
||||
const mockCustomerCreate = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
id: testCustomerId,
|
||||
name: 'Jest_User',
|
||||
@@ -50,11 +51,11 @@ const mockSubRetrieveObj = {
|
||||
customer: testCustomerId,
|
||||
status: 'active'
|
||||
};
|
||||
const mockSubRetrieve = jest.fn(() => Promise.resolve(mockSubRetrieveObj));
|
||||
const mockCheckoutSessionCreate = jest.fn(() =>
|
||||
const mockSubRetrieve = vi.fn(() => Promise.resolve(mockSubRetrieveObj));
|
||||
const mockCheckoutSessionCreate = vi.fn(() =>
|
||||
Promise.resolve({ id: 'checkout_session_id' })
|
||||
);
|
||||
const mockCustomerUpdate = jest.fn();
|
||||
const mockCustomerUpdate = vi.fn();
|
||||
const generateMockSubCreate = (status: string) => () =>
|
||||
Promise.resolve({
|
||||
id: testSubscriptionId,
|
||||
@@ -65,27 +66,29 @@ const generateMockSubCreate = (status: string) => () =>
|
||||
}
|
||||
}
|
||||
});
|
||||
jest.mock('stripe', () => {
|
||||
return jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
customers: {
|
||||
create: mockCustomerCreate,
|
||||
update: mockCustomerUpdate
|
||||
},
|
||||
paymentMethods: {
|
||||
attach: mockAttachPaymentMethod
|
||||
},
|
||||
subscriptions: {
|
||||
create: mockSubCreate,
|
||||
retrieve: mockSubRetrieve
|
||||
},
|
||||
checkout: {
|
||||
sessions: {
|
||||
create: mockCheckoutSessionCreate
|
||||
vi.mock('stripe', () => {
|
||||
return {
|
||||
default: vi.fn().mockImplementation(() => {
|
||||
return {
|
||||
customers: {
|
||||
create: mockCustomerCreate,
|
||||
update: mockCustomerUpdate
|
||||
},
|
||||
paymentMethods: {
|
||||
attach: mockAttachPaymentMethod
|
||||
},
|
||||
subscriptions: {
|
||||
create: mockSubCreate,
|
||||
retrieve: mockSubRetrieve
|
||||
},
|
||||
checkout: {
|
||||
sessions: {
|
||||
create: mockCheckoutSessionCreate
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
describe('Donate', () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import { setupServer, superRequest } from '../../../jest.utils';
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import { setupServer, superRequest } from '../../../vitest.utils';
|
||||
import { HOME_LOCATION } from '../../utils/env';
|
||||
import { createUserInput } from '../../utils/create-user';
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { devLogin, setupServer, superRequest } from '../../../jest.utils';
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { devLogin, setupServer, superRequest } from '../../../vitest.utils';
|
||||
import { HOME_LOCATION } from '../../utils/env';
|
||||
|
||||
describe('GET /signout', () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { setupServer, superRequest } from '../../../jest.utils';
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import { setupServer, superRequest } from '../../../vitest.utils';
|
||||
import { DEPLOYMENT_VERSION } from '../../utils/env';
|
||||
|
||||
describe('/status', () => {
|
||||
|
||||
@@ -1,17 +1,27 @@
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import _ from 'lodash';
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
test,
|
||||
expect,
|
||||
beforeAll,
|
||||
beforeEach,
|
||||
afterAll,
|
||||
vi
|
||||
} from 'vitest';
|
||||
|
||||
import { createUserInput } from '../../utils/create-user';
|
||||
import {
|
||||
defaultUserEmail,
|
||||
setupServer,
|
||||
createSuperRequest
|
||||
} from '../../../jest.utils';
|
||||
} from '../../../vitest.utils';
|
||||
import { replacePrivateData } from './user';
|
||||
|
||||
const mockedFetch = jest.fn();
|
||||
jest.spyOn(globalThis, 'fetch').mockImplementation(mockedFetch);
|
||||
const mockedFetch = vi.fn();
|
||||
vi.spyOn(globalThis, 'fetch').mockImplementation(mockedFetch);
|
||||
|
||||
// This is used to build a test user.
|
||||
const testUserData: Prisma.userCreateInput = {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import Ajv from 'ajv';
|
||||
import secureSchema from 'ajv/lib/refs/json-schema-secure.json';
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { setupServer, superRequest } from '../jest.utils';
|
||||
import { describe, test, expect, vi } from 'vitest';
|
||||
import { setupServer, superRequest } from '../vitest.utils';
|
||||
import { HOME_LOCATION } from './utils/env';
|
||||
|
||||
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'),
|
||||
...actual,
|
||||
COOKIE_DOMAIN: 'freecodecamp.org'
|
||||
};
|
||||
});
|
||||
|
||||
@@ -40,7 +40,7 @@ function createTestConnectionURL(url: string, dbId?: string) {
|
||||
assert.ok(
|
||||
dbId,
|
||||
`dbId is required for test connection URL. Is this running in a test environment?
|
||||
If so, ensure that the environment variable JEST_WORKER_ID is set.`
|
||||
If so, ensure that the environment variable VITEST_WORKER_ID is set.`
|
||||
);
|
||||
return url.replace(/(.*)(\?.*)/, `$1${dbId}$2`);
|
||||
}
|
||||
@@ -162,7 +162,7 @@ export const MONGOHQ_URL =
|
||||
process.env.NODE_ENV === 'test'
|
||||
? createTestConnectionURL(
|
||||
process.env.MONGOHQ_URL,
|
||||
process.env.JEST_WORKER_ID
|
||||
process.env.VITEST_WORKER_ID
|
||||
)
|
||||
: process.env.MONGOHQ_URL;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Exam, Question } from '@prisma/client';
|
||||
import {
|
||||
examJson,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import { getChallenges } from './get-challenges';
|
||||
import { isObjectID } from './validation';
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import { base64URLEncode, challenge, verifier } from '.';
|
||||
|
||||
describe('utils', () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import {
|
||||
normalizeTwitter,
|
||||
normalizeProfileUI,
|
||||
@@ -9,17 +10,17 @@ import {
|
||||
|
||||
describe('normalize', () => {
|
||||
describe('normalizeTwitter', () => {
|
||||
it('returns the input if it is a url', () => {
|
||||
test('returns the input if it is a url', () => {
|
||||
const url = 'https://twitter.com/a_generic_user';
|
||||
expect(normalizeTwitter(url)).toEqual(url);
|
||||
});
|
||||
it('adds the handle to twitter.com if it is not a url', () => {
|
||||
test('adds the handle to twitter.com if it is not a url', () => {
|
||||
const handle = '@a_generic_user';
|
||||
expect(normalizeTwitter(handle)).toEqual(
|
||||
'https://twitter.com/a_generic_user'
|
||||
);
|
||||
});
|
||||
it('returns undefined if that is the input', () => {
|
||||
test('returns undefined if that is the input', () => {
|
||||
expect(normalizeTwitter('')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -51,16 +52,16 @@ describe('normalize', () => {
|
||||
};
|
||||
|
||||
describe('normalizeProfileUI', () => {
|
||||
it('should return the input if it is not null', () => {
|
||||
test('should return the input if it is not null', () => {
|
||||
expect(normalizeProfileUI(profileUIInput)).toEqual(profileUIInput);
|
||||
});
|
||||
|
||||
it('should return the default profileUI if the input is null', () => {
|
||||
test('should return the default profileUI if the input is null', () => {
|
||||
const input = null;
|
||||
expect(normalizeProfileUI(input)).toEqual(defaultProfileUI);
|
||||
});
|
||||
|
||||
it('should convert all "null" values to "undefined"', () => {
|
||||
test('should convert all "null" values to "undefined"', () => {
|
||||
const input = {
|
||||
isLocked: null,
|
||||
showAbout: false,
|
||||
@@ -89,7 +90,7 @@ describe('normalize', () => {
|
||||
});
|
||||
|
||||
describe('normalizeChallenges', () => {
|
||||
it('should remove null values from the input', () => {
|
||||
test('should remove null values from the input', () => {
|
||||
const completedChallenges = [
|
||||
{
|
||||
id: 'a6b0bb188d873cb2c8729495',
|
||||
@@ -143,7 +144,7 @@ describe('normalize', () => {
|
||||
});
|
||||
|
||||
describe('normalizeFlags', () => {
|
||||
it('should replace nulls with false', () => {
|
||||
test('should replace nulls with false', () => {
|
||||
const flags = {
|
||||
isLocked: null,
|
||||
showAbout: false,
|
||||
@@ -160,14 +161,14 @@ describe('normalize', () => {
|
||||
});
|
||||
|
||||
describe('normalizeDate', () => {
|
||||
it('should return the date as a number', () => {
|
||||
test('should return the date as a number', () => {
|
||||
expect(normalizeDate(1)).toEqual(1);
|
||||
expect(normalizeDate({ $date: '2023-10-01T00:00:00Z' })).toEqual(
|
||||
1696118400000
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if the date is not in the expected shape', () => {
|
||||
test('should throw an error if the date is not in the expected shape', () => {
|
||||
expect(() => normalizeDate('2023-10-01T00:00:00Z')).toThrow(
|
||||
'Unexpected date value: "2023-10-01T00:00:00Z"'
|
||||
);
|
||||
@@ -178,13 +179,13 @@ describe('normalize', () => {
|
||||
});
|
||||
|
||||
describe('normalizeChallengeType', () => {
|
||||
it('should return the challenge type as a number or null', () => {
|
||||
test('should return the challenge type as a number or null', () => {
|
||||
expect(normalizeChallengeType(10)).toEqual(10);
|
||||
expect(normalizeChallengeType('10')).toEqual(10);
|
||||
expect(normalizeChallengeType(null)).toEqual(null);
|
||||
});
|
||||
|
||||
it('should throw an error if the challenge type is not in the expected shape', () => {
|
||||
test('should throw an error if the challenge type is not in the expected shape', () => {
|
||||
expect(() => normalizeChallengeType('invalid')).toThrow(
|
||||
'Unexpected challengeType value: "invalid"'
|
||||
);
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import { getCalendar, getPoints } from './progress';
|
||||
|
||||
describe('utils/progress', () => {
|
||||
describe('getCalendar', () => {
|
||||
it('should return an empty object if no timestamps are passed', () => {
|
||||
test('should return an empty object if no timestamps are passed', () => {
|
||||
expect(getCalendar([])).toEqual({});
|
||||
expect(getCalendar(null)).toEqual({});
|
||||
});
|
||||
it('should take timestamps and return a calendar object', () => {
|
||||
test('should take timestamps and return a calendar object', () => {
|
||||
const timestamps = [-1111001, 0, 1111000, 1111500, 1113000, 9999999];
|
||||
|
||||
expect(getCalendar(timestamps)).toEqual({
|
||||
@@ -18,7 +19,7 @@ describe('utils/progress', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle null, { timestamp: number } and float entries', () => {
|
||||
test('should handle null, { timestamp: number } and float entries', () => {
|
||||
const timestamps = [null, { timestamp: 1113000 }, 1111000.5];
|
||||
|
||||
expect(getCalendar(timestamps)).toEqual({
|
||||
@@ -29,10 +30,10 @@ describe('utils/progress', () => {
|
||||
});
|
||||
|
||||
describe('getPoints', () => {
|
||||
it('should return 1 if there are no progressTimestamps', () => {
|
||||
test('should return 1 if there are no progressTimestamps', () => {
|
||||
expect(getPoints(null)).toEqual(1);
|
||||
});
|
||||
it('should return then number of progressTimestamps if there are any', () => {
|
||||
test('should return then number of progressTimestamps if there are any', () => {
|
||||
expect(getPoints([0, 1, 2])).toEqual(3);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { describe, test, expect, vi } from 'vitest';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
import {
|
||||
@@ -26,7 +27,7 @@ const defaultObject = {
|
||||
// TODO: tidy this up (the mocking is a bit of a mess)
|
||||
describe('redirection', () => {
|
||||
describe('getReturnTo', () => {
|
||||
it('should extract returnTo from a jwt', () => {
|
||||
test('should extract returnTo from a jwt', () => {
|
||||
expect.assertions(1);
|
||||
|
||||
const encryptedReturnTo = jwt.sign(
|
||||
@@ -41,10 +42,10 @@ describe('redirection', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a default url if the secrets do not match', () => {
|
||||
test('should return a default url if the secrets do not match', () => {
|
||||
const oldLog = console.log;
|
||||
expect.assertions(2);
|
||||
console.log = jest.fn();
|
||||
console.log = vi.fn();
|
||||
const encryptedReturnTo = jwt.sign(
|
||||
{ returnTo: validReturnTo },
|
||||
invalidJWTSecret
|
||||
@@ -56,7 +57,7 @@ describe('redirection', () => {
|
||||
console.log = oldLog;
|
||||
});
|
||||
|
||||
it('should return a default url for unknown origins', () => {
|
||||
test('should return a default url for unknown origins', () => {
|
||||
expect.assertions(1);
|
||||
const encryptedReturnTo = jwt.sign(
|
||||
{ returnTo: invalidReturnTo },
|
||||
@@ -68,18 +69,18 @@ describe('redirection', () => {
|
||||
});
|
||||
});
|
||||
describe('normalizeParams', () => {
|
||||
it('should return a {returnTo, origin, pathPrefix} object', () => {
|
||||
test('should return a {returnTo, origin, pathPrefix} object', () => {
|
||||
expect.assertions(2);
|
||||
const keys = Object.keys(normalizeParams({}));
|
||||
const expectedKeys = ['returnTo', 'origin', 'pathPrefix'];
|
||||
expect(keys.length).toBe(3);
|
||||
expect(keys).toEqual(expect.arrayContaining(expectedKeys));
|
||||
});
|
||||
it('should default to process.env.HOME_LOCATION', () => {
|
||||
test('should default to process.env.HOME_LOCATION', () => {
|
||||
expect.assertions(1);
|
||||
expect(normalizeParams({}, defaultOrigin)).toEqual(defaultObject);
|
||||
});
|
||||
it('should convert an unknown pathPrefix to ""', () => {
|
||||
test('should convert an unknown pathPrefix to ""', () => {
|
||||
expect.assertions(1);
|
||||
const brokenPrefix = {
|
||||
...defaultObject,
|
||||
@@ -89,7 +90,7 @@ describe('redirection', () => {
|
||||
defaultObject
|
||||
);
|
||||
});
|
||||
it('should not change a known pathPrefix', () => {
|
||||
test('should not change a known pathPrefix', () => {
|
||||
expect.assertions(1);
|
||||
const spanishPrefix = {
|
||||
...defaultObject,
|
||||
@@ -103,7 +104,7 @@ describe('redirection', () => {
|
||||
// process.env.HOME_LOCATION/path, but if the origin is wrong something unexpected is
|
||||
// going on. In that case it's probably best to just send them to
|
||||
// process.env.HOME_LOCATION/learn.
|
||||
it('should return default parameters if the origin is unknown', () => {
|
||||
test('should return default parameters if the origin is unknown', () => {
|
||||
expect.assertions(1);
|
||||
const exampleOrigin = {
|
||||
...defaultObject,
|
||||
@@ -114,7 +115,7 @@ describe('redirection', () => {
|
||||
defaultObject
|
||||
);
|
||||
});
|
||||
it('should return default parameters if the returnTo is unknown', () => {
|
||||
test('should return default parameters if the returnTo is unknown', () => {
|
||||
expect.assertions(1);
|
||||
const exampleReturnTo = {
|
||||
...defaultObject,
|
||||
@@ -126,7 +127,7 @@ describe('redirection', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject returnTo without trailing slashes', () => {
|
||||
test('should reject returnTo without trailing slashes', () => {
|
||||
const exampleReturnTo = {
|
||||
...defaultObject,
|
||||
returnTo: 'https://www.freecodecamp.dev'
|
||||
@@ -136,7 +137,7 @@ describe('redirection', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should not modify the returnTo if it is valid', () => {
|
||||
test('should not modify the returnTo if it is valid', () => {
|
||||
const exampleReturnTo = {
|
||||
...defaultObject,
|
||||
returnTo: 'https://www.freecodecamp.dev/'
|
||||
@@ -148,7 +149,7 @@ describe('redirection', () => {
|
||||
});
|
||||
|
||||
describe('getRedirectParams', () => {
|
||||
it('should return origin, pathPrefix and returnTo given valid headers', () => {
|
||||
test('should return origin, pathPrefix and returnTo given valid headers', () => {
|
||||
const req = {
|
||||
headers: {
|
||||
referer: `https://www.freecodecamp.org/espanol/learn/rosetta-code/`
|
||||
@@ -165,7 +166,7 @@ describe('redirection', () => {
|
||||
expect(result).toEqual(expectedReturn);
|
||||
});
|
||||
|
||||
it('should strip off any query parameters from the referer', () => {
|
||||
test('should strip off any query parameters from the referer', () => {
|
||||
const req = {
|
||||
headers: {
|
||||
referer: `https://www.freecodecamp.org/espanol/learn/rosetta-code/?query=param`
|
||||
@@ -182,7 +183,7 @@ describe('redirection', () => {
|
||||
expect(result).toEqual(expectedReturn);
|
||||
});
|
||||
|
||||
it('should returnTo the origin if the referer is missing', () => {
|
||||
test('should returnTo the origin if the referer is missing', () => {
|
||||
const req = {
|
||||
headers: {}
|
||||
};
|
||||
@@ -197,7 +198,7 @@ describe('redirection', () => {
|
||||
expect(result).toEqual(expectedReturn);
|
||||
});
|
||||
|
||||
it('should returnTo the origin if the referrer is invalid', () => {
|
||||
test('should returnTo the origin if the referrer is invalid', () => {
|
||||
const req = {
|
||||
headers: {
|
||||
referer: 'invalid-url'
|
||||
@@ -216,7 +217,7 @@ describe('redirection', () => {
|
||||
});
|
||||
|
||||
describe('getLoginRedirectParams', () => {
|
||||
it('should use the login-returnto cookie if present', () => {
|
||||
test('should use the login-returnto cookie if present', () => {
|
||||
const mockReq = {
|
||||
cookies: {
|
||||
'login-returnto': 'https://www.freecodecamp.org/espanol/learn'
|
||||
@@ -236,25 +237,25 @@ describe('redirection', () => {
|
||||
});
|
||||
|
||||
describe('getPrefixedLandingPath', () => {
|
||||
it('should return the origin when no pathPrefix is provided', () => {
|
||||
test('should return the origin when no pathPrefix is provided', () => {
|
||||
const result = getPrefixedLandingPath(defaultOrigin);
|
||||
expect(result).toEqual(defaultOrigin);
|
||||
});
|
||||
|
||||
it('should append pathPrefix to origin when pathPrefix is provided', () => {
|
||||
test('should append pathPrefix to origin when pathPrefix is provided', () => {
|
||||
const expectedPath = `${defaultOrigin}/learn`;
|
||||
const result = getPrefixedLandingPath(defaultOrigin, 'learn');
|
||||
expect(result).toEqual(expectedPath);
|
||||
});
|
||||
|
||||
it('should handle empty origin', () => {
|
||||
test('should handle empty origin', () => {
|
||||
const pathPrefix = 'learn';
|
||||
const expectedPath = '/learn';
|
||||
const result = getPrefixedLandingPath('', pathPrefix);
|
||||
expect(result).toEqual(expectedPath);
|
||||
});
|
||||
|
||||
it('should handle empty pathPrefix', () => {
|
||||
test('should handle empty pathPrefix', () => {
|
||||
const result = getPrefixedLandingPath(defaultOrigin, '');
|
||||
expect(result).toEqual(defaultOrigin);
|
||||
});
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
jest.useFakeTimers();
|
||||
import { describe, test, expect, vi } from 'vitest';
|
||||
|
||||
vi.useFakeTimers();
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { createAccessToken, createAuthToken, isExpired } from './tokens';
|
||||
|
||||
describe('createAccessToken', () => {
|
||||
it('creates an object with id, ttl, created and userId', () => {
|
||||
test('creates an object with id, ttl, created and userId', () => {
|
||||
const userId = 'abc';
|
||||
|
||||
const actual = createAccessToken(userId);
|
||||
@@ -18,7 +20,7 @@ describe('createAccessToken', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('sets the ttl, defaulting to 77760000000 ms', () => {
|
||||
test('sets the ttl, defaulting to 77760000000 ms', () => {
|
||||
const userId = 'abc';
|
||||
const ttl = 123;
|
||||
const actual = createAccessToken(userId, ttl);
|
||||
@@ -29,7 +31,7 @@ describe('createAccessToken', () => {
|
||||
});
|
||||
|
||||
describe('createAuthToken', () => {
|
||||
it('creates an object with id, ttl, created and userId', () => {
|
||||
test('creates an object with id, ttl, created and userId', () => {
|
||||
const userId = 'abc';
|
||||
|
||||
const actual = createAuthToken(userId);
|
||||
@@ -44,7 +46,7 @@ describe('createAuthToken', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('sets the ttl, defaulting to 900000 ms', () => {
|
||||
test('sets the ttl, defaulting to 900000 ms', () => {
|
||||
const userId = 'abc';
|
||||
const ttl = 123;
|
||||
const actual = createAuthToken(userId, ttl);
|
||||
@@ -55,23 +57,23 @@ describe('createAuthToken', () => {
|
||||
});
|
||||
|
||||
describe('isExpired', () => {
|
||||
it('returns true if the token expiry date is in the past', () => {
|
||||
test('returns true if the token expiry date is in the past', () => {
|
||||
const token = createAccessToken('abc', 1000);
|
||||
expect(isExpired(token)).toBe(false);
|
||||
jest.advanceTimersByTime(500);
|
||||
vi.advanceTimersByTime(500);
|
||||
expect(isExpired(token)).toBe(false);
|
||||
jest.advanceTimersByTime(500);
|
||||
vi.advanceTimersByTime(500);
|
||||
expect(isExpired(token)).toBe(false);
|
||||
jest.advanceTimersByTime(1);
|
||||
vi.advanceTimersByTime(1);
|
||||
expect(isExpired(token)).toBe(true);
|
||||
});
|
||||
|
||||
it('handles tokens with Date values for created', () => {
|
||||
test('handles tokens with Date values for created', () => {
|
||||
const token = { ...createAccessToken('abc', 2000), created: new Date() };
|
||||
expect(isExpired(token)).toBe(false);
|
||||
jest.advanceTimersByTime(2000);
|
||||
vi.advanceTimersByTime(2000);
|
||||
expect(isExpired(token)).toBe(false);
|
||||
jest.advanceTimersByTime(1);
|
||||
vi.advanceTimersByTime(1);
|
||||
expect(isExpired(token)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
|
||||
import { inLastFiveMinutes } from './validate-donation';
|
||||
|
||||
describe('inLastFiveMinutes', () => {
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should return true if the timestamp is within the last five minutes', () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { isObjectID } from './validation';
|
||||
|
||||
describe('Validation', () => {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"noEmit": true,
|
||||
"noUncheckedIndexedAccess": true
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"skipLibCheck": true /* This should only be necessary while migrating to vitest, once we're done with that migration we can move to ESM and *hopefully* get rid of this */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getCsrfToken, getCookies } from './jest.utils';
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import { getCsrfToken, getCookies } from './vitest.utils';
|
||||
|
||||
const fakeCookies = [
|
||||
'_csrf=123; Path=/; HttpOnly; SameSite=Strict',
|
||||
@@ -1,3 +1,4 @@
|
||||
import { beforeAll, afterAll, expect, vi } from 'vitest';
|
||||
import request from 'supertest';
|
||||
|
||||
import { build, buildOptions } from './src/app';
|
||||
@@ -172,8 +173,7 @@ export function setupServer(): void {
|
||||
let fastify: FastifyTestInstance;
|
||||
beforeAll(async () => {
|
||||
if (process.env.FCC_ENABLE_TEST_LOGGING !== 'true') {
|
||||
// @ts-expect-error Disable logging by unsetting logger
|
||||
buildOptions.logger = undefined;
|
||||
delete buildOptions.loggerInstance;
|
||||
}
|
||||
fastify = await build(buildOptions);
|
||||
await fastify.ready();
|
||||
@@ -216,17 +216,8 @@ export const defaultUserEmail = 'foo@bar.com';
|
||||
export const defaultUsername = 'fcc-test-user';
|
||||
|
||||
export const resetDefaultUser = async (): Promise<void> => {
|
||||
await fastifyTestInstance.prisma.examEnvironmentAuthorizationToken.deleteMany(
|
||||
{
|
||||
where: { userId: defaultUserId }
|
||||
}
|
||||
);
|
||||
await fastifyTestInstance.prisma.user.deleteMany({
|
||||
where: { id: defaultUserId }
|
||||
});
|
||||
|
||||
await fastifyTestInstance.prisma.user.deleteMany({
|
||||
where: { email: defaultUserEmail }
|
||||
where: { OR: [{ id: defaultUserId }, { email: defaultUserEmail }] }
|
||||
});
|
||||
|
||||
await fastifyTestInstance.prisma.user.create({
|
||||
@@ -262,7 +253,7 @@ export async function seedExam(): Promise<void> {
|
||||
}
|
||||
|
||||
export function createFetchMock({ ok = true, body = {} } = {}) {
|
||||
return jest.fn().mockResolvedValue(
|
||||
return vi.fn().mockResolvedValue(
|
||||
Promise.resolve({
|
||||
ok,
|
||||
json: () => Promise.resolve(body)
|
||||
@@ -71,7 +71,7 @@
|
||||
"start": "npm-run-all create:shared -p develop:server serve:client",
|
||||
"test": "NODE_OPTIONS='--max-old-space-size=7168' run-s create:shared build:curriculum build-workers test:*",
|
||||
"test:source": "jest",
|
||||
"test:api": "cd api && jest --force-exit",
|
||||
"test:api": "cd api && pnpm test",
|
||||
"test:curriculum": "cd ./curriculum && pnpm test",
|
||||
"test-curriculum-full-output": "cd ./curriculum && pnpm run test:full-output",
|
||||
"test-client": "jest client",
|
||||
|
||||
1581
pnpm-lock.yaml
generated
1581
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user