diff --git a/.eslintrc.js b/.eslintrc.js index 67b3a3c..1259c9e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -9,6 +9,8 @@ module.exports = { '@typescript-eslint/explicit-module-boundary-types': 0, '@typescript-eslint/no-unused-vars': 0, 'semi-style': ['error', 'last'], + 'import/named': 0, + 'import/prefer-default-export': 0, }, // This gets rid of weird react error for non-react project settings: { diff --git a/.gitignore b/.gitignore index 86c84b5..93298bc 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ nohup.out *src/certificates* Makefile start.sh +*secrets* # ignore database dumps dump/ diff --git a/src/entities/User.ts b/src/entities/User.ts index d5a1b0d..43b3f7e 100644 --- a/src/entities/User.ts +++ b/src/entities/User.ts @@ -6,6 +6,7 @@ import WeeklyDebrief from './WeeklyDebrief'; import { PublicationSlug } from '../common/types'; import { Flyer } from './Flyer'; import { Organization } from './Organization'; +import { Magazine } from './Magazine'; @ObjectType({ description: 'The User Model' }) export class User { @@ -42,6 +43,10 @@ export class User { @Property({ required: true, type: () => Article, default: [] }) readArticles: mongoose.Types.DocumentArray>; + @Field((type) => [Magazine]) + @Property({ required: true, type: () => Magazine, default: [] }) + readMagazines: mongoose.Types.DocumentArray>; + @Field((type) => [Flyer]) @Property({ required: true, type: () => Flyer, default: [] }) readFlyers: mongoose.Types.DocumentArray>; diff --git a/src/entities/WeeklyDebrief.ts b/src/entities/WeeklyDebrief.ts index 0f14279..b884afd 100644 --- a/src/entities/WeeklyDebrief.ts +++ b/src/entities/WeeklyDebrief.ts @@ -2,6 +2,7 @@ import { Field, ID, ObjectType } from 'type-graphql'; import { prop as Property, DocumentType } from '@typegoose/typegoose'; import mongoose from 'mongoose'; import { Article } from './Article'; +import { Magazine } from './Magazine'; @ObjectType({ description: 'The Weekly Debrief Model' }) export default class WeeklyDebrief { @@ -36,6 +37,10 @@ export default class WeeklyDebrief { @Property({ required: true, type: () => Article, default: [] }) readArticles: mongoose.Types.DocumentArray>; + @Field((type) => [Magazine]) + @Property({ required: true, type: () => Magazine, default: [] }) + readMagazines: mongoose.Types.DocumentArray>; + @Field((type) => [Article]) @Property({ required: true, type: () => Article, default: [] }) randomArticles: mongoose.Types.DocumentArray>; diff --git a/src/repos/UserRepo.ts b/src/repos/UserRepo.ts index de9fd41..1997965 100644 --- a/src/repos/UserRepo.ts +++ b/src/repos/UserRepo.ts @@ -3,6 +3,8 @@ import { Article } from '../entities/Article'; import ArticleRepo from './ArticleRepo'; import { PublicationSlug } from '../common/types'; import { User, UserModel } from '../entities/User'; +import { Magazine } from '../entities/Magazine'; +import MagazineRepo from './MagazineRepo'; /** * Create new user associated with deviceToken and followedPublicationsSlugs of deviceType. @@ -96,6 +98,25 @@ const appendReadArticle = async (uuid: string, articleID: string): Promise return user; }; +/** + * Add a magazine to a user's readMagazines + */ +const appendReadMagazine = async (uuid: string, magazineID: string): Promise => { + const user = await UserModel.findOne({ uuid }); + + if (!user) return user; + + const magazine = await MagazineRepo.getMagazineByID(magazineID); + const checkDuplicates = (prev: boolean, cur: Magazine) => prev || cur.id === magazineID; + + if (magazine && !user.readMagazines.reduce(checkDuplicates, false)) { + user.readMagazines.push(magazine); + } + + user.save(); + return user; +}; + /** * Increment shoutouts in user's numShoutouts */ @@ -126,6 +147,7 @@ const incrementBookmarks = async (uuid: string): Promise => { export default { appendReadArticle, + appendReadMagazine, createUser, followPublication, getUserByUUID, diff --git a/src/repos/WeeklyDebriefRepo.ts b/src/repos/WeeklyDebriefRepo.ts index e7f1472..d314146 100644 --- a/src/repos/WeeklyDebriefRepo.ts +++ b/src/repos/WeeklyDebriefRepo.ts @@ -24,7 +24,9 @@ const createWeeklyDebrief = async ( numShoutouts: user.numShoutouts, numBookmarkedArticles: user.numBookmarkedArticles, readArticles: user.readArticles.slice(0, 2), + readMagazines: user.readMagazines.slice(0, 2), numReadArticles: user.readArticles.length, + numReadMagazines: user.readMagazines.length, randomArticles: await articleAggregate.sample(2).exec(), }); return UserModel.findOneAndUpdate( @@ -32,6 +34,7 @@ const createWeeklyDebrief = async ( { $set: { readArticles: [], + readMagazines: [], numShoutouts: 0, numBookmarkedArticles: 0, weeklyDebrief, diff --git a/src/resolvers/UserResolver.ts b/src/resolvers/UserResolver.ts index 69a8e9f..7b73f39 100644 --- a/src/resolvers/UserResolver.ts +++ b/src/resolvers/UserResolver.ts @@ -50,6 +50,14 @@ class UserResolver { return await UserRepo.appendReadArticle(uuid, articleID); } + @Mutation((_returns) => User, { + nullable: true, + description: "Adds the given by the to the read magazines", + }) + async readMagazine(@Arg('uuid') uuid: string, @Arg('magazineID') magazineID: string) { + return await UserRepo.appendReadMagazine(uuid, magazineID); + } + @Mutation((_returns) => User, { nullable: true, description: 'Increments the number of bookmarks for the given by ', diff --git a/src/tests/data/MagazineFactory.ts b/src/tests/data/MagazineFactory.ts new file mode 100644 index 0000000..b09d6d7 --- /dev/null +++ b/src/tests/data/MagazineFactory.ts @@ -0,0 +1,56 @@ +/* eslint-disable no-underscore-dangle */ +import { faker } from '@faker-js/faker'; +import { _ } from 'underscore'; +import PublicationFactory from './PublicationFactory'; +import { Magazine } from '../../entities/Magazine'; +import FactoryUtils from './FactoryUtils'; + +class MagazineFactory { + public static async create(n: number): Promise { + /** + * Returns a list of n number of random Magazine objects + */ + return Promise.all(FactoryUtils.create(n, MagazineFactory.fake)); + } + + public static async createSpecific( + n: number, + newMappings: { [key: string]: any }, + ): Promise { + /** + * Returns a list of n number of random Magazine objects with specified + * field values in new Mappings + */ + const arr = await MagazineFactory.create(n); + return arr.map((x) => { + const newDoc = x; + Object.entries(newMappings).forEach(([k, v]) => { + newDoc[k] = v; + }); + return newDoc; + }); + } + + public static async fake(): Promise { + /** + * Returns a Magazine with random values in its instance variables + */ + const fakeMagazine = new Magazine(); + const examplePublication = await PublicationFactory.getRandomPublication(); + + fakeMagazine.date = faker.date.past(); + fakeMagazine.semester = _.sample(['FA22', 'SP23']); + fakeMagazine.pdfURL = faker.image.cats(); + fakeMagazine.publication = examplePublication; + fakeMagazine.publicationSlug = examplePublication.slug; + fakeMagazine.shoutouts = _.random(0, 50); + fakeMagazine.title = faker.commerce.productDescription(); + fakeMagazine.nsfw = _.sample([true, false]); + fakeMagazine.isFeatured = _.sample([true, false]); + fakeMagazine.trendiness = 0; + fakeMagazine.isFiltered = _.sample([true, false]); + + return fakeMagazine; + } +} +export default MagazineFactory; diff --git a/src/tests/data/UserFactory.ts b/src/tests/data/UserFactory.ts new file mode 100644 index 0000000..70b0137 --- /dev/null +++ b/src/tests/data/UserFactory.ts @@ -0,0 +1,55 @@ +/* eslint-disable no-underscore-dangle */ +import { faker } from '@faker-js/faker'; +import { _ } from 'underscore'; +import { User } from '../../entities/User'; +import FactoryUtils from './FactoryUtils'; + +class UserFactory { + public static async create(n: number): Promise { + /** + * Returns a list of n number of random User objects + * + * @param n The number of desired random User objects + * @returns A Promise of the list of n number of random User objects + */ + return Promise.all(FactoryUtils.create(n, UserFactory.fake)); + } + + public static async createSpecific( + n: number, + newMappings: { [key: string]: any }, + ): Promise { + /** + * Returns a list of n number of random User objects with specified + * fields values in newMappings + * + * @param n The number of desired random User objects + * @param newMappings The specified values for User parameters [key] + * @returns A Promise of the list of n number of random User objects + */ + const arr = await UserFactory.create(n); + return arr.map((x) => { + const newDoc = x; + Object.entries(newMappings).forEach(([k, v]) => { + newDoc[k] = v; + }); + return newDoc; + }); + } + + public static async fake(): Promise { + /** + * Returns a User with random values in its instance variables + * + * @returns The User object with random values in its instance variables + */ + const fakeUser = new User(); + + fakeUser.deviceToken = faker.datatype.string(); + fakeUser.deviceType = _.sample(['IOS', 'ANDROID']); + fakeUser.uuid = faker.datatype.uuid(); + + return fakeUser; + } +} +export default UserFactory; diff --git a/src/tests/magazine.test.ts b/src/tests/magazine.test.ts new file mode 100644 index 0000000..d9cf358 --- /dev/null +++ b/src/tests/magazine.test.ts @@ -0,0 +1,172 @@ +/* eslint-disable no-underscore-dangle */ +/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable dot-notation */ +import { _ } from 'underscore'; +import { MagazineModel } from '../entities/Magazine'; +import MagazineRepo from '../repos/MagazineRepo'; +import MagazineFactory from './data/MagazineFactory'; +import PublicationRepo from '../repos/PublicationRepo'; +import PublicationFactory from './data/PublicationFactory'; +import FactoryUtils from './data/FactoryUtils'; + +import { dbConnection, disconnectDB } from './data/TestingDBConnection'; + +beforeAll(async () => { + await dbConnection(); + await PublicationRepo.addPublicationsToDB(); + await MagazineModel.createCollection; +}); + +beforeEach(async () => { + await MagazineModel.deleteMany({}); +}); + +afterAll(async () => { + MagazineModel.deleteMany({}).then(disconnectDB); +}); + +describe('getAllMagazine tests:', () => { + test('getAllMagazines - No Magazines', async () => { + const getMagazineResponses = await MagazineRepo.getAllMagazines(); + expect(getMagazineResponses).toHaveLength(0); + }); + test('getAllMagazines - 5 Magazines', async () => { + const magazines = await MagazineFactory.create(5); + await MagazineModel.insertMany(magazines); + + const getMagazinesResponse = await MagazineRepo.getAllMagazines(); + expect(getMagazinesResponse).toHaveLength(5); + }); + test('getAllMagazines limit 2', async () => { + const magazines = await MagazineFactory.create(3); + await MagazineModel.insertMany(magazines); + + const getMagazinesResponse = await MagazineRepo.getAllMagazines(0, 2); + expect(getMagazinesResponse).toHaveLength(2); + }); + test('getAllMagazines - Sort by date desc, offset 2, limit 2', async () => { + const magazines = await MagazineFactory.create(5); + magazines.sort(FactoryUtils.compareByDate); + await MagazineModel.insertMany(magazines); + + const magazineTitles = FactoryUtils.mapToValue(magazines.slice(2, 4), 'title'); // offset=2, limit=2 + + const getMagazinesResponse = await MagazineRepo.getAllMagazines(2, 2); + const respTitles = FactoryUtils.mapToValue(getMagazinesResponse, 'title'); + + expect(respTitles).toEqual(magazineTitles); + }); +}); + +describe('getMagazine(s)ByID(s) tests:', () => { + test('getMagazineByID - 1 Magazine', async () => { + const Magazines = await MagazineFactory.create(1); + const insertOutput = await MagazineModel.insertMany(Magazines); + const id = insertOutput[0]._id; + + const getMagazinesResponse = await MagazineRepo.getMagazineByID(id); + expect(getMagazinesResponse.title).toEqual(Magazines[0].title); + }); + test('getMagazinesByIDs - 3 Magazines', async () => { + const Magazines = await MagazineFactory.create(3); + const insertOutput = await MagazineModel.insertMany(Magazines); + const ids = FactoryUtils.mapToValue(insertOutput, '_id'); + const getMagazinesResponse = await MagazineRepo.getMagazinesByIDs(ids); + + expect(FactoryUtils.mapToValue(getMagazinesResponse, 'title')).toEqual( + FactoryUtils.mapToValue(Magazines, 'title'), + ); + }); +}); + +describe('getMagazinesByPublicationSlug(s) tests', () => { + test('getMagazinesByPublicationSlug - 1 publication, 1 Magazine', async () => { + const pub = await PublicationFactory.getRandomPublication(); + const Magazines = await MagazineFactory.createSpecific(1, { + publicationSlug: pub.slug, + publication: pub, + }); + await MagazineModel.insertMany(Magazines); + + const getMagazinesResponse = await MagazineRepo.getMagazinesByPublicationSlug(pub.slug); + expect(getMagazinesResponse[0].title).toEqual(Magazines[0].title); + }); + + test('getMagazinesByPublicationSlug - 1 publication, 3 Magazines', async () => { + const pub = await PublicationFactory.getRandomPublication(); + const Magazines = ( + await MagazineFactory.createSpecific(3, { + publicationSlug: pub.slug, + publication: pub, + }) + ).sort(FactoryUtils.compareByDate); + + await MagazineModel.insertMany(Magazines); + const getMagazinesResponse = await MagazineRepo.getMagazinesByPublicationSlug(pub.slug); + + expect(FactoryUtils.mapToValue(getMagazinesResponse, 'title')).toEqual( + FactoryUtils.mapToValue(Magazines, 'title'), + ); + }); + + test('getMagazinesByPublicationSlugs - many publications, 5 Magazines', async () => { + const Magazines = (await MagazineFactory.create(3)).sort(FactoryUtils.compareByDate); + + await MagazineModel.insertMany(Magazines); + const getMagazinesResponse = await MagazineRepo.getMagazinesByPublicationSlug( + FactoryUtils.mapToValue(Magazines, 'publicationSlug'), + ); + + expect(FactoryUtils.mapToValue(getMagazinesResponse, 'title')).toEqual( + FactoryUtils.mapToValue(Magazines, 'title'), + ); + }); +}); + +describe('getMagazinesBySemester test', () => { + test('getMagazinesBySemester, Semester - random semester, 3 articles', async () => { + const sem = _.sample(['FA22', 'SP23']); + const magazines = ( + await MagazineFactory.createSpecific(3, { + semester: sem, + }) + ).sort(FactoryUtils.compareByDate); + + await MagazineModel.insertMany(magazines); + const getMagazinesResponse = await MagazineRepo.getMagazinesBySemester(sem); + + expect(FactoryUtils.mapToValue(getMagazinesResponse, 'title')).toEqual( + FactoryUtils.mapToValue(magazines, 'title'), + ); + }); +}); + +describe('getFeaturedMagazines test', () => { + test('getFeaturedMagazines, featured = true, 3 articles', async () => { + const magazines = ( + await MagazineFactory.createSpecific(3, { + isFeatured: true, + }) + ).sort(FactoryUtils.compareByDate); + + await MagazineModel.insertMany(magazines); + const getMagazinesResponse = await MagazineRepo.getFeaturedMagazines(3); + + expect(FactoryUtils.mapToValue(getMagazinesResponse, 'title')).toEqual( + FactoryUtils.mapToValue(magazines, 'title'), + ); + }); +}); + +describe('incrementShoutouts tests', () => { + test('incrementShoutouts - Shoutout 1 Magazine', async () => { + const Magazines = await MagazineFactory.create(1); + const oldShoutouts = Magazines[0].shoutouts; + const insertOutput = await MagazineModel.insertMany(Magazines); + + await MagazineRepo.incrementShoutouts(insertOutput[0]._id); + + const getMagazinesResponse = await MagazineRepo.getMagazineByID(insertOutput[0]._id); + expect(getMagazinesResponse.shoutouts).toEqual(oldShoutouts + 1); + }); +}); diff --git a/src/tests/user.test.ts b/src/tests/user.test.ts new file mode 100644 index 0000000..73179d3 --- /dev/null +++ b/src/tests/user.test.ts @@ -0,0 +1,157 @@ +/* eslint-disable no-underscore-dangle */ +/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable dot-notation */ +/* eslint-disable no-await-in-loop */ +import { UserModel } from '../entities/User'; +import UserRepo from '../repos/UserRepo'; +import UserFactory from './data/UserFactory'; +import PublicationRepo from '../repos/PublicationRepo'; +import FactoryUtils from './data/FactoryUtils'; +import { MagazineModel } from '../entities/Magazine'; +import { ArticleModel } from '../entities/Article'; +import { dbConnection, disconnectDB } from './data/TestingDBConnection'; +import PublicationFactory from './data/PublicationFactory'; +import MagazineFactory from './data/MagazineFactory'; +import ArticleFactory from './data/ArticleFactory'; + +beforeAll(async () => { + await dbConnection(); + await PublicationRepo.addPublicationsToDB(); + await MagazineModel.createCollection(); + await UserModel.createCollection(); + await ArticleModel.createCollection(); +}); + +beforeEach(async () => { + await UserModel.deleteMany({}); + await MagazineModel.deleteMany({}); + await ArticleModel.deleteMany({}); +}); + +afterAll(async () => { + UserModel.deleteMany({}); + ArticleModel.deleteMany({}); + MagazineModel.deleteMany({}).then(disconnectDB); +}); + +describe('getUserByUUID test:', () => { + test('getUserByUUID - 1 user', async () => { + const user = await UserFactory.create(1); + const insertOutput = await UserModel.insertMany(user); + const id = insertOutput[0].uuid; + + const getUserResponse = await UserRepo.getUserByUUID(id); + expect(getUserResponse.id).toEqual(insertOutput[0].id); + }); +}); + +describe('(un)followPublication tests:', () => { + test('followPublication - 1 user, 1 publication', async () => { + const publication = await PublicationFactory.getRandomPublication(); + const users = await UserFactory.create(1); + await UserModel.insertMany(users); + await UserRepo.followPublication(users[0].uuid, publication.slug); + + const getUserResponse = await UserRepo.getUsersFollowingPublication(publication.slug); + expect(FactoryUtils.mapToValue(getUserResponse, 'uuid')).toEqual( + FactoryUtils.mapToValue(users, 'uuid'), + ); + }); + + test('followPublication - 3 users, 1 publication', async () => { + const publication = await PublicationFactory.getRandomPublication(); + const users = await UserFactory.create(3); + await UserModel.insertMany(users); + for (let i = 0; i < 3; i++) await UserRepo.followPublication(users[i].uuid, publication.slug); + + const getUsersResponse = await UserRepo.getUsersFollowingPublication(publication.slug); + expect(FactoryUtils.mapToValue(getUsersResponse, 'uuid')).toEqual( + FactoryUtils.mapToValue(users, 'uuid'), + ); + }); + + test('unfollowPublication - 1 user, 1 publication', async () => { + const publication = await PublicationFactory.getRandomPublication(); + const users = await UserFactory.create(1); + await UserModel.insertMany(users); + + await UserRepo.followPublication(users[0].uuid, publication.slug); + await UserRepo.unfollowPublication(users[0].uuid, publication.slug); + + const getUsersResponse = await UserRepo.getUsersFollowingPublication(publication.slug); + expect(getUsersResponse).toHaveLength(0); + }); + + test('unfollowPublication - 3 users, 1 publication', async () => { + const publication = await PublicationFactory.getRandomPublication(); + const users = await UserFactory.create(3); + await UserModel.insertMany(users); + + for (let i = 0; i < 3; i++) await UserRepo.followPublication(users[i].uuid, publication.slug); + for (let i = 0; i < 2; i++) await UserRepo.unfollowPublication(users[i].uuid, publication.slug); + + const getUsersResponse = await UserRepo.getUsersFollowingPublication(publication.slug); + expect(getUsersResponse).toHaveLength(1); + }); +}); + +// testing with user.test.ts instead of a separate weekly debrief file because +// mutations are done with UserRepo, also because testing WeeklyDebrief requires +// mocking which isn't a priority as of right now +describe('weekly debrief tests:', () => { + test('incrementShoutouts - 1 user, 1 shoutout', async () => { + const users = await UserFactory.create(1); + await UserModel.insertMany(users); + await UserRepo.incrementShoutouts(users[0].uuid); + + // update database + const pub = await PublicationFactory.getRandomPublication(); + await UserRepo.followPublication(users[0].uuid, pub.slug); + + const getUsersResponse = await UserRepo.getUserByUUID(users[0].uuid); + expect(getUsersResponse.numShoutouts).toEqual(1); + }); + + test('incrementBookmarks - 1 user, 1 shoutout', async () => { + const users = await UserFactory.create(1); + const insertOutput = await UserModel.insertMany(users); + await UserRepo.incrementBookmarks(insertOutput[0].uuid); + + // update database + const pub = await PublicationFactory.getRandomPublication(); + await UserRepo.followPublication(users[0].uuid, pub.slug); + + const getUsersResponse = await UserRepo.getUserByUUID(insertOutput[0].uuid); + expect(getUsersResponse.numBookmarkedArticles).toEqual(1); + }); + + test('appendReadMagazine', async () => { + const users = await UserFactory.create(1); + const magazines = await MagazineFactory.create(1); + await UserModel.insertMany(users); + const insertOutput = await MagazineModel.insertMany(magazines); + await UserRepo.appendReadMagazine(users[0].uuid, insertOutput[0].id); + + // update database + const pub = await PublicationFactory.getRandomPublication(); + await UserRepo.followPublication(users[0].uuid, pub.slug); + + const getUsersResponse = await UserRepo.getUserByUUID(users[0].uuid); + expect(getUsersResponse.readMagazines).toHaveLength(1); + }); + + test('appendReadArticle', async () => { + const users = await UserFactory.create(1); + const articles = await ArticleFactory.create(1); + await UserModel.insertMany(users); + const insertOutput = await ArticleModel.insertMany(articles); + await UserRepo.appendReadArticle(users[0].uuid, insertOutput[0].id); + + // update database + const pub = await PublicationFactory.getRandomPublication(); + await UserRepo.followPublication(users[0].uuid, pub.slug); + + const getUsersResponse = await UserRepo.getUserByUUID(users[0].uuid); + expect(getUsersResponse.readArticles).toHaveLength(1); + }); +});