Wrote end-to-end cypress tests

This commit is contained in:
ireic
2020-01-05 02:54:46 +01:00
parent ad74afb628
commit 64b237e046
60 changed files with 3698 additions and 215 deletions

52
api/package-lock.json generated
View File

@@ -1045,6 +1045,58 @@
"capture-stack-trace": "^1.0.0"
}
},
"cross-env": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-6.0.3.tgz",
"integrity": "sha512-+KqxF6LCvfhWvADcDPqo64yVIB31gv/jQulX2NGzKS/g3GEVz6/pt4wjHFtFWsHMddebWD/sDthJemzM4MaAag==",
"dev": true,
"requires": {
"cross-spawn": "^7.0.0"
},
"dependencies": {
"cross-spawn": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz",
"integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==",
"dev": true,
"requires": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
}
},
"path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true
},
"shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"requires": {
"shebang-regex": "^3.0.0"
}
},
"shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true
},
"which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"requires": {
"isexe": "^2.0.0"
}
}
}
},
"cross-spawn": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",

View File

@@ -5,7 +5,7 @@
"license": "MIT",
"scripts": {
"start": "nodemon --exec ts-node --files src/index.ts",
"db-seed": "nodemon --exec ts-node --files src/database/seeds/development/index.ts",
"test:start": "cross-env NODE_ENV='test' DB_DATABASE='jira_test' nodemon --exec ts-node --files src/index.ts",
"pre-commit": "lint-staged"
},
"dependencies": {
@@ -32,6 +32,7 @@
"@types/node": "^12.12.11",
"@typescript-eslint/eslint-plugin": "^2.7.0",
"@typescript-eslint/parser": "^2.7.0",
"cross-env": "^6.0.3",
"eslint": "^6.1.0",
"eslint-config-airbnb-base": "^14.0.0",
"eslint-config-prettier": "^6.7.0",

View File

@@ -2,14 +2,14 @@ import express from 'express';
import { catchErrors } from 'errors';
import { signToken } from 'utils/authToken';
import seedGuestUserEntities from 'database/seeds/guestUser';
import createGuestAccount from 'database/createGuestAccount';
const router = express.Router();
router.post(
'/authentication/guest',
catchErrors(async (_req, res) => {
const user = await seedGuestUserEntities();
const user = await createGuestAccount();
res.respond({
authToken: signToken({ sub: user.id }),
});

View File

@@ -0,0 +1,28 @@
import express from 'express';
import { catchErrors } from 'errors';
import { signToken } from 'utils/authToken';
import resetDatabase from 'database/resetDatabase';
import createTestAccount from 'database/createTestAccount';
const router = express.Router();
router.delete(
'/test/reset-database',
catchErrors(async (_req, res) => {
await resetDatabase();
res.respond(true);
}),
);
router.post(
'/test/create-account',
catchErrors(async (_req, res) => {
const user = await createTestAccount();
res.respond({
authToken: signToken({ sub: user.id }),
});
}),
);
export default router;

View File

@@ -1,5 +1,3 @@
import { sample } from 'lodash';
import { Comment, Issue, Project, User } from 'entities';
import { ProjectCategory } from 'constants/projects';
import { IssueType, IssueStatus, IssuePriority } from 'constants/issues';
@@ -37,7 +35,8 @@ const seedProject = (users: User[]): Promise<Project> =>
});
const seedIssues = (project: Project): Promise<Issue[]> => {
const getRandomUser = (): User => sample(project.users) as User;
const { users } = project;
const issues = [
createEntity(Issue, {
title: 'This is an issue of type: Task.',
@@ -46,9 +45,9 @@ const seedIssues = (project: Project): Promise<Issue[]> => {
priority: IssuePriority.LOWEST,
listPosition: 1,
estimate: 8,
reporterId: getRandomUser().id,
reporterId: users[1].id,
project,
users: [getRandomUser()],
users: [users[0]],
}),
createEntity(Issue, {
title: "Click on an issue to see what's behind it.",
@@ -58,7 +57,7 @@ const seedIssues = (project: Project): Promise<Issue[]> => {
listPosition: 2,
description: 'Nothing in particular.',
estimate: 40,
reporterId: getRandomUser().id,
reporterId: users[2].id,
project,
}),
createEntity(Issue, {
@@ -68,9 +67,9 @@ const seedIssues = (project: Project): Promise<Issue[]> => {
priority: IssuePriority.MEDIUM,
listPosition: 3,
estimate: 15,
reporterId: getRandomUser().id,
reporterId: users[1].id,
project,
users: [getRandomUser()],
users: [users[2]],
}),
createEntity(Issue, {
title: 'You can use markdown for issue descriptions.',
@@ -80,9 +79,9 @@ const seedIssues = (project: Project): Promise<Issue[]> => {
listPosition: 4,
description: '#### Colons can be used to align columns.',
estimate: 4,
reporterId: getRandomUser().id,
reporterId: users[0].id,
project,
users: [getRandomUser()],
users: [users[2]],
}),
createEntity(Issue, {
title: 'You must assign priority from lowest to highest to all issues.',
@@ -91,7 +90,7 @@ const seedIssues = (project: Project): Promise<Issue[]> => {
priority: IssuePriority.HIGHEST,
listPosition: 5,
estimate: 15,
reporterId: getRandomUser().id,
reporterId: users[2].id,
project,
}),
createEntity(Issue, {
@@ -101,9 +100,9 @@ const seedIssues = (project: Project): Promise<Issue[]> => {
priority: IssuePriority.MEDIUM,
listPosition: 6,
estimate: 55,
reporterId: getRandomUser().id,
reporterId: users[1].id,
project,
users: [getRandomUser()],
users: [users[0]],
}),
createEntity(Issue, {
title: 'Try leaving a comment on this issue.',
@@ -112,7 +111,7 @@ const seedIssues = (project: Project): Promise<Issue[]> => {
priority: IssuePriority.MEDIUM,
listPosition: 7,
estimate: 12,
reporterId: getRandomUser().id,
reporterId: users[0].id,
project,
}),
];
@@ -122,16 +121,16 @@ const seedIssues = (project: Project): Promise<Issue[]> => {
const seedComments = (issue: Issue, user: User): Promise<Comment> =>
createEntity(Comment, {
body: "Be nice to each other! Don't be mean to each other!",
issue,
user,
issueId: issue.id,
userId: user.id,
});
const seedGuestUserEntities = async (): Promise<User> => {
const createGuestAccount = async (): Promise<User> => {
const users = await seedUsers();
const project = await seedProject(users);
const issues = await seedIssues(project);
await seedComments(issues[issues.length - 1], project.users[0]);
await seedComments(issues[0], project.users[0]);
return users[0];
};
export default seedGuestUserEntities;
export default createGuestAccount;

View File

@@ -0,0 +1,87 @@
import { Comment, Issue, Project, User } from 'entities';
import { ProjectCategory } from 'constants/projects';
import { IssueType, IssueStatus, IssuePriority } from 'constants/issues';
import { createEntity } from 'utils/typeorm';
const seedUsers = (): Promise<User[]> => {
const users = [
createEntity(User, {
email: 'gaben@jira.test',
name: 'Gaben',
avatarUrl: 'https://i.ibb.co/6RJ5hq6/gaben.jpg',
}),
createEntity(User, {
email: 'yoda@jira.test',
name: 'Yoda',
avatarUrl: 'https://i.ibb.co/6n0hLML/baby-yoda.jpg',
}),
];
return Promise.all(users);
};
const seedProject = (users: User[]): Promise<Project> =>
createEntity(Project, {
name: 'Project name',
url: 'https://www.testurl.com',
description: 'Project description',
category: ProjectCategory.SOFTWARE,
users,
});
const seedIssues = (project: Project): Promise<Issue[]> => {
const { users } = project;
const issues = [
createEntity(Issue, {
title: 'Issue title 1',
type: IssueType.TASK,
status: IssueStatus.BACKLOG,
priority: IssuePriority.LOWEST,
listPosition: 1,
reporterId: users[0].id,
project,
}),
createEntity(Issue, {
title: 'Issue title 2',
type: IssueType.TASK,
status: IssueStatus.BACKLOG,
priority: IssuePriority.MEDIUM,
listPosition: 2,
estimate: 5,
description: 'Issue description 2',
reporterId: users[0].id,
users: [users[0]],
project,
}),
createEntity(Issue, {
title: 'Issue title 3',
type: IssueType.STORY,
status: IssueStatus.SELECTED,
priority: IssuePriority.HIGH,
listPosition: 3,
estimate: 10,
description: 'Issue description 3',
reporterId: users[0].id,
users: [users[0], users[1]],
project,
}),
];
return Promise.all(issues);
};
const seedComments = (issue: Issue, user: User): Promise<Comment> =>
createEntity(Comment, {
body: 'Comment body',
issueId: issue.id,
userId: user.id,
});
const createTestAccount = async (): Promise<User> => {
const users = await seedUsers();
const project = await seedProject(users);
const issues = await seedIssues(project);
await seedComments(issues[0], project.users[0]);
return users[0];
};
export default createTestAccount;

View File

@@ -0,0 +1,9 @@
import { getConnection } from 'typeorm';
const resetDatabase = async (): Promise<void> => {
const connection = getConnection();
await connection.dropDatabase();
await connection.synchronize();
};
export default resetDatabase;

View File

@@ -1,10 +0,0 @@
import faker from 'faker';
import Comment from 'entities/Comment';
const generateComment = (data: Partial<Comment> = {}): Partial<Comment> => ({
body: faker.lorem.paragraph(),
...data,
});
export default generateComment;

View File

@@ -1,69 +0,0 @@
import 'module-alias/register';
import 'reflect-metadata';
import 'dotenv/config';
import { times, sample } from 'lodash';
import { Comment, Issue, Project, User } from 'entities';
import createDatabaseConnection from 'database/connection';
import { createEntity } from 'utils/typeorm';
import generateUser from './user';
import generateProject from './project';
import generateIssue from './issue';
import generateComment from './comment';
const seedUsers = (): Promise<User[]> => {
const users = times(4, () => createEntity(User, generateUser()));
return Promise.all(users);
};
const seedProjects = (users: User[]): Promise<Project[]> => {
const projects = times(2, () => createEntity(Project, generateProject({ users })));
return Promise.all(projects);
};
const seedIssues = (projects: Project[]): Promise<Issue[]> => {
const issues = projects
.map(project =>
times(10, i =>
createEntity(
Issue,
generateIssue({
listPosition: i + 1,
reporterId: (sample(project.users) as User).id,
project,
users: [sample(project.users) as User],
}),
),
),
)
.flat();
return Promise.all(issues);
};
const seedComments = (issues: Issue[]): Promise<Comment[]> => {
const comments = issues.map(issue =>
createEntity(Comment, generateComment({ issue, user: sample(issue.project.users) })),
);
return Promise.all(comments);
};
const seedEntities = async (): Promise<void> => {
const users = await seedUsers();
const projects = await seedProjects(users);
const issues = await seedIssues(projects);
await seedComments(issues);
};
const initializeSeed = async (): Promise<void> => {
try {
const Connection = await createDatabaseConnection();
await Connection.dropDatabase();
await Connection.synchronize();
await seedEntities();
console.log('Seeding completed!');
} catch (error) {
console.log(error);
}
};
initializeSeed();

View File

@@ -1,17 +0,0 @@
import faker from 'faker';
import { sample, random } from 'lodash';
import Issue from 'entities/Issue';
import { IssueType, IssueStatus, IssuePriority } from 'constants/issues';
const generateIssue = (data: Partial<Issue> = {}): Partial<Issue> => ({
title: faker.company.catchPhrase(),
type: sample(Object.values(IssueType)),
status: sample(Object.values(IssueStatus)),
priority: sample(Object.values(IssuePriority)),
description: faker.lorem.paragraph(),
estimate: random(1, 40),
...data,
});
export default generateIssue;

View File

@@ -1,15 +0,0 @@
import faker from 'faker';
import { sample } from 'lodash';
import Project from 'entities/Project';
import { ProjectCategory } from 'constants/projects';
const generateProject = (data: Partial<Project> = {}): Partial<Project> => ({
name: faker.company.companyName(),
url: faker.internet.url(),
description: faker.lorem.paragraph(),
category: sample(Object.values(ProjectCategory)),
...data,
});
export default generateProject;

View File

@@ -1,12 +0,0 @@
import faker from 'faker';
import User from 'entities/User';
const generateUser = (data: Partial<User> = {}): Partial<User> => ({
name: faker.company.companyName(),
avatarUrl: faker.image.avatar(),
email: faker.internet.email(),
...data,
});
export default generateUser;

View File

@@ -4,12 +4,13 @@ import 'reflect-metadata';
import express from 'express';
import cors from 'cors';
import createDatabaseConnection from 'database/connection';
import createDatabaseConnection from 'database/createConnection';
import { authenticateUser } from 'middleware/authentication';
import authenticationRoutes from 'controllers/authentication';
import commentsRoutes from 'controllers/comments';
import issuesRoutes from 'controllers/issues';
import projectsRoutes from 'controllers/projects';
import testRoutes from 'controllers/test';
import usersRoutes from 'controllers/users';
import { RouteNotFoundError } from 'errors';
import { errorHandler } from 'errors/errorHandler';
@@ -37,6 +38,10 @@ const initializeExpress = (): void => {
next();
});
if (process.env.NODE_ENV === 'test') {
app.use('/', testRoutes);
}
app.use('/', authenticationRoutes);
app.use('/', authenticateUser);

View File

@@ -2,51 +2,57 @@
"compilerOptions": {
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"lib": ["dom", "es6", "es2017", "es2019", "esnext.asynciterable"], /* Specify library files to be included in the compilation. */
"target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
"lib": [
"dom",
"es6",
"es2017",
"es2019",
"esnext.asynciterable"
] /* Specify library files to be included in the compilation. */,
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
"sourceMap": true, /* Generates corresponding '.map' file. */
"sourceMap": true /* Generates corresponding '.map' file. */,
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./build", /* Redirect output structure to the directory. */
"outDir": "./build" /* Redirect output structure to the directory. */,
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
"removeComments": true, /* Do not emit comments to output. */
"removeComments": true /* Do not emit comments to output. */,
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
"strictNullChecks": true, /* Enable strict null checks. */
"strictFunctionTypes": true, /* Enable strict checking of function types. */
"strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
"strictPropertyInitialization": false, /* Enable strict checking of property initialization in classes. */
"noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
"alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
"strict": true /* Enable all strict type-checking options. */,
"noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
"strictNullChecks": true /* Enable strict null checks. */,
"strictFunctionTypes": true /* Enable strict checking of function types. */,
"strictBindCallApply": true /* Enable strict 'bind', 'call', and 'apply' methods on functions. */,
"strictPropertyInitialization": false /* Enable strict checking of property initialization in classes. */,
"noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */,
"alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */,
/* Additional Checks */
"noUnusedLocals": true, /* Report errors on unused locals. */
"noUnusedParameters": true, /* Report errors on unused parameters. */
"noImplicitReturns": false, /* Report error when not all code paths in function return a value. */
"noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
"noUnusedLocals": true /* Report errors on unused locals. */,
"noUnusedParameters": true /* Report errors on unused parameters. */,
"noImplicitReturns": false /* Report error when not all code paths in function return a value. */,
"noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,
/* Module Resolution Options */
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
"moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
"baseUrl": "./src",
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
"allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"types": ["node"] /* Type declaration files to be included in compilation. */,
"allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */,
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
@@ -57,11 +63,11 @@
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
"emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
"experimentalDecorators": true /* Enables experimental support for ES7 decorators. */,
"emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */,
/* Advanced Options */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
},
"exclude": ["node_modules"],
"include": ["./src/**/*.ts"]