mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-10-24 12:13:31 +08:00
Co-authored-by: Shaun Hamilton <shauhami020@gmail.com>
1463 lines
46 KiB
TypeScript
1463 lines
46 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
import jwt, { JwtPayload } from 'jsonwebtoken';
|
|
import type { Prisma } from '@prisma/client';
|
|
import { ObjectId } from 'mongodb';
|
|
import _ from 'lodash';
|
|
|
|
import { createUserInput } from '../utils/create-user';
|
|
import {
|
|
defaultUserId,
|
|
defaultUserEmail,
|
|
devLogin,
|
|
setupServer,
|
|
superRequest,
|
|
createSuperRequest
|
|
} from '../../jest.utils';
|
|
import { JWT_SECRET } from '../utils/env';
|
|
import { getMsTranscriptApiUrl, replacePrivateData } from './user';
|
|
|
|
const mockedFetch = jest.fn();
|
|
jest.spyOn(globalThis, 'fetch').mockImplementation(mockedFetch);
|
|
|
|
// This is used to build a test user.
|
|
const testUserData: Prisma.userCreateInput = {
|
|
...createUserInput(defaultUserEmail),
|
|
username: 'foobar',
|
|
usernameDisplay: 'Foo Bar',
|
|
progressTimestamps: [1520002973119, 1520440323273],
|
|
completedChallenges: [
|
|
{
|
|
id: 'a6b0bb188d873cb2c8729495',
|
|
completedDate: 1520002973119,
|
|
solution: null,
|
|
challengeType: 5,
|
|
files: [
|
|
{
|
|
contents: 'test',
|
|
ext: 'js',
|
|
key: 'indexjs',
|
|
name: 'test',
|
|
path: 'path-test'
|
|
},
|
|
{
|
|
contents: 'test2',
|
|
ext: 'html',
|
|
key: 'html-test',
|
|
name: 'test2'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'a5229172f011153519423690',
|
|
completedDate: 1520440323273,
|
|
solution: null,
|
|
challengeType: 5,
|
|
files: []
|
|
},
|
|
{
|
|
id: 'a5229172f011153519423692',
|
|
completedDate: 1520440323274,
|
|
githubLink: '',
|
|
challengeType: 5
|
|
}
|
|
],
|
|
partiallyCompletedChallenges: [{ id: '123', completedDate: 123 }],
|
|
completedExams: [],
|
|
githubProfile: 'github.com/foobar',
|
|
website: 'https://www.freecodecamp.org',
|
|
donationEmails: ['an@add.ress'],
|
|
portfolio: [
|
|
{
|
|
description: 'A portfolio',
|
|
id: 'a6b0bb188d873cb2c8729495',
|
|
image: 'https://www.freecodecamp.org/cat.png',
|
|
title: 'A portfolio',
|
|
url: 'https://www.freecodecamp.org'
|
|
}
|
|
],
|
|
savedChallenges: [
|
|
{
|
|
id: 'a6b0bb188d873cb2c8729495',
|
|
lastSavedDate: 123,
|
|
files: [
|
|
{
|
|
contents: 'test-contents',
|
|
ext: 'js',
|
|
history: ['indexjs'],
|
|
key: 'indexjs',
|
|
name: 'test-name'
|
|
}
|
|
]
|
|
}
|
|
],
|
|
yearsTopContributor: ['2018'],
|
|
twitter: '@foobar',
|
|
linkedin: 'linkedin.com/foobar'
|
|
};
|
|
|
|
const minimalUserData: Prisma.userCreateInput = {
|
|
about: 'I am a test user',
|
|
acceptedPrivacyTerms: true,
|
|
email: testUserData.email,
|
|
emailVerified: true,
|
|
externalId: '1234567890',
|
|
isDonating: false,
|
|
picture: 'https://www.freecodecamp.org/cat.png',
|
|
sendQuincyEmail: true,
|
|
username: 'testuser',
|
|
unsubscribeId: '1234567890'
|
|
};
|
|
|
|
const lockedProfileUI = {
|
|
isLocked: true,
|
|
showAbout: false,
|
|
showCerts: false,
|
|
showDonation: false,
|
|
showHeatMap: false,
|
|
showLocation: false,
|
|
showName: false,
|
|
showPoints: false,
|
|
showPortfolio: false,
|
|
showTimeLine: false
|
|
};
|
|
|
|
// These are not part of the schema, but are added to the user object by
|
|
// get-session-user's handler
|
|
const computedProperties = {
|
|
calendar: {},
|
|
completedChallengeCount: 0,
|
|
isEmailVerified: minimalUserData.emailVerified,
|
|
points: 1,
|
|
// This is the default value if profileUI is missing. If individual properties
|
|
// are missing from the db, they will be omitted from the response.
|
|
profileUI: lockedProfileUI
|
|
};
|
|
|
|
// The following appears in get-session-user responses, but not
|
|
// get-public-profile
|
|
const sessionOnlyData = {
|
|
currentChallengeId: testUserData.currentChallengeId,
|
|
email: testUserData.email,
|
|
emailVerified: testUserData.emailVerified,
|
|
isEmailVerified: testUserData.emailVerified,
|
|
sendQuincyEmail: testUserData.sendQuincyEmail,
|
|
theme: testUserData.theme,
|
|
keyboardShortcuts: testUserData.keyboardShortcuts,
|
|
completedChallengeCount: 3,
|
|
acceptedPrivacyTerms: testUserData.acceptedPrivacyTerms
|
|
};
|
|
|
|
const publicUserData = {
|
|
about: testUserData.about,
|
|
calendar: { 1520002973: 1, 1520440323: 1 },
|
|
// testUserData.completedChallenges, with nulls removed
|
|
completedChallenges: [
|
|
{
|
|
id: 'a6b0bb188d873cb2c8729495',
|
|
completedDate: 1520002973119,
|
|
challengeType: 5,
|
|
files: [
|
|
{
|
|
contents: 'test',
|
|
ext: 'js',
|
|
key: 'indexjs',
|
|
name: 'test',
|
|
path: 'path-test'
|
|
},
|
|
{
|
|
contents: 'test2',
|
|
ext: 'html',
|
|
key: 'html-test',
|
|
name: 'test2'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'a5229172f011153519423690',
|
|
completedDate: 1520440323273,
|
|
challengeType: 5,
|
|
files: []
|
|
},
|
|
{
|
|
id: 'a5229172f011153519423692',
|
|
completedDate: 1520440323274,
|
|
githubLink: '',
|
|
challengeType: 5,
|
|
files: []
|
|
}
|
|
],
|
|
completedExams: testUserData.completedExams,
|
|
completedSurveys: [], // TODO: add surveys
|
|
githubProfile: testUserData.githubProfile,
|
|
is2018DataVisCert: testUserData.is2018DataVisCert,
|
|
is2018FullStackCert: testUserData.is2018FullStackCert, // TODO: should this be returned? The client doesn't use it at the moment.
|
|
isApisMicroservicesCert: testUserData.isApisMicroservicesCert,
|
|
isBackEndCert: testUserData.isBackEndCert,
|
|
isCheater: testUserData.isCheater,
|
|
isCollegeAlgebraPyCertV8: testUserData.isCollegeAlgebraPyCertV8,
|
|
isDataAnalysisPyCertV7: testUserData.isDataAnalysisPyCertV7,
|
|
isDataVisCert: testUserData.isDataVisCert,
|
|
isDonating: testUserData.isDonating,
|
|
isFoundationalCSharpCertV8: testUserData.isFoundationalCSharpCertV8,
|
|
isFrontEndCert: testUserData.isFrontEndCert,
|
|
isFrontEndLibsCert: testUserData.isFrontEndLibsCert,
|
|
isFullStackCert: testUserData.isFullStackCert,
|
|
isHonest: testUserData.isHonest,
|
|
isInfosecCertV7: testUserData.isInfosecCertV7,
|
|
isInfosecQaCert: testUserData.isInfosecQaCert,
|
|
isJsAlgoDataStructCert: testUserData.isJsAlgoDataStructCert,
|
|
isJsAlgoDataStructCertV8: testUserData.isJsAlgoDataStructCertV8,
|
|
isMachineLearningPyCertV7: testUserData.isMachineLearningPyCertV7,
|
|
isQaCertV7: testUserData.isQaCertV7,
|
|
isRelationalDatabaseCertV8: testUserData.isRelationalDatabaseCertV8,
|
|
isRespWebDesignCert: testUserData.isRespWebDesignCert,
|
|
isSciCompPyCertV7: testUserData.isSciCompPyCertV7,
|
|
linkedin: testUserData.linkedin,
|
|
location: testUserData.location,
|
|
name: testUserData.name,
|
|
partiallyCompletedChallenges: [{ id: '123', completedDate: 123 }],
|
|
picture: testUserData.picture,
|
|
points: 2,
|
|
portfolio: testUserData.portfolio,
|
|
profileUI: testUserData.profileUI,
|
|
savedChallenges: testUserData.savedChallenges,
|
|
twitter: 'https://twitter.com/foobar',
|
|
username: testUserData.usernameDisplay, // It defaults to usernameDisplay
|
|
website: testUserData.website,
|
|
yearsTopContributor: testUserData.yearsTopContributor
|
|
};
|
|
|
|
// This is (most of) what we expect to get back from the API. The remaining
|
|
// properties are 'id' and 'joinDate', which are generated by the database.
|
|
// We're currently filtering properties with null values, since the old api just
|
|
// would not return those.
|
|
const sessionUserData = {
|
|
...sessionOnlyData,
|
|
...publicUserData
|
|
};
|
|
|
|
const baseProgressData = {
|
|
currentChallengeId: '',
|
|
isRespWebDesignCert: false,
|
|
is2018DataVisCert: false,
|
|
isFrontEndLibsCert: false,
|
|
isJsAlgoDataStructCert: false,
|
|
isApisMicroservicesCert: false,
|
|
isInfosecQaCert: false,
|
|
isQaCertV7: false,
|
|
isInfosecCertV7: false,
|
|
is2018FullStackCert: false,
|
|
isFrontEndCert: false,
|
|
isBackEndCert: false,
|
|
isDataVisCert: false,
|
|
isFullStackCert: false,
|
|
isSciCompPyCertV7: false,
|
|
isDataAnalysisPyCertV7: false,
|
|
isMachineLearningPyCertV7: false,
|
|
isRelationalDatabaseCertV8: false,
|
|
isCollegeAlgebraPyCertV8: false,
|
|
completedChallenges: [],
|
|
completedExams: [],
|
|
savedChallenges: [],
|
|
partiallyCompletedChallenges: [],
|
|
needsModeration: false
|
|
};
|
|
|
|
const modifiedProgressData = {
|
|
...baseProgressData,
|
|
currentChallengeId: 'hello there',
|
|
isRespWebDesignCert: true,
|
|
isJsAlgoDataStructCert: true,
|
|
isRelationalDatabaseCertV8: true,
|
|
needsModeration: true
|
|
};
|
|
|
|
const userTokenId = 'dummy-id';
|
|
const otherUserId = 'aaaaaaaaaaaaaaaaaaaaaaaa';
|
|
|
|
const msUsernameData = [
|
|
{ msUsername: 'foobar', userId: defaultUserId, ttl: 123 },
|
|
{ msUsername: 'foobar2', userId: defaultUserId, ttl: 123 },
|
|
{ msUsername: 'foobar3', userId: otherUserId, ttl: 123 }
|
|
];
|
|
|
|
const tokenData = [
|
|
{ created: new Date(), id: '123', ttl: 1000, userId: defaultUserId },
|
|
{ created: new Date(), id: '456', ttl: 1000, userId: defaultUserId },
|
|
{ created: new Date(), id: '789', ttl: 1000, userId: otherUserId }
|
|
];
|
|
|
|
const mockSurveyResults = {
|
|
title: 'Foundational C# with Microsoft Survey',
|
|
responses: [
|
|
{
|
|
question: 'Please describe your role:',
|
|
response: 'Beginner developer (less than 2 years experience)'
|
|
},
|
|
{
|
|
question:
|
|
'Prior to this course, how experienced were you with .NET and C#?',
|
|
response: 'Novice (no prior experience)'
|
|
}
|
|
]
|
|
};
|
|
|
|
describe('userRoutes', () => {
|
|
setupServer();
|
|
|
|
describe('Authenticated user', () => {
|
|
let superGet: ReturnType<typeof createSuperRequest>;
|
|
let superPost: ReturnType<typeof createSuperRequest>;
|
|
let superDelete: ReturnType<typeof createSuperRequest>;
|
|
|
|
beforeEach(async () => {
|
|
const setCookies = await devLogin();
|
|
superGet = createSuperRequest({ method: 'GET', setCookies });
|
|
superPost = createSuperRequest({ method: 'POST', setCookies });
|
|
superDelete = createSuperRequest({ method: 'DELETE', setCookies });
|
|
});
|
|
|
|
describe('/account/delete', () => {
|
|
afterEach(async () => {
|
|
await fastifyTestInstance.prisma.userToken.deleteMany({
|
|
where: { OR: [{ userId: defaultUserId }, { userId: otherUserId }] }
|
|
});
|
|
await fastifyTestInstance.prisma.msUsername.deleteMany({
|
|
where: { OR: [{ userId: defaultUserId }, { userId: otherUserId }] }
|
|
});
|
|
});
|
|
|
|
test('POST returns 200 status code with empty object', async () => {
|
|
expect(await fastifyTestInstance.prisma.user.count()).toBe(1);
|
|
const response = await superPost('/account/delete');
|
|
const userCount = await fastifyTestInstance.prisma.user.count({
|
|
where: { email: testUserData.email }
|
|
});
|
|
|
|
expect(response.body).toStrictEqual({});
|
|
expect(response.status).toBe(200);
|
|
expect(userCount).toBe(0);
|
|
});
|
|
|
|
test('POST deletes Microsoft usernames associated with the user', async () => {
|
|
await fastifyTestInstance.prisma.msUsername.createMany({
|
|
data: msUsernameData
|
|
});
|
|
|
|
await superPost('/account/delete');
|
|
expect(await fastifyTestInstance.prisma.msUsername.count()).toBe(1);
|
|
});
|
|
|
|
test('POST deletes userTokens associated with the user', async () => {
|
|
await fastifyTestInstance.prisma.userToken.createMany({
|
|
data: tokenData
|
|
});
|
|
|
|
await superPost('/account/delete');
|
|
|
|
const userTokens =
|
|
await fastifyTestInstance.prisma.userToken.findMany();
|
|
expect(userTokens).toHaveLength(1);
|
|
expect(userTokens[0]?.userId).toBe(otherUserId);
|
|
});
|
|
});
|
|
|
|
describe('/account/reset-progress', () => {
|
|
afterEach(async () => {
|
|
await fastifyTestInstance.prisma.userToken.deleteMany({
|
|
where: { OR: [{ userId: defaultUserId }, { userId: otherUserId }] }
|
|
});
|
|
await fastifyTestInstance.prisma.msUsername.deleteMany({
|
|
where: { OR: [{ userId: defaultUserId }, { userId: otherUserId }] }
|
|
});
|
|
});
|
|
test('POST returns 200 status code with empty object', async () => {
|
|
await fastifyTestInstance.prisma.user.updateMany({
|
|
where: { email: testUserData.email },
|
|
data: modifiedProgressData
|
|
});
|
|
|
|
const response = await superPost('/account/reset-progress');
|
|
|
|
const user = await fastifyTestInstance.prisma.user.findFirst({
|
|
where: { email: testUserData.email }
|
|
});
|
|
|
|
expect(response.body).toStrictEqual({});
|
|
expect(response.status).toBe(200);
|
|
|
|
expect(user?.progressTimestamps).toHaveLength(1);
|
|
expect(user).toMatchObject(baseProgressData);
|
|
});
|
|
|
|
test('POST deletes Microsoft usernames associated with the user', async () => {
|
|
await fastifyTestInstance.prisma.msUsername.createMany({
|
|
data: msUsernameData
|
|
});
|
|
|
|
await superPost('/account/reset-progress');
|
|
|
|
expect(await fastifyTestInstance.prisma.msUsername.count()).toBe(1);
|
|
});
|
|
|
|
test('POST deletes userTokens associated with the user', async () => {
|
|
await fastifyTestInstance.prisma.userToken.createMany({
|
|
data: tokenData
|
|
});
|
|
|
|
await superPost('/account/reset-progress');
|
|
|
|
const userTokens =
|
|
await fastifyTestInstance.prisma.userToken.findMany();
|
|
expect(userTokens).toHaveLength(1);
|
|
expect(userTokens[0]?.userId).toBe(otherUserId);
|
|
});
|
|
|
|
test.todo('POST resets the user to the default state');
|
|
});
|
|
|
|
describe('/user/user-token', () => {
|
|
beforeEach(async () => {
|
|
await fastifyTestInstance.prisma.userToken.create({
|
|
data: {
|
|
created: new Date(),
|
|
id: '123',
|
|
ttl: 1000,
|
|
userId: defaultUserId
|
|
}
|
|
});
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await fastifyTestInstance.prisma.userToken.deleteMany({
|
|
where: {
|
|
userId: defaultUserId
|
|
}
|
|
});
|
|
});
|
|
|
|
// TODO(Post-MVP): consider using PUT and updating the logic to upsert
|
|
test('POST success response includes a JWT encoded string', async () => {
|
|
const response = await superPost('/user/user-token');
|
|
|
|
const userToken = response.body.userToken;
|
|
const decodedToken = jwt.decode(userToken);
|
|
|
|
expect(response.body).toStrictEqual({ userToken: expect.any(String) });
|
|
expect(decodedToken).toStrictEqual({
|
|
userToken: expect.stringMatching(/^[a-zA-Z0-9]{64}$/),
|
|
iat: expect.any(Number)
|
|
});
|
|
|
|
expect(() => jwt.verify(userToken, 'wrong-secret')).toThrow();
|
|
expect(() => jwt.verify(userToken, JWT_SECRET)).not.toThrow();
|
|
|
|
// TODO(Post-MVP): consider using 201 for new tokens.
|
|
expect(response.status).toBe(200);
|
|
});
|
|
|
|
test('POST responds with an encoded UserToken id', async () => {
|
|
const response = await superPost('/user/user-token');
|
|
|
|
const decodedToken = jwt.decode(response.body.userToken);
|
|
const userTokenId = (decodedToken as JwtPayload).userToken;
|
|
|
|
// Verify that the token has been created.
|
|
await fastifyTestInstance.prisma.userToken.findUniqueOrThrow({
|
|
where: { id: userTokenId }
|
|
});
|
|
|
|
// TODO(Post-MVP): consider using 201 for new tokens.
|
|
expect(response.status).toBe(200);
|
|
});
|
|
|
|
test('POST deletes old tokens when creating a new one', async () => {
|
|
const response = await superPost('/user/user-token');
|
|
|
|
const decodedToken = jwt.decode(response.body.userToken);
|
|
const userTokenId = (decodedToken as JwtPayload).userToken;
|
|
|
|
// Verify that the token has been created.
|
|
await fastifyTestInstance.prisma.userToken.findUniqueOrThrow({
|
|
where: { id: userTokenId }
|
|
});
|
|
|
|
await superPost('/user/user-token');
|
|
|
|
// Verify that the old token has been deleted.
|
|
expect(
|
|
await fastifyTestInstance.prisma.userToken.findUnique({
|
|
where: { id: userTokenId }
|
|
})
|
|
).toBeNull();
|
|
expect(await fastifyTestInstance.prisma.userToken.count()).toBe(1);
|
|
});
|
|
|
|
test('DELETE returns 200 status with null userToken', async () => {
|
|
const response = await superDelete('/user/user-token');
|
|
|
|
expect(response.body).toStrictEqual({ userToken: null });
|
|
expect(response.status).toBe(200);
|
|
expect(await fastifyTestInstance.prisma.userToken.count()).toBe(0);
|
|
});
|
|
|
|
test('DELETEing a missing userToken returns 404 status with an error message', async () => {
|
|
await superDelete('/user/user-token');
|
|
|
|
const response = await superDelete('/user/user-token');
|
|
|
|
expect(response.body).toStrictEqual({
|
|
type: 'info',
|
|
message: 'userToken not found'
|
|
});
|
|
expect(response.status).toBe(404);
|
|
});
|
|
});
|
|
|
|
describe('/user/get-user-session', () => {
|
|
beforeEach(async () => {
|
|
await fastifyTestInstance.prisma.user.updateMany({
|
|
where: { email: testUserData.email },
|
|
data: testUserData
|
|
});
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await fastifyTestInstance.prisma.userToken.deleteMany({
|
|
where: { id: userTokenId }
|
|
});
|
|
});
|
|
|
|
test('GET rejects with 500 status code if the username is missing', async () => {
|
|
await fastifyTestInstance.prisma.user.updateMany({
|
|
where: { email: testUserData.email },
|
|
data: { username: '' }
|
|
});
|
|
|
|
const response = await superGet('/user/get-session-user');
|
|
|
|
expect(response.body).toStrictEqual({ user: {}, result: '' });
|
|
expect(response.statusCode).toBe(500);
|
|
});
|
|
|
|
// This should help debugging, since this the route returns this if
|
|
// anything throws in the handler.
|
|
test('GET does not return the error response if the request is valid', async () => {
|
|
const response = await superGet('/user/get-session-user');
|
|
|
|
expect(response.body).not.toEqual({ user: {}, result: '' });
|
|
});
|
|
|
|
test('GET returns username as the result property', async () => {
|
|
const response = await superGet('/user/get-session-user');
|
|
|
|
expect(response.body).toMatchObject({
|
|
result: testUserData.username
|
|
});
|
|
expect(response.statusCode).toBe(200);
|
|
});
|
|
|
|
test('GET returns the public user object', async () => {
|
|
// TODO: This gets the user from the database so that we can verify the
|
|
// joinDate. It feels like there should be a better way to do this.
|
|
const testUser = await fastifyTestInstance?.prisma.user.findFirst({
|
|
where: { email: testUserData.email }
|
|
});
|
|
const publicUser = {
|
|
...sessionUserData,
|
|
id: testUser?.id,
|
|
joinDate: new ObjectId(testUser?.id).getTimestamp().toISOString()
|
|
};
|
|
|
|
const response = await superGet('/user/get-session-user');
|
|
const {
|
|
user: { foobar }
|
|
} = response.body as unknown as {
|
|
user: { foobar: typeof publicUser };
|
|
};
|
|
|
|
expect(testUser).not.toBeNull();
|
|
expect(testUser?.id).not.toBeNull();
|
|
expect(foobar).toEqual(publicUser);
|
|
});
|
|
|
|
test('GET returns the userToken if it exists', async () => {
|
|
const tokenData = {
|
|
userId: defaultUserId,
|
|
ttl: 123,
|
|
id: userTokenId,
|
|
created: new Date()
|
|
};
|
|
|
|
await fastifyTestInstance.prisma.userToken.create({
|
|
data: tokenData
|
|
});
|
|
|
|
const tokens = await fastifyTestInstance.prisma.userToken.count();
|
|
expect(tokens).toBe(1);
|
|
|
|
const response = await superGet('/user/get-session-user');
|
|
|
|
const { userToken } = jwt.decode(
|
|
response.body.user.foobar.userToken
|
|
) as { userToken: string };
|
|
|
|
expect(tokenData.id).toBe(userToken);
|
|
});
|
|
|
|
test('GET returns the msUsername if it exists', async () => {
|
|
await fastifyTestInstance.prisma.msUsername.create({
|
|
data: msUsernameData[0] as (typeof msUsernameData)[0]
|
|
});
|
|
|
|
const msUsernames = await fastifyTestInstance.prisma.msUsername.count();
|
|
expect(msUsernames).toBe(1);
|
|
|
|
const response = await superGet('/user/get-session-user');
|
|
|
|
const { msUsername } = response.body.user.foobar;
|
|
|
|
expect(msUsername).toBe(msUsernameData[0]?.msUsername);
|
|
});
|
|
|
|
test('GET returns a minimal user when all optional properties are missing', async () => {
|
|
// To get a minimal test user we first delete the existing one...
|
|
await fastifyTestInstance.prisma.user.deleteMany({
|
|
where: {
|
|
email: minimalUserData.email
|
|
}
|
|
});
|
|
// ...then recreate it using only the properties that the schema
|
|
// requires. The alternative is to update, but that would require
|
|
// a lot of unsets (this is neater)
|
|
const testUser = await fastifyTestInstance.prisma.user.create({
|
|
data: minimalUserData
|
|
});
|
|
|
|
// devLogin must not be used here since it overrides the user
|
|
const res = await superRequest('/signin', { method: 'GET' });
|
|
const setCookies = res.get('Set-Cookie');
|
|
|
|
const publicUser = {
|
|
..._.omit(minimalUserData, ['externalId', 'unsubscribeId']),
|
|
...computedProperties,
|
|
id: testUser.id,
|
|
joinDate: new ObjectId(testUser.id).getTimestamp().toISOString(),
|
|
// the following properties are defaults provided if the field is
|
|
// missing in the user document.
|
|
currentChallengeId: '',
|
|
completedChallenges: [],
|
|
completedExams: [],
|
|
completedSurveys: [],
|
|
partiallyCompletedChallenges: [],
|
|
portfolio: [],
|
|
savedChallenges: [],
|
|
yearsTopContributor: [],
|
|
is2018DataVisCert: false,
|
|
is2018FullStackCert: false,
|
|
isApisMicroservicesCert: false,
|
|
isBackEndCert: false,
|
|
isCheater: false,
|
|
isCollegeAlgebraPyCertV8: false,
|
|
isDataAnalysisPyCertV7: false,
|
|
isDataVisCert: false,
|
|
isFoundationalCSharpCertV8: false,
|
|
isFrontEndCert: false,
|
|
isFrontEndLibsCert: false,
|
|
isFullStackCert: false,
|
|
isHonest: false,
|
|
isInfosecCertV7: false,
|
|
isInfosecQaCert: false,
|
|
isJsAlgoDataStructCert: false,
|
|
isJsAlgoDataStructCertV8: false,
|
|
isMachineLearningPyCertV7: false,
|
|
isQaCertV7: false,
|
|
isRelationalDatabaseCertV8: false,
|
|
isRespWebDesignCert: false,
|
|
isSciCompPyCertV7: false,
|
|
keyboardShortcuts: false,
|
|
location: '',
|
|
name: '',
|
|
theme: 'default'
|
|
};
|
|
|
|
const response = await superRequest('/user/get-session-user', {
|
|
method: 'GET',
|
|
setCookies
|
|
});
|
|
|
|
const {
|
|
user: { testuser }
|
|
} = response.body as unknown as {
|
|
user: { testuser: typeof publicUser };
|
|
};
|
|
|
|
expect(testuser).toStrictEqual(publicUser);
|
|
});
|
|
});
|
|
|
|
describe('/user/report-user', () => {
|
|
let sendEmailSpy: jest.SpyInstance;
|
|
beforeEach(() => {
|
|
sendEmailSpy = jest
|
|
.spyOn(fastifyTestInstance, 'sendEmail')
|
|
.mockImplementation(jest.fn());
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
test('POST returns 400 for empty username', async () => {
|
|
const response = await superPost('/user/report-user').send({
|
|
username: '',
|
|
reportDescription: 'Test Report'
|
|
});
|
|
|
|
expect(response.statusCode).toBe(400);
|
|
expect(response.body).toStrictEqual({
|
|
type: 'danger',
|
|
message: 'flash.provide-username'
|
|
});
|
|
});
|
|
|
|
test('POST returns 400 for empty report', async () => {
|
|
const response = await superPost('/user/report-user').send({
|
|
username: 'darth-vader',
|
|
reportDescription: ''
|
|
});
|
|
|
|
expect(response.statusCode).toBe(400);
|
|
expect(response.body).toStrictEqual({
|
|
type: 'danger',
|
|
message: 'flash.provide-username'
|
|
});
|
|
});
|
|
|
|
test('POST sanitises report description', async () => {
|
|
await superPost('/user/report-user').send({
|
|
username: 'darth-vader',
|
|
reportDescription:
|
|
'<script>const breath = "loud"</script>Luke, I am your father'
|
|
});
|
|
|
|
expect(sendEmailSpy).toHaveBeenCalledTimes(1);
|
|
expect(sendEmailSpy).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
text: expect.stringContaining(
|
|
'Report Details:\n\nLuke, I am your father'
|
|
)
|
|
})
|
|
);
|
|
});
|
|
|
|
test('POST returns 200 status code with "success" message', async () => {
|
|
const testUser = await fastifyTestInstance.prisma.user.findFirstOrThrow(
|
|
{
|
|
where: { email: testUserData.email }
|
|
}
|
|
);
|
|
const response = await superPost('/user/report-user').send({
|
|
username: 'darth-vader',
|
|
reportDescription: 'Luke, I am your father'
|
|
});
|
|
|
|
expect(sendEmailSpy).toHaveBeenCalledTimes(1);
|
|
expect(sendEmailSpy).toHaveBeenCalledWith({
|
|
from: 'team@freecodecamp.org',
|
|
to: 'support@freecodecamp.org',
|
|
cc: 'foo@bar.com',
|
|
subject: "Abuse Report: Reporting darth-vader's profile",
|
|
text: `
|
|
Hello Team,
|
|
|
|
This is to report the profile of darth-vader.
|
|
|
|
Report Details:
|
|
|
|
Luke, I am your father
|
|
|
|
|
|
Reported by:
|
|
Username: ${testUser.username}
|
|
Name:
|
|
Email: foo@bar.com
|
|
|
|
Thanks and regards,
|
|
`
|
|
});
|
|
|
|
expect(response.statusCode).toBe(200);
|
|
expect(response.body).toStrictEqual({
|
|
type: 'info',
|
|
message: 'flash.report-sent',
|
|
variables: { email: 'foo@bar.com' }
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('/user/ms-username', () => {
|
|
describe('DELETE', () => {
|
|
afterEach(async () => {
|
|
await fastifyTestInstance.prisma.msUsername.deleteMany({
|
|
where: { userId: otherUserId }
|
|
});
|
|
});
|
|
|
|
test('deletes all Microsoft usernames associated with the user', async () => {
|
|
await fastifyTestInstance.prisma.msUsername.createMany({
|
|
data: [
|
|
{ msUsername: 'foobar', userId: defaultUserId, ttl: 123 },
|
|
{ msUsername: 'foobar2', userId: defaultUserId, ttl: 123 }
|
|
]
|
|
});
|
|
|
|
const response = await superDelete('/user/ms-username');
|
|
|
|
const msUsernames =
|
|
await fastifyTestInstance.prisma.msUsername.count();
|
|
|
|
expect(msUsernames).toBe(0);
|
|
expect(response.body).toStrictEqual({ msUsername: null });
|
|
expect(response.statusCode).toBe(200);
|
|
});
|
|
|
|
test('does not delete Microsoft usernames associated with other users', async () => {
|
|
await fastifyTestInstance.prisma.msUsername.createMany({
|
|
data: [
|
|
{ msUsername: 'foobar', userId: otherUserId, ttl: 123 },
|
|
{ msUsername: 'foobar2', userId: defaultUserId, ttl: 123 }
|
|
]
|
|
});
|
|
|
|
await superDelete('/user/ms-username');
|
|
|
|
const msUsernames =
|
|
await fastifyTestInstance.prisma.msUsername.count();
|
|
|
|
expect(msUsernames).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('POST', () => {
|
|
beforeEach(() => {
|
|
mockedFetch.mockClear();
|
|
});
|
|
afterEach(async () => {
|
|
await fastifyTestInstance.prisma.msUsername.deleteMany({
|
|
where: {
|
|
OR: [
|
|
{ userId: defaultUserId },
|
|
{ userId: 'aaaaaaaaaaaaaaaaaaaaaaaa' }
|
|
]
|
|
}
|
|
});
|
|
});
|
|
|
|
it('handles missing transcript urls', async () => {
|
|
const response = await superPost('/user/ms-username');
|
|
|
|
expect(response.body).toStrictEqual({
|
|
type: 'error',
|
|
message: 'flash.ms.transcript.link-err-1'
|
|
});
|
|
expect(response.statusCode).toBe(400);
|
|
});
|
|
|
|
it('handles invalid transcript urls', async () => {
|
|
mockedFetch.mockImplementationOnce(() =>
|
|
Promise.resolve({
|
|
ok: false
|
|
})
|
|
);
|
|
|
|
const response = await superPost('/user/ms-username').send({
|
|
msTranscriptUrl: 'https://www.example.com'
|
|
});
|
|
|
|
expect(response.body).toStrictEqual({
|
|
type: 'error',
|
|
message: 'flash.ms.transcript.link-err-2'
|
|
});
|
|
expect(response.statusCode).toBe(404);
|
|
});
|
|
|
|
it('handles the case that MS does not return a username', async () => {
|
|
mockedFetch.mockImplementationOnce(() =>
|
|
Promise.resolve({
|
|
ok: true,
|
|
json: () => Promise.resolve({})
|
|
})
|
|
);
|
|
|
|
const response = await superPost('/user/ms-username').send({
|
|
msTranscriptUrl: 'https://www.example.com'
|
|
});
|
|
|
|
expect(response.body).toStrictEqual({
|
|
type: 'error',
|
|
message: 'flash.ms.transcript.link-err-3'
|
|
});
|
|
expect(response.statusCode).toBe(500);
|
|
});
|
|
|
|
it('handles duplicate Microsoft usernames', async () => {
|
|
mockedFetch.mockImplementationOnce(() =>
|
|
Promise.resolve({
|
|
ok: true,
|
|
json: () =>
|
|
Promise.resolve({
|
|
userName: 'foobar'
|
|
})
|
|
})
|
|
);
|
|
|
|
await fastifyTestInstance.prisma.msUsername.create({
|
|
data: {
|
|
msUsername: 'foobar',
|
|
userId: defaultUserId,
|
|
ttl: 77760000000
|
|
}
|
|
});
|
|
|
|
const response = await superPost('/user/ms-username').send({
|
|
msTranscriptUrl: 'https://www.example.com'
|
|
});
|
|
|
|
expect(response.body).toStrictEqual({
|
|
type: 'error',
|
|
message: 'flash.ms.transcript.link-err-4'
|
|
});
|
|
|
|
expect(response.statusCode).toBe(403);
|
|
});
|
|
|
|
it('returns the username on success', async () => {
|
|
const msUsername = 'ms-user';
|
|
mockedFetch.mockImplementationOnce(() =>
|
|
Promise.resolve({
|
|
ok: true,
|
|
json: () =>
|
|
Promise.resolve({
|
|
userName: msUsername
|
|
})
|
|
})
|
|
);
|
|
const response = await superPost('/user/ms-username').send({
|
|
msTranscriptUrl: 'https://www.example.com'
|
|
});
|
|
|
|
expect(response.body).toStrictEqual({
|
|
msUsername
|
|
});
|
|
expect(response.statusCode).toBe(200);
|
|
});
|
|
|
|
it('creates a record of the linked account', async () => {
|
|
const msUsername = 'super-user';
|
|
mockedFetch.mockImplementationOnce(() =>
|
|
Promise.resolve({
|
|
ok: true,
|
|
json: () =>
|
|
Promise.resolve({
|
|
userName: msUsername
|
|
})
|
|
})
|
|
);
|
|
|
|
await superPost('/user/ms-username').send({
|
|
msTranscriptUrl: 'https://www.example.com'
|
|
});
|
|
|
|
const linkedAccount =
|
|
await fastifyTestInstance.prisma.msUsername.findFirstOrThrow({
|
|
where: { msUsername }
|
|
});
|
|
|
|
expect(linkedAccount).toStrictEqual({
|
|
id: expect.stringMatching(/^[a-f\d]{24}$/),
|
|
userId: defaultUserId,
|
|
ttl: 77760000000,
|
|
msUsername
|
|
});
|
|
});
|
|
|
|
it('removes any other accounts linked to the same user', async () => {
|
|
const msUsernameOne = 'super-user';
|
|
const msUsernameTwo = 'super-user-2';
|
|
mockedFetch
|
|
.mockImplementationOnce(() =>
|
|
Promise.resolve({
|
|
ok: true,
|
|
json: () =>
|
|
Promise.resolve({
|
|
userName: msUsernameOne
|
|
})
|
|
})
|
|
)
|
|
.mockImplementationOnce(() =>
|
|
Promise.resolve({
|
|
ok: true,
|
|
json: () =>
|
|
Promise.resolve({
|
|
userName: msUsernameTwo
|
|
})
|
|
})
|
|
);
|
|
|
|
await fastifyTestInstance.prisma.msUsername.create({
|
|
data: {
|
|
msUsername: 'dummy',
|
|
userId: 'aaaaaaaaaaaaaaaaaaaaaaaa',
|
|
ttl: 77760000000
|
|
}
|
|
});
|
|
|
|
await superPost('/user/ms-username').send({
|
|
msTranscriptUrl: 'https://www.example.com'
|
|
});
|
|
await superPost('/user/ms-username').send({
|
|
msTranscriptUrl: 'https://www.example.com'
|
|
});
|
|
|
|
const linkedAccounts =
|
|
await fastifyTestInstance.prisma.msUsername.findMany({});
|
|
|
|
expect(linkedAccounts).toHaveLength(2);
|
|
expect(linkedAccounts[1]?.msUsername).toBe(msUsernameTwo);
|
|
});
|
|
|
|
it('calls the Microsoft API with the correct url', async () => {
|
|
const msTranscriptUrl =
|
|
'https://learn.microsoft.com/en-us/users/mot01/transcript/8u6awert43q1plo';
|
|
|
|
const msTranscriptApiUrl =
|
|
'https://learn.microsoft.com/api/profiles/transcript/share/8u6awert43q1plo';
|
|
|
|
await superPost('/user/ms-username').send({
|
|
msTranscriptUrl
|
|
});
|
|
|
|
expect(mockedFetch).toHaveBeenCalledWith(msTranscriptApiUrl);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('/user/submit-survey', () => {
|
|
afterEach(async () => {
|
|
await fastifyTestInstance.prisma.survey.deleteMany({
|
|
where: { userId: defaultUserId }
|
|
});
|
|
});
|
|
|
|
test('POST returns 400 for invalid survey title', async () => {
|
|
const response = await superPost('/user/submit-survey').send({
|
|
surveyResults: { ...mockSurveyResults, title: 'Invalid Survey' }
|
|
});
|
|
|
|
expect(response.statusCode).toBe(400);
|
|
expect(response.body).toStrictEqual({
|
|
type: 'error',
|
|
message: 'flash.survey.err-1'
|
|
});
|
|
});
|
|
|
|
test('POST returns 400 if user already submitted survey', async () => {
|
|
// Submit survey for first time
|
|
await superPost('/user/submit-survey').send({
|
|
surveyResults: mockSurveyResults
|
|
});
|
|
|
|
// Submit same survey again to get failed response
|
|
const response = await superPost('/user/submit-survey').send({
|
|
surveyResults: mockSurveyResults
|
|
});
|
|
|
|
expect(response.statusCode).toBe(400);
|
|
expect(response.body).toStrictEqual({
|
|
type: 'error',
|
|
message: 'flash.survey.err-2'
|
|
});
|
|
});
|
|
|
|
test('POST returns 200 status code with "success" message', async () => {
|
|
const response = await superPost('/user/submit-survey').send({
|
|
surveyResults: mockSurveyResults
|
|
});
|
|
|
|
expect(response.statusCode).toBe(200);
|
|
expect(response.body).toStrictEqual({
|
|
type: 'success',
|
|
message: 'flash.survey.success'
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Unauthenticated user', () => {
|
|
let setCookies: string[];
|
|
// Get the CSRF cookies from an unprotected route
|
|
beforeAll(async () => {
|
|
const res = await superRequest('/status/ping', { method: 'GET' });
|
|
setCookies = res.get('Set-Cookie');
|
|
});
|
|
|
|
const endpoints: { path: string; method: 'GET' | 'POST' | 'DELETE' }[] = [
|
|
{ path: '/account/delete', method: 'POST' },
|
|
{ path: '/account/reset-progress', method: 'POST' },
|
|
{ path: '/user/get-session-user', method: 'GET' },
|
|
{ path: '/user/user-token', method: 'DELETE' },
|
|
{ path: '/user/user-token', method: 'POST' },
|
|
{ path: '/user/ms-username', method: 'DELETE' },
|
|
{ path: '/user/report-user', method: 'POST' },
|
|
{ path: '/user/ms-username', method: 'POST' },
|
|
{ path: '/user/submit-survey', method: 'POST' }
|
|
];
|
|
|
|
endpoints.forEach(({ path, method }) => {
|
|
test(`${method} ${path} returns 401 status code with error message`, async () => {
|
|
const response = await superRequest(path, {
|
|
method,
|
|
setCookies
|
|
});
|
|
expect(response.statusCode).toBe(401);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Public', () => {
|
|
let superGet: ReturnType<typeof createSuperRequest>;
|
|
|
|
beforeEach(() => {
|
|
superGet = createSuperRequest({ method: 'GET' });
|
|
});
|
|
|
|
describe('/api/users/get-public-profile', () => {
|
|
const profilelessUsername = 'profileless-user';
|
|
const lockedUsername = 'locked-user';
|
|
const publicUsername = 'public-user';
|
|
const lockedUserProfileUI = {
|
|
isLocked: true,
|
|
showAbout: true,
|
|
showPortfolio: false
|
|
};
|
|
const unlockedUserProfileUI = {
|
|
isLocked: false,
|
|
showAbout: true,
|
|
showCerts: true,
|
|
showDonation: true,
|
|
showHeatMap: true,
|
|
showLocation: true,
|
|
showName: true,
|
|
showPoints: true,
|
|
showPortfolio: true,
|
|
showTimeLine: true
|
|
};
|
|
const users = [profilelessUsername, lockedUsername, publicUsername];
|
|
beforeAll(async () => {
|
|
await fastifyTestInstance.prisma.user.create({
|
|
data: {
|
|
...minimalUserData,
|
|
email: profilelessUsername,
|
|
username: profilelessUsername
|
|
}
|
|
});
|
|
await fastifyTestInstance.prisma.user.create({
|
|
data: {
|
|
...minimalUserData,
|
|
email: lockedUsername,
|
|
username: lockedUsername,
|
|
profileUI: lockedUserProfileUI
|
|
}
|
|
});
|
|
await fastifyTestInstance.prisma.user.create({
|
|
data: {
|
|
...testUserData,
|
|
email: publicUsername,
|
|
username: publicUsername,
|
|
profileUI: unlockedUserProfileUI
|
|
}
|
|
});
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await fastifyTestInstance.prisma.user.deleteMany({
|
|
where: {
|
|
OR: users.map(username => ({ username }))
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('GET', () => {
|
|
test('returns 400 status code if the username param is missing', async () => {
|
|
const res = await superGet('/api/users/get-public-profile');
|
|
// TODO(Post-MVP): return something more informative
|
|
expect(res.body).toStrictEqual({});
|
|
expect(res.statusCode).toBe(400);
|
|
});
|
|
|
|
test('returns 400 status code if the username param is empty', async () => {
|
|
const res = await superGet('/api/users/get-public-profile?username=');
|
|
// TODO(Post-MVP): return something more informative
|
|
expect(res.body).toStrictEqual({});
|
|
expect(res.statusCode).toBe(400);
|
|
});
|
|
|
|
test('returns 404 status code for non-existent user', async () => {
|
|
const response = await superGet(
|
|
'/api/users/get-public-profile?username=non-existent'
|
|
);
|
|
// TODO(Post-MVP): return something more informative
|
|
expect(response.body).toStrictEqual({});
|
|
expect(response.statusCode).toBe(404);
|
|
});
|
|
|
|
test('returns 200 status code with a locked profile if the profile is private', async () => {
|
|
const response = await superGet(
|
|
`/api/users/get-public-profile?username=${lockedUsername}`
|
|
);
|
|
|
|
expect(response.body).toStrictEqual({
|
|
entities: {
|
|
user: {
|
|
[lockedUsername]: {
|
|
isLocked: true,
|
|
profileUI: lockedUserProfileUI,
|
|
username: lockedUsername
|
|
}
|
|
}
|
|
},
|
|
result: lockedUsername
|
|
});
|
|
expect(response.statusCode).toBe(200);
|
|
});
|
|
|
|
test('returns 200 status code locked profile if the profile is missing', async () => {
|
|
const response = await superGet(
|
|
`/api/users/get-public-profile?username=${profilelessUsername}`
|
|
);
|
|
|
|
expect(response.body).toStrictEqual({
|
|
entities: {
|
|
user: {
|
|
[profilelessUsername]: {
|
|
isLocked: true,
|
|
profileUI: lockedProfileUI,
|
|
username: profilelessUsername
|
|
}
|
|
}
|
|
},
|
|
result: profilelessUsername
|
|
});
|
|
expect(response.statusCode).toBe(200);
|
|
});
|
|
// TODO: create a list of public properties like the api-server and use that
|
|
// to restrict the output of this and get-session-user.
|
|
test('returns 200 status code with public user object', async () => {
|
|
const testUser =
|
|
await fastifyTestInstance.prisma.user.findFirstOrThrow({
|
|
where: { email: publicUsername }
|
|
});
|
|
const response = await superGet(
|
|
`/api/users/get-public-profile?username=${publicUsername}`
|
|
);
|
|
|
|
// TODO: create a fixture for this without 'completedSurveys', ideally
|
|
// it should contain the entire body.
|
|
const publicUser = {
|
|
// TODO(Post-MVP, maybe): return completedSurveys?
|
|
..._.omit(publicUserData, 'completedSurveys'),
|
|
username: publicUsername,
|
|
joinDate: new ObjectId(testUser.id).getTimestamp().toISOString(),
|
|
profileUI: unlockedUserProfileUI
|
|
};
|
|
|
|
expect(response.body).toStrictEqual({
|
|
entities: {
|
|
user: {
|
|
[publicUsername]: publicUser
|
|
}
|
|
},
|
|
result: publicUsername
|
|
});
|
|
expect(response.statusCode).toBe(200);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Microsoft helpers', () => {
|
|
describe('getMsTranscriptApiUrl', () => {
|
|
const expectedUrl =
|
|
'https://learn.microsoft.com/api/profiles/transcript/share/8u6awert43q1plo';
|
|
|
|
const urlWithoutSlash =
|
|
'https://learn.microsoft.com/en-us/users/mot01/transcript/8u6awert43q1plo';
|
|
const urlWithSlash = `${urlWithoutSlash}/`;
|
|
const urlWithQueryParams = `${urlWithoutSlash}?foo=bar`;
|
|
const urlWithQueryParamsAndSlash = `${urlWithSlash}?foo=bar`;
|
|
|
|
it('should extract the transcript id from the url', () => {
|
|
expect(getMsTranscriptApiUrl(urlWithoutSlash)).toBe(expectedUrl);
|
|
});
|
|
|
|
it('should handle trailing slashes', () => {
|
|
expect(getMsTranscriptApiUrl(urlWithSlash)).toBe(expectedUrl);
|
|
});
|
|
|
|
it('should ignore query params', () => {
|
|
expect(getMsTranscriptApiUrl(urlWithQueryParams)).toBe(expectedUrl);
|
|
expect(getMsTranscriptApiUrl(urlWithQueryParamsAndSlash)).toBe(
|
|
expectedUrl
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('get-public-profile helpers', () => {
|
|
describe('replacePrivateData', () => {
|
|
const user = {
|
|
about: 'about',
|
|
calendar: { 1: 1, 2: 1 } as const,
|
|
completedChallenges: [
|
|
{ id: '123', completedDate: 123, files: [] },
|
|
{ id: '456', completedDate: 456, challengeType: 7, files: [] }
|
|
],
|
|
id: '5f5b1b3b1c9d440000d9e3b4',
|
|
isDonating: false,
|
|
location: 'location',
|
|
joinDate: 'joinDate',
|
|
name: 'name',
|
|
points: 2,
|
|
portfolio: [
|
|
{
|
|
id: '789',
|
|
title: 'portfolio',
|
|
url: 'url',
|
|
image: 'image',
|
|
description: 'description'
|
|
}
|
|
],
|
|
profileUI: {
|
|
isLocked: false,
|
|
showAbout: true,
|
|
showCerts: true,
|
|
showDonation: true,
|
|
showHeatMap: true,
|
|
showLocation: true,
|
|
showName: true,
|
|
showPoints: true,
|
|
showPortfolio: true,
|
|
showTimeLine: true
|
|
}
|
|
};
|
|
|
|
test(`returns "" for 'about' if showAbout is not true`, () => {
|
|
const userWithoutAbout = {
|
|
...user,
|
|
profileUI: { ...user.profileUI, showAbout: false }
|
|
};
|
|
expect(replacePrivateData(userWithoutAbout)).toMatchObject({
|
|
about: ''
|
|
});
|
|
});
|
|
|
|
test('returns {} for calendar if showHeatMap is not true', () => {
|
|
const userWithoutHeatMap = {
|
|
...user,
|
|
profileUI: { ...user.profileUI, showHeatMap: false }
|
|
};
|
|
expect(replacePrivateData(userWithoutHeatMap).calendar).toEqual({});
|
|
});
|
|
|
|
test(`returns [] for completeChallenges if showTimeLine is not true`, () => {
|
|
const userWithoutTimeLine = {
|
|
...user,
|
|
profileUI: { ...user.profileUI, showTimeLine: false }
|
|
};
|
|
expect(replacePrivateData(userWithoutTimeLine)).toMatchObject({
|
|
completedChallenges: []
|
|
});
|
|
});
|
|
|
|
test('omits certifications from completedChallenges if showCerts is not true', () => {
|
|
const userWithoutCerts = {
|
|
...user,
|
|
profileUI: { ...user.profileUI, showCerts: false }
|
|
};
|
|
expect(replacePrivateData(userWithoutCerts)).toMatchObject({
|
|
completedChallenges: [{ id: '123', completedDate: 123, files: [] }]
|
|
});
|
|
});
|
|
|
|
test('returns null for isDonating if showDonation is not true', () => {
|
|
const userWithoutDonation = {
|
|
...user,
|
|
profileUI: { ...user.profileUI, showDonation: false }
|
|
};
|
|
expect(replacePrivateData(userWithoutDonation)).toMatchObject({
|
|
isDonating: null
|
|
});
|
|
});
|
|
|
|
test('returns "" for joinDate if showAbout is not true', () => {
|
|
const userWithoutAbout = {
|
|
...user,
|
|
profileUI: { ...user.profileUI, showAbout: false }
|
|
};
|
|
expect(replacePrivateData(userWithoutAbout)).toMatchObject({
|
|
joinDate: ''
|
|
});
|
|
});
|
|
|
|
test(`returns "" for 'location' if showLocation is not true`, () => {
|
|
const userWithoutLocation = {
|
|
...user,
|
|
profileUI: { ...user.profileUI, showLocation: false }
|
|
};
|
|
expect(replacePrivateData(userWithoutLocation)).toMatchObject({
|
|
location: ''
|
|
});
|
|
});
|
|
|
|
test(`returns "" for 'name' if showName is not true`, () => {
|
|
const userWithoutName = {
|
|
...user,
|
|
profileUI: { ...user.profileUI, showName: false }
|
|
};
|
|
expect(replacePrivateData(userWithoutName)).toMatchObject({
|
|
name: ''
|
|
});
|
|
});
|
|
|
|
test('returns null for points if showPoints is not true', () => {
|
|
const userWithoutPoints = {
|
|
...user,
|
|
profileUI: { ...user.profileUI, showPoints: false }
|
|
};
|
|
expect(replacePrivateData(userWithoutPoints)).toMatchObject({
|
|
points: null
|
|
});
|
|
});
|
|
|
|
test('returns [] for portfolio if showPortfolio is not true', () => {
|
|
const userWithoutPortfolio = {
|
|
...user,
|
|
profileUI: { ...user.profileUI, showPortfolio: false }
|
|
};
|
|
expect(replacePrivateData(userWithoutPortfolio)).toMatchObject({
|
|
portfolio: []
|
|
});
|
|
});
|
|
|
|
test('returns the expected public user object if all showX flags are true', () => {
|
|
expect(replacePrivateData(user)).toEqual(
|
|
_.omit(user, ['id', 'profileUI'])
|
|
);
|
|
});
|
|
});
|
|
});
|