My project is a blogging app, which is a recruitment assignment. It is now finished and working.
Following function handles updating a comment votes, very similar as on StackExchange sites, preventing duplicated votes and ensuring single vote per IP address.
I am not sure how to make the following function unit testable in a functional way. The dependency here is the database provider, which is a TypeORM instance. It involves several database calls and a transaction, which make it difficult for me to make it testable.
I know how to use currying to "inject" simple dependencies such as configuration, but database manager is a complicated object which would be very difficult to mock.
To explain more, there are two tables in the database, Comment
which holds the vote count (votes
) and CommentVote
which holds individual votes for IP address.
Full code here.
export async function changeCommentVote(
commentId: string,
ipAddress: string,
voteDelta: 1 | -1,
) {
const result = await dbDataSource.transaction(async (manager) => {
async function handleExistingVotes(): Promise<Result<statusCodeType>> {
// check if this IP already voted
const existingVote = await manager.findOneBy(CommentVote, {
commentId,
ipAddress,
});
if (existingVote && existingVote.vote === voteDelta) {
// repeated vote in the same direction is prohibited
return error({
code: STATUS_CODES.BAD_REQUEST,
message: 'Already voted',
});
} else if (existingVote) {
// voting with the opossite for the second time (e.g. first up, second down)
// it means that user is withdrawing his vote
await manager.delete(CommentVote, existingVote);
// here the status code are not expressive enough,
// a custom codes should be included in the body, but let's keep it for now
return ok(STATUS_CODES.OK);
} else {
// first vote
await manager.insert(CommentVote, {
commentId,
ipAddress,
vote: voteDelta,
});
return ok(STATUS_CODES.CREATED);
}
}
const resultFromExistingVotes = await handleExistingVotes();
if (isError(resultFromExistingVotes)) {
return resultFromExistingVotes;
}
const updateResult = await manager
.createQueryBuilder()
.from(Comment, 'comment')
.setParameter('delta', voteDelta)
.update({ votes: () => 'votes + :delta' })
.where('id = :id', { id: commentId })
.returning(['id', 'votes'])
.execute();
return updateResult.affected === 1
? ok({
code: resultFromExistingVotes.value,
newVotes: updateResult.raw[0].votes as number,
})
: error({ code: STATUS_CODES.NOT_FOUND });
});
if (isOk(result)) {
// uses websockets to send a realtime response to the browser
commentCallbacks.afterVotingCallback({
commentId,
votes: result.value!.newVotes,
});
}
return result;
}