Skip to content

Commit

Permalink
Merge pull request #49 from ASparton/feat/43-add-number-of-articles-t…
Browse files Browse the repository at this point in the history
…o-return

feat: min articles count per feed and authentication taken into account on /api/articles
  • Loading branch information
ASparton authored Nov 21, 2023
2 parents 0ac24cd + ab46319 commit ca49f1a
Show file tree
Hide file tree
Showing 8 changed files with 164 additions and 34 deletions.
73 changes: 61 additions & 12 deletions back/__tests__/controllers/articles.test.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,97 @@
import HttpStatusCode from '#types/HttpStatusCode';
import { Article } from '@prisma/client';
import {
deleteAllFeeds,
deleteAllUsers,
exampleArticles,
populateArticles,
populateFeeds,
} from 'prisma/seedingOperatons';
import { database } from 'src/lucia';
import request from 'supertest';
import app from '../../src/app';
import HttpStatusCode from '#types/HttpStatusCode';
import ApiErrors from '~apiErrors';

let authToken = '';

describe('Articles router tests', () => {
beforeAll(async () => {
await deleteAllUsers(database);
});

describe('GET all articles', () => {
beforeAll(async () => {
await populateFeeds(database);
await populateArticles(database);
const res = await request(app).post('/api/users/register').send({
email: '[email protected]',
password: 'mySecretPassword',
username: 'Alexis',
});
authToken = res.body.token;
});

test('GET all articles without keywords', async () => {
test('GET all articles without keywords unauthenticated', async () => {
const res = await request(app).get('/api/articles');
expect(res.statusCode).toStrictEqual(HttpStatusCode.OK_200);
for (const expectedArticle of exampleArticles.filter(
(article) => article.id !== 5
)) {
expect(
(res.body as Article[]).find(
(receivedArticle) => receivedArticle.id === expectedArticle.id
)
).toBeTruthy();
}
});

test('GET all articles with keywords unauthenticated', async () => {
const res = await request(app).get('/api/articles?keyworkds=bitcoin');
expect(res.statusCode).toStrictEqual(HttpStatusCode.OK_200);
for (const expectedArticle of exampleArticles.filter(
(article) => article.id !== 5
)) {
expect(
(res.body as Article[]).find(
(receivedArticle) => receivedArticle.id === expectedArticle.id
)
).toBeTruthy();
}
});

test('GET all articles without keywords', async () => {
const res = await request(app)
.get('/api/articles')
.set('Authorization', `Bearer ${authToken}`);
expect(res.statusCode).toStrictEqual(HttpStatusCode.OK_200);
expect(JSON.parse(JSON.stringify(res.body))).toStrictEqual(
JSON.parse(JSON.stringify(exampleArticles))
);
});

test('GET all articles with ignored params', async () => {
const res = await request(app).get(
'/api/articles?date=2023-10-10&orderBy=desc'
);
const res = await request(app)
.get('/api/articles?date=2023-10-10&orderBy=desc')
.set('Authorization', `Bearer ${authToken}`);
expect(res.statusCode).toStrictEqual(HttpStatusCode.OK_200);
expect(JSON.parse(JSON.stringify(res.body))).toStrictEqual(
JSON.parse(JSON.stringify(exampleArticles))
);
});

test('GET all articles with empty keywords', async () => {
const res = await request(app).get('/api/articles?keywords=');
const res = await request(app)
.get('/api/articles?keywords=')
.set('Authorization', `Bearer ${authToken}`);
expect(res.statusCode).toStrictEqual(HttpStatusCode.OK_200);
expect(JSON.parse(JSON.stringify(res.body))).toStrictEqual(
JSON.parse(JSON.stringify(exampleArticles))
);
});

test('GET all articles with one existing keyword', async () => {
const res = await request(app).get('/api/articles?keywords=bitcoin');
const res = await request(app)
.get('/api/articles?keywords=bitcoin')
.set('Authorization', `Bearer ${authToken}`);
expect(res.statusCode).toStrictEqual(HttpStatusCode.OK_200);
expect(JSON.parse(JSON.stringify(res.body))).toStrictEqual(
JSON.parse(
Expand All @@ -58,9 +105,9 @@ describe('Articles router tests', () => {
});

test('GET all articles with multiple existing keywords', async () => {
const res = await request(app).get(
'/api/articles?keywords=bitcoin;wallet'
);
const res = await request(app)
.get('/api/articles?keywords=bitcoin;wallet')
.set('Authorization', `Bearer ${authToken}`);
expect(res.statusCode).toStrictEqual(HttpStatusCode.OK_200);
expect(JSON.parse(JSON.stringify(res.body))).toStrictEqual(
JSON.parse(
Expand All @@ -70,7 +117,9 @@ describe('Articles router tests', () => {
});

test('GET all articles with non existing keywords', async () => {
const res = await request(app).get('/api/articles?keywords=alexismoins');
const res = await request(app)
.get('/api/articles?keywords=alexismoins')
.set('Authorization', `Bearer ${authToken}`);
expect(res.statusCode).toStrictEqual(HttpStatusCode.OK_200);
expect(res.body).toStrictEqual([]);
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Feed" ADD COLUMN "min_articles_count" INTEGER NOT NULL DEFAULT 3;
13 changes: 7 additions & 6 deletions back/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,14 @@ model Key {
}

model Feed {
id Int @id @default(autoincrement())
url String @unique
articles Article[]
id Int @id @default(autoincrement())
url String @unique
min_articles_count Int @default(3)
articles Article[]
}

model Article {
id Int @id @default(autoincrement())
id Int @id @default(autoincrement())
title String
url String @unique
image_url String?
Expand All @@ -54,8 +55,8 @@ model Article {
source_feed_id Int
source_feed Feed @relation(references: [id], fields: [source_feed_id], onDelete: Cascade)
@@index([source_feed_id])
@@index([title])
@@index([source_feed_id])
@@index([title])
}

model Keyword {
Expand Down
23 changes: 23 additions & 0 deletions back/prisma/seedingOperatons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,17 @@ const exampleFeeds: Feed[] = [
{
id: 1,
url: 'https://cointelegraph.com/rss/tag/bitcoin',
min_articles_count: 3,
},
{
id: 2,
url: 'https://cointelegraph.com/rss/tag/altcoin',
min_articles_count: 2,
},
{
id: 3,
url: 'https://cointelegraph.com/rss/tag/ethereum',
min_articles_count: 1,
},
];

Expand Down Expand Up @@ -98,6 +101,18 @@ export const exampleArticles: Article[] = [
image_url:
'https://cointelegraph.com/magazine/wp-content/uploads/2023/11/nov-18-scaled.jpg',
},
{
id: 5,
title:
'Michael Saylor’s a fan, but Frisby says bull run needs a new guru: X Hall of Flame',
url: 'https://cointelegraph.com/magazine/michael-saylor-fan-dominic-frisby-bull-run-new-guru-x-hall-of-flame/',
content:
'<p style="float:right; margin:0 0 10px 15px; width:240px;"><img src="https://cointelegraph.com/magazine/wp-content/uploads/2023/11/Crypto-X-Hall-of-Flame-Dominic-Frisby-scaled.jpg"></p><p>Bitcoiner Dominic Frisby counts Michael Saylor as a fan… but says we need a new Bitcoin evangalist & narrative to propel the next bull run.</p>',
published: new Date('Tue, 21 Nov 2023 14:30:00 +0000'),
source_feed_id: 3,
image_url:
'https://cointelegraph.com/magazine/wp-content/uploads/2023/11/Crypto-X-Hall-of-Flame-Dominic-Frisby-scaled.jpg',
},
];

export async function populateUser(
Expand Down Expand Up @@ -141,6 +156,14 @@ export async function populateArticles(
console.log('Article population finished.');
}

export async function deleteAllUsers(
prismaClient: PrismaClient<Prisma.PrismaClientOptions, never, DefaultArgs>
) {
console.log('Deleting all Feed and Article data...');
await prismaClient.user.deleteMany();
console.log('Feed and Article data deleted.');
}

export async function deleteAllFeeds(
prismaClient: PrismaClient<Prisma.PrismaClientOptions, never, DefaultArgs>
) {
Expand Down
6 changes: 3 additions & 3 deletions back/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import 'express-async-errors';

import cors from 'cors';

import articles from '@controllers/articles';
import auth from '@controllers/auth';
import users from '@controllers/users';
import articles from '@controllers/articles';

import { errorHandler, isAuthenticated, logger } from '~middlewares';
import { authenticationRequired, errorHandler, logger } from '~middlewares';

const app = express();

Expand All @@ -16,7 +16,7 @@ app.use(express.json());

app.use(logger);

app.use('/api/users/', auth, isAuthenticated, users);
app.use('/api/users/', auth, authenticationRequired, users);
app.use('/api/articles', articles);

app.use(errorHandler);
Expand Down
31 changes: 21 additions & 10 deletions back/src/controllers/articles.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
import express from 'express';
import ApiErrors, { APIError } from '~apiErrors';
import HttpStatusCode from '#types/HttpStatusCode';
import AllArticlesDTO from '#types/dto/articles/AllArticlesDTO';
import OneArticleDTO from '#types/dto/articles/OneArticleDTO';
import { findArticleById, findArticles } from '../database/articles';
import { Article } from '@prisma/client';
import express from 'express';
import ApiErrors, { APIError } from '~apiErrors';
import { isAuthenticated } from '~middlewares';
import {
findArticleById,
findArticlesByKeywords,
findCountRestrictedArticles,
} from '../database/articles';

const controller = express.Router();

controller.get('/', async (req, res) => {
let keywords: string[] = [];
const queryParams = AllArticlesDTO.safeParse(req.query);
if (queryParams.success)
keywords = getKeywordsFromQueryParam(queryParams.data.keywords);
controller.get('/', isAuthenticated, async (req, res) => {
let articlesFound: Article[] = [];

if (req.cookies._isAuth) {
let keywords: string[] = [];
const queryParams = AllArticlesDTO.safeParse(req.query);
if (queryParams.success)
keywords = getKeywordsFromQueryParam(queryParams.data.keywords);

articlesFound = await findArticlesByKeywords(keywords);
} else articlesFound = await findCountRestrictedArticles();

const articles = await findArticles(keywords);
return res.status(HttpStatusCode.OK_200).send(articles);
return res.status(HttpStatusCode.OK_200).send(articlesFound);
});

controller.get('/:id', async (req, res) => {
Expand Down
22 changes: 20 additions & 2 deletions back/src/database/articles.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Article, Prisma } from '@prisma/client';
import { Article, Feed, Prisma } from '@prisma/client';
import { database } from '~lucia';

export async function findArticles(keywords: string[]): Promise<Article[]> {
export async function findArticlesByKeywords(
keywords: string[],
): Promise<Article[]> {
// Build keyword filter query
const keywordFilters = keywords.map((keyword) => {
return {
Expand All @@ -22,10 +24,26 @@ export async function findArticles(keywords: string[]): Promise<Article[]> {
return await database.article.findMany(query);
}

export async function findCountRestrictedArticles(): Promise<Article[]> {
let articlesFound: Article[] = [];
const feeds = await database.feed.findMany({ include: { articles: true } });
for (const feed of feeds)
articlesFound = articlesFound.concat(await getMinArticlesCountOfFeed(feed));
return articlesFound;
}

export async function findArticleById(id: number): Promise<Article | null> {
return await database.article.findUnique({
where: {
id: id,
},
});
}

async function getMinArticlesCountOfFeed(feed: Feed): Promise<Article[]> {
return await database.article.findMany({
where: { source_feed_id: feed.id },
orderBy: { published: 'asc' },
take: feed.min_articles_count,
});
}
28 changes: 27 additions & 1 deletion back/src/middlewares.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function logger(req: Request, _: Response, next: NextFunction) {
/**
* Only pass to the next middleware if the request is authenticated.
*/
export async function isAuthenticated(
export async function authenticationRequired(
req: Request,
res: Response,
next: NextFunction,
Expand All @@ -42,6 +42,32 @@ export async function isAuthenticated(

next();
}

/**
* Get user info if authenticated and go to next handler
*/
export async function isAuthenticated(
req: Request,
res: Response,
next: NextFunction,
) {
console.log('[AUTH] can be authenticated endpoint middleware');
const handler = auth.handleRequest(req, res);
const session = await handler.validateBearerToken();
if (!req.cookies) req.cookies = {};
req.cookies._isAuth = session !== null; // Provide info to next handler that user is authenticated or not

if (session !== null) {
// Set user and session info for convenience
req.lucia = {
sessionId: session.sessionId,
user: session.user,
};
}

next();
}

/**
* Handle errors raised by the controllers.
*/
Expand Down

0 comments on commit ca49f1a

Please sign in to comment.