1
\$\begingroup\$

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;
}
\$\endgroup\$
0

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.