Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable parallel tests in vitest #539

Merged
merged 3 commits into from
Mar 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
33 changes: 18 additions & 15 deletions .drone.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,34 @@ steps:
commands:
- yarn lint

- name: test
- name: build
image: node:16-alpine
when:
event:
- push
branch:
exclude: [prod]
depends_on:
- setup
commands:
- yarn build
- yarn build:cli

- name: test
image: node:16-alpine
when:
event:
- push
depends_on:
- build
commands:
- yarn build:cli
- yarn coverage
environment:
MONGO_URL: mongodb://mongodb:27017/vote-test
REDIS_URL: redis
VITEST_MAX_THREADS: 8
VITEST_MIN_THREADS: 1

# - name: coverage
# image: node:16-alpine
Expand All @@ -51,19 +67,6 @@ steps:
# COVERALLS_GIT_BRANCH: ${DRONE_BRANCH}
# COVERALLS_SERVICE_NUMBER: ${DRONE_BUILD_NUMBER}

- name: build
image: node:16-alpine
when:
event:
- push
branch:
exclude: [prod]
depends_on:
- setup
commands:
- yarn build
- yarn build:cli

- name: server
image: node:16-alpine
detach: true
Expand Down Expand Up @@ -149,4 +152,4 @@ services:

---
kind: signature
hmac: de4f6c10d05a6d14bbd096067143a17bd6a545411b9f2582123e3192260861e1
hmac: 3d344951a86035c2f80e867438ae2f3ba3f9369ac1b6425ed2bf3bcf641ad5ed
2 changes: 1 addition & 1 deletion app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ app.set('views', `./app/views`);
app.set('mongourl', env.MONGO_URL);

mongoose.set('strictQuery', true);
mongoose.connect(app.get('mongourl'));
mongoose.connect(app.get('mongourl'), { dbName: env.MONGO_DB });

if (env.NODE_ENV == 'production') {
raven.config(env.RAVEN_DSN).install();
Expand Down
61 changes: 34 additions & 27 deletions app/controllers/vote.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { RequestHandler } from 'express';
import Client from 'ioredis';
import Redlock from 'redlock';
import Redlock, { ExecutionError } from 'redlock';

import Election from '../models/election';
import Vote from '../models/vote';
Expand All @@ -24,36 +24,43 @@ export const create: RequestHandler = async (req, res) => {
}

// Create a new lock for this user to ensure nobody double-votes
const lock = await redlock.acquire(['vote:' + user._id], 1000);
return Election.findById(req.body.election._id)
.then(async (election) => {
// Election does not exist
if (!election) throw new errors.NotFoundError('election');
try {
const lock = await redlock.acquire(['vote:' + user._id], 1000);
return Election.findById(req.body.election._id)
.then(async (election) => {
// Election does not exist
if (!election) throw new errors.NotFoundError('election');

// Priorities cant be longer then alternatives
if (priorities.length > election.alternatives.length) {
throw new errors.InvalidSTVPrioritiesLengthError(priorities, election);
}
// Priorities cant be longer then alternatives
if (priorities.length > election.alternatives.length) {
throw new errors.InvalidSTVPrioritiesLengthError(
priorities,
election
);
}

// If this is a normal election, then the ballot can only contain 0 or 1 priorities
if (election.type === ElectionTypes.NORMAL && priorities.length > 1) {
throw new errors.InvalidNormalPrioritiesLengthError(priorities);
}
// If this is a normal election, then the ballot can only contain 0 or 1 priorities
if (election.type === ElectionTypes.NORMAL && priorities.length > 1) {
throw new errors.InvalidNormalPrioritiesLengthError(priorities);
}

// Payload has priorites that are not in the election alternatives
const diff = priorities.filter(
(x) => !election.alternatives.includes(x._id)
);
if (diff.length > 0) {
throw new errors.InvalidPriorityError(diff[0], election.title);
}
const vote = await election.addVote(user, priorities);
// Unlock when voted
await lock.release();
// Payload has priorites that are not in the election alternatives
const diff = priorities.filter(
(x) => !election.alternatives.includes(x._id)
);
if (diff.length > 0) {
throw new errors.InvalidPriorityError(diff[0], election.title);
}
const vote = await election.addVote(user, priorities);
// Unlock when voted
await lock.release();

return vote;
})
.then((vote) => res.status(201).json(vote));
return vote;
})
.then((vote) => res.status(201).json(vote));
} catch (ExecutionError) {
throw new errors.DuplicateVoteError();
}
};

export const retrieve: RequestHandler = async (req, res) => {
Expand Down
10 changes: 10 additions & 0 deletions app/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,15 @@ class InvalidElectionTypeError extends HTTPError {
}
}

class DuplicateVoteError extends HTTPError {
constructor() {
super();
this.name = 'DuplicateVoteError';
this.message = "Can't vote twice on the same election";
this.status = 409;
}
}

export default {
InactiveUserError,
AlreadyVotedError,
Expand All @@ -279,6 +288,7 @@ export default {
AlreadyActiveElectionError,
MailError,
NoAssociatedUserError,
DuplicateVoteError,
};

export const handleError = (
Expand Down
63 changes: 38 additions & 25 deletions bin/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import mongoose, { HydratedDocumentFromSchema } from 'mongoose';
import chalk from 'chalk';
import promptly from 'promptly';
import User, { userSchema } from '../app/models/user';
import { parseInt } from 'lodash';
const program = new Command();

function done(err: Error, user: HydratedDocumentFromSchema<typeof userSchema>) {
Expand All @@ -31,35 +32,47 @@ function validator(value: string) {
program
.version('1.0.0')
.command('create-user <username> <cardKey>')
.option('-m, --mode <mode>', 'The mode for the user. [1|2|3]')
.option('-p, --password <password>', 'password for user')
.description('create a new user')
.action((username, cardKey) => {
.action(async (username, cardKey, options) => {
const mongoURL = process.env.MONGO_URL || 'mongodb://localhost:27017/vote';

promptly.prompt(
'Usermode: \n [1] for User \n [2] for Moderator \n [3] for Admin \n Enter mode: ',
{ validator, retry: true },
(modeErr, mode) => {
const modeId = parseInt(mode);
mongoose.set('strictQuery', true);
mongoose.connect(mongoURL, {}, (connectErr) => {
if (connectErr) return done(connectErr, null);
const mongoDbName =
process.env.NODE_ENV == 'test' && process.env.VITEST_WORKER_ID
? `vote-test-${process.env.VITEST_WORKER_ID}`
: null;

promptly.password('Enter password: ', async (pwErr, password) => {
if (pwErr) return done(pwErr, null);
const user = new User({
username: username,
admin: modeId == 3,
moderator: modeId == 2 || modeId == 3,
cardKey: cardKey,
});
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const registeredUser = await User.register(user, password);
done(null, registeredUser);
});
});
}
);
let { mode, password } = options;

if (!['1', '2', '3'].includes(mode)) {
mode = await promptly.prompt(
'Usermode: \n [1] for User \n [2] for Moderator \n [3] for Admin \n Enter mode: ',
{ validator, retry: true }
);
}

const modeId = parseInt(mode);

if (!(password?.length > 0)) {
password = await promptly.password('Enter password: ');
}

mongoose.set('strictQuery', true);
mongoose.connect(mongoURL, { dbName: mongoDbName }, async (connectErr) => {
if (connectErr) return done(connectErr, null);

const user = new User({
username: username,
admin: modeId == 3,
moderator: modeId == 2 || modeId == 3,
cardKey: cardKey,
});
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const registeredUser = await User.register(user, password);
done(null, registeredUser);
});
});

program.parse(process.argv);
4 changes: 4 additions & 0 deletions env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ const env = {
// Host used when binding. Use 0.0.0.0 to bind all interfaces
HOST: process.env.HOST || 'localhost',
MONGO_URL: process.env.MONGO_URL || 'mongodb://localhost:27017/vote',
MONGO_DB:
process.env.NODE_ENV == 'test' && process.env.VITEST_WORKER_ID
? `vote-test-${process.env.VITEST_WORKER_ID}`
: null,
REDIS_URL: process.env.REDIS_URL || 'localhost',
FRONTEND_URL: process.env.FRONTEND_URL || 'http://localhost:3000',

Expand Down
4 changes: 3 additions & 1 deletion test/api/vote.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ describe('Vote API', () => {
request(app)
.post('/api/vote')
.send(votePayload(ctx.activeSTVElection, [ctx.activeSTVAlternative]));
await Promise.all([
const reqs = await Promise.all([
create(),
create(),
create(),
Expand All @@ -388,6 +388,8 @@ describe('Vote API', () => {
]);
const votes = await Vote.find({ election: ctx.activeSTVElection._id });
votes.length.should.equal(1);
reqs.filter((req) => req.status === 201).length.should.equal(1);
reqs.some((req) => req.status === 409).should.be.true;
});

test('should not be possible to vote without logging in', async function (ctx) {
Expand Down
8 changes: 0 additions & 8 deletions test/cleanup.js

This file was deleted.