Skip to content

Commit

Permalink
Merge pull request #275 from lexicongovernance/develop
Browse files Browse the repository at this point in the history
v2.3.0
  • Loading branch information
diegoalzate authored Mar 22, 2024
2 parents ebc9976 + 9e959f6 commit 3455a0f
Show file tree
Hide file tree
Showing 9 changed files with 334 additions and 27 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "forum",
"version": "2.2.3",
"version": "2.3.0",
"description": "",
"main": "dist/index.js",
"scripts": {
Expand Down
37 changes: 37 additions & 0 deletions src/services/comments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ export function saveComment(dbPool: PostgresJsDatabase<typeof db>) {
return res.status(400).json({ errors: body.error.issues });
}

const canComment = await userCanComment(dbPool, userId, body.data.questionOptionId);

if (!canComment) {
return res.status(403).json({ errors: [{ message: 'User cannot comment on this option' }] });
}

try {
const out = await insertComment(dbPool, body.data, userId);
return res.json({ data: out });
Expand Down Expand Up @@ -138,3 +144,34 @@ export function getCommentsForOption(dbPool: PostgresJsDatabase<typeof db>) {
}
};
}

/**
* Checks whether a user can comment based on their registration status.
* @param {PostgresJsDatabase<typeof db>} dbPool - The PostgreSQL database pool.
* @param {string} userId - The ID of the user attempting to comment.
* @param {string | undefined | null} optionId - The ID of the option for which the user is attempting to comment.
* @returns {Promise<boolean>} A promise that resolves to true if the user can comment, false otherwise.
*/
async function userCanComment(
dbPool: PostgresJsDatabase<typeof db>,
userId: string,
optionId: string | undefined | null,
) {
if (!optionId) {
return false;
}

// check if user has an approved registration
const res = await dbPool
.selectDistinct({
user: db.registrations.userId,
})
.from(db.registrations)
.where(and(eq(db.registrations.userId, userId), eq(db.registrations.status, 'APPROVED')));

if (!res.length) {
return false;
}

return true;
}
37 changes: 37 additions & 0 deletions src/services/likes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ export function saveLike(dbPool: PostgresJsDatabase<typeof db>) {
return res.status(400).json({ errors: ['commentId is required'] });
}

const canLike = await userCanLike(dbPool, userId, commentId);

if (!canLike) {
return res.status(403).json({ errors: [{ message: 'User cannot like this comment' }] });
}

const like = await dbPool.query.likes.findFirst({
where: and(eq(db.likes.commentId, commentId), eq(db.likes.userId, userId)),
});
Expand Down Expand Up @@ -80,3 +86,34 @@ export function deleteLike(dbPool: PostgresJsDatabase<typeof db>) {
}
};
}

/**
* Checks whether a user can like a comment based on their registration status.
* @param {PostgresJsDatabase<typeof db>} dbPool - The PostgreSQL database pool.
* @param {string} userId - The ID of the user attempting to like the comment.
* @param {string} commentId - The ID of the comment to be liked.
* @returns {Promise<boolean>} A promise that resolves to true if the user can like the comment, false otherwise.
*/
async function userCanLike(
dbPool: PostgresJsDatabase<typeof db>,
userId: string,
commentId: string,
) {
if (!commentId) {
return false;
}

// check if user has an approved registration
const res = await dbPool
.selectDistinct({
user: db.registrations.userId,
})
.from(db.registrations)
.where(and(eq(db.registrations.userId, userId), eq(db.registrations.status, 'APPROVED')));

if (!res.length) {
return false;
}

return true;
}
187 changes: 187 additions & 0 deletions src/services/registrationData.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as db from '../db';
import { createDbPool } from '../utils/db/createDbPool';
import { runMigrations } from '../utils/db/runMigrations';
import { insertRegistrationSchema } from '../types';
import { cleanup, seed } from '../utils/db/seed';
import { z } from 'zod';
import {
upsertRegistrationData,
fetchRegistrationFields,
filterRegistrationData,
} from './registrationData';
import { sendRegistrationData } from './registrations';
import { isNotNull } from 'drizzle-orm';

const DB_CONNECTION_URL = 'postgresql://postgres:secretpassword@localhost:5432';

describe('service: registrationData', () => {
let dbPool: PostgresJsDatabase<typeof db>;
let dbConnection: postgres.Sql<NonNullable<unknown>>;
let registrationField: db.RegistrationField | undefined;
let otherRegistrationField: db.RegistrationField | undefined;
let otherOtherRegistrationField: db.RegistrationField | undefined;
let registration: db.Registration | undefined;
let testRegistration: z.infer<typeof insertRegistrationSchema>;
let forumQuestion: db.ForumQuestion | undefined;

beforeAll(async () => {
const initDb = createDbPool(DB_CONNECTION_URL, { max: 1 });
await runMigrations(DB_CONNECTION_URL);
dbPool = initDb.dbPool;
dbConnection = initDb.connection;
// seed
const { events, users, forumQuestions, registrationFields } = await seed(dbPool);

// Define data
forumQuestion = forumQuestions[0];
registrationField = registrationFields[0];
otherRegistrationField = registrationFields[1];
otherOtherRegistrationField = registrationFields[2];

testRegistration = {
userId: users[0]?.id ?? '',
eventId: events[0]?.id ?? '',
status: 'DRAFT',
registrationData: [
{
registrationFieldId: registrationFields[0]?.id ?? '',
value: 'title',
},
{
registrationFieldId: registrationFields[1]?.id ?? '',
value: 'sub title',
},
{
registrationFieldId: registrationFields[2]?.id ?? '',
value: 'other',
},
],
};

// Add test registration data to the db
await dbPool
.update(db.registrationFields)
.set({ questionId: forumQuestion?.id ?? '' })
.where(isNotNull(db.registrationFields.questionOptionType));
registration = await sendRegistrationData(dbPool, testRegistration, testRegistration.userId);
});

test('should update existing records', async () => {
// Call the function with registration ID and registration data to update
const registrationId = registration?.id ?? '';
const registrationFieldId = registrationField?.id ?? '';
const updatedValue = 'updated';

const registrationTestData = [
{
registrationFieldId: registrationFieldId,
value: updatedValue,
},
];

const updatedData = await upsertRegistrationData({
dbPool,
registrationId: registrationId,
registrationData: registrationTestData,
});

// Assert that the updated data array is not empty
expect(updatedData).toBeDefined();
expect(updatedData).not.toBeNull();

if (updatedData) {
// Assert that the updated data has the correct structure
expect(updatedData.length).toBeGreaterThan(0);
expect(updatedData[0]).toHaveProperty('id');
expect(updatedData[0]).toHaveProperty('registrationId', registrationId);
expect(updatedData[0]).toHaveProperty('registrationFieldId', registrationFieldId);
expect(updatedData[0]).toHaveProperty('value', updatedValue);
expect(updatedData[0]).toHaveProperty('createdAt');
expect(updatedData[0]).toHaveProperty('updatedAt');
}
});

test('should return null when an error occurs', async () => {
// Provide an invalid registration id to trigger the error
const registrationId = '';
const registrationFieldId = registrationField?.id ?? '';
const updatedValue = 'updated';

const registrationTestData = [
{
registrationFieldId: registrationFieldId,
value: updatedValue,
},
];

const updatedData = await upsertRegistrationData({
dbPool,
registrationId: registrationId,
registrationData: registrationTestData,
});

// Assert that the function returns null when an error occurs
expect(updatedData).toBeNull();
});

test('should fetch registration fields from the database', async () => {
// Fetch registration fields from the database and call the function
const registrationFieldIds = [
registrationField?.id ?? '',
otherRegistrationField?.id ?? '',
otherOtherRegistrationField?.id ?? '',
];
const result = await fetchRegistrationFields(dbPool, registrationFieldIds);

// Assert that the result is an array and not empty
expect(Array.isArray(result)).toBe(true);
expect(result.length).toBeGreaterThan(0);

// function should neglects registration fields with questionOptionType equal null
expect(result.length).toEqual(2);

// Assert that each item in the result array has the expected properties
result.forEach((item) => {
expect(item).toHaveProperty('registrationFieldId');
expect(item).toHaveProperty('questionId');
expect(item).toHaveProperty('questionOptionType');
});
});

test('should filter registration data based on available registration fields', async () => {
// Query all registration data
const registrationDataQueryResult = await dbPool.query.registrationData.findMany();

// Extract the necessary properties from the query results
const registrationData = registrationDataQueryResult.map(
({ id, registrationFieldId, registrationId, value }) => ({
id,
registrationFieldId,
registrationId,
value,
}),
);

const registrationFieldIds = [
registrationField?.id ?? '',
otherRegistrationField?.id ?? '',
otherOtherRegistrationField?.id ?? '',
];
const registrationFields = await fetchRegistrationFields(dbPool, registrationFieldIds);

// Call the filterRegistrationData function
const filteredData = filterRegistrationData(registrationData, registrationFields);

// Assert that the filtered data contains only the relevant registration data
expect(filteredData).toBeDefined();
expect(filteredData).not.toBeNull();
expect(filteredData).toHaveLength(2);
});

afterAll(async () => {
await cleanup(dbPool);
await dbConnection.end();
});
});
4 changes: 2 additions & 2 deletions src/services/registrationData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export async function upsertRegistrationData({
* @param registrationFieldIds - Array of registration field IDs.
* @returns An array of registration fields.
*/
async function fetchRegistrationFields(
export async function fetchRegistrationFields(
dbPool: PostgresJsDatabase<typeof db>,
registrationFieldIds: string[],
): Promise<
Expand Down Expand Up @@ -146,7 +146,7 @@ async function fetchRegistrationFields(
* @param registrationFields - An array of registration fields.
* @returns Filtered registration data.
*/
function filterRegistrationData(
export function filterRegistrationData(
registrationData: {
id: string;
registrationFieldId: string;
Expand Down
3 changes: 0 additions & 3 deletions src/services/statistics.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { insertVotesSchema } from '../types';
import { cleanup, seed } from '../utils/db/seed';
import { z } from 'zod';
import { executeResultQueries } from './statistics';
import { eq } from 'drizzle-orm';

const DB_CONNECTION_URL = 'postgresql://postgres:secretpassword@localhost:5432';

Expand All @@ -16,7 +15,6 @@ describe('service: statistics', () => {
let dbConnection: postgres.Sql<NonNullable<unknown>>;
let userTestData: z.infer<typeof insertVotesSchema>;
let otherUserTestData: z.infer<typeof insertVotesSchema>;
let cycle: db.Cycle | undefined;
let questionOption: db.QuestionOption | undefined;
let forumQuestion: db.ForumQuestion | undefined;
let user: db.User | undefined;
Expand All @@ -34,7 +32,6 @@ describe('service: statistics', () => {
forumQuestion = forumQuestions[0];
user = users[0];
otherUser = users[1];
cycle = cycles[0];
userTestData = {
numOfVotes: 2,
optionId: questionOption?.id ?? '',
Expand Down
23 changes: 10 additions & 13 deletions src/services/votes.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { runMigrations } from '../utils/db/runMigrations';
import { insertVotesSchema } from '../types';
import { cleanup, seed } from '../utils/db/seed';
import { z } from 'zod';
import { saveVote, getVotesForCycleByUser } from './votes';
import { saveVote, getVotesForCycleByUser, userCanVote } from './votes';
import { eq } from 'drizzle-orm';

const DB_CONNECTION_URL = 'postgresql://postgres:secretpassword@localhost:5432';
Expand Down Expand Up @@ -42,7 +42,12 @@ describe('service: votes', () => {
});

it('should save vote', async () => {
await dbPool.update(db.cycles).set({ status: 'OPEN' }).where(eq(db.cycles.id, cycle!.id));
// accept user registration
await dbPool.insert(db.registrations).values({
status: 'APPROVED',
userId: user!.id ?? '',
eventId: cycle!.eventId ?? '',
});
// Call the saveVote function
const { data: response } = await saveVote(dbPool, testData);
// Check if response is defined
Expand All @@ -57,17 +62,9 @@ describe('service: votes', () => {
expect(response?.updatedAt).toEqual(expect.any(Date));
});

it('should not save vote if cycle is closed', async () => {
// update cycle to closed state
await dbPool.update(db.cycles).set({ status: 'CLOSED' }).where(eq(db.cycles.id, cycle!.id));
// Call the saveVote function
const { data: response, errors } = await saveVote(dbPool, testData);

// expect response to be undefined
expect(response).toBeUndefined();

// expect error message
expect(errors).toBeDefined();
it('should not allow voting on users that are not registered', async () => {
const canVote = await userCanVote(dbPool, otherUser!.id, questionOption!.id);
expect(canVote).toBe(false);
});

it('should not save vote if cycle is upcoming', async () => {
Expand Down
Loading

0 comments on commit 3455a0f

Please sign in to comment.