Skip to content

Commit

Permalink
Initial commit; moving existing progress to new public repo
Browse files Browse the repository at this point in the history
  • Loading branch information
GeordieP committed Oct 3, 2018
0 parents commit ce9b45d
Show file tree
Hide file tree
Showing 154 changed files with 23,371 additions and 0 deletions.
27 changes: 27 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.

# dependencies
/node_modules

# server
/server/.cache
/server/dist
/server/db
/server/src/keys.*

# testing
/coverage

# production
/build

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*
17 changes: 17 additions & 0 deletions ExtraModules.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// use this file to declare any modules that don't have type definitions in order to satisfy
// the TS compiler.

declare module 'waait';
declare module 'classcat';

declare module 'koa-passport';
declare module 'passport-local';
declare module 'bcryptjs';
declare module 'koa';
declare module 'koa-bodyparser';
declare module 'koa-mount';
declare module 'koa-graphql';
declare module 'koa-session';
declare module 'koa-redis';
declare module 'koa-router';
declare module 'mongoose';
109 changes: 109 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# issue tracker

Inspired by Jira, GitHub Issues, Redmine, Mantis, and similar applications, this is an experiment into creating my own alternative, while also exploring a number of technologies I haven't yet tried out (most notably TypeScript, Koa, and GraphQL).

##### This project is **NOT** production ready! It's still very much in the proof of concept / experimentation phase.

![App Preview](./readme-img/app.png)

## Libraries
#### Frontend
- [TypeScript](https://www.typescriptlang.org/)
- [React](https://reactjs.org/)
- [create-react-app](https://github.com/facebook/create-react-app)
- [react-app-rewired](https://github.com/timarney/react-app-rewired)
- [react-router](https://github.com/ReactTraining/react-router)
- [Apollo GraphQL Client](https://www.apollographql.com/)
- [apollo-link-state](https://www.apollographql.com/)
- [PostCSS](https://github.com/postcss/postcss)
- [react-testing-library](https://github.com/kentcdodds/react-testing-library)

#### Backend
- [TypeScript](https://www.typescriptlang.org/)
- [Koa](https://koajs.com/)
- [Passport.js](http://www.passportjs.org/)
- [GraphQL](https://graphql.org/)
- [Redis](https://redis.io/)
- [MongoDB](https://www.mongodb.com/)
- [bcryptjs](https://www.npmjs.com/package/bcryptjs)
- [Parcel](https://parceljs.org/)

### Testing & other tools
- [Jest](https://jestjs.io/) (Unit tests)
- [Cypress](https://www.cypress.io/) (Integration & E2E Tests)
- [stmux](https://github.com/rse/stmux) (Display the output from several node commands in one window)

## Development Setup

### Prerequisites

##### Dev Database

At this stage of development, the application relies on a MongoDB database to be running on port 27017, accessible with the credentials `admin:admin`, as well as a Redis instance on its default port of 6379. While any running MongoDB instance will work, a simple solution using Docker is included in the project (docker is required, and docker-compose is suggested).

Under the directory `server/` you'll find a `docker-compose.yml` file to start Docker containers for these required tools automatically, with the database peristing to a local path of `server/db/`. Also included is mongo express, allowing basic control over the database through an interface served on http://localhost:8081.

The compose file can be used manually, or by using the provided bash scripts - from the `server/` directory, you can run `./start-db` and `./stop-db` to start and stop respectively. Of course, you can alternatively start the docker containers manually through the command line.

##### Keys file for `koa-session`

`koa-session` relies on a keys file, located at `server/src/keys.json`. The file should contain an array of key strings (at least 1). This file is excluded from the git index, so you'll have to create your own.

A Python script is provided to automatically generate a basic keys file for you - From the `server/src/` directory, simply run `python gen-keys.py`


### Running in dev mode

Run `yarn start` or `npm start` to run the app in dev mode. This will run several commands through stmux and give you a "dashboard" view of the following processes;

- [Top left] create-react-app in dev mode serving the app frontend on http://localhost:3000 (requests inside the app are proxied along to localhost:4769)
- [Top right] Jest running unit tests in watch mode
- [Bottom left] Parcel build on backend Koa server
- [Bottom right] Nodemon instance to auto-restart the server on compilation (server runs on port 4769)

![Dev mode preview](./readme-img/dev.png)

## Testing

Unit tests are automatically run during dev mode, but can also be executed using the command `yarn app:test` (or `npm run app:test`).

End to end tests are handled by [Cypress](https://www.cypress.io/). Run the command `yarn stmux:test` to start the issue tracker server, serve the app frontend, and open the Cypress UI. Run the entire e2e test suite by clicking the "Run all specs".

## Project Structure

### Frontend (`src/`)

`index.tsx` does some initial setup of Apollo (notably `apollo-link-state`), sends some initial server requests, and renders `LayoutRoutes.tsx` inside all necessary app-level providers.

`LayoutRoutes.tsx` is stateless, and simply routes all necessary top-level routes to the appropriate **Layout** components.

#### Layouts
Layouts are stateless components that define different ways of organizing the various pieces (or "slots") of the page, and render them with the styles necessary to place them where they belong. Currently there are two layouts; one standard "page" layout, and one "split" layout where a sidebar is rendered next to the content area.

Common slots like the site navigation bar or main content area might be placed or rendered differently between certain layouts. Some elements, such as a sidebar, may only exist in some layouts and completely ignored in others. Layouts render divs representing the slots that make up the page, each one containing react-router routes that decide which components should be rendered.

#### Views

Views are what (usually) get rendered into layout slots. They are stateless, and present all the components relevant to a certain piece of the page (eg. Login view, project list view, project details view, issue details view, etc), and wrap these components in the necessary Apollo queries/mutations.

#### Containers

Containers are re-usable bits of the interface that do something "extra"; they might be stateful, or interact with Apollo queries and mutations.

#### Components

Standard stateless components that simply render markup. Most (though not all) components accept CSS-related props and pass them down to their root element, allowing parents to properly style their children. Some components have their own styles, independent from the rest of the site's control.


### Backend (`server/`)

`server/index.ts` sets up a Koa server that serves REST endpoints for authentication (`auth/register`, `auth/login`, `auth/logout`, `auth/status`), and a `/graphql` endpoint for GraphQL requests. A GET to `/graphql` will serve the GraphiQL IDE interface for playing around with the server's GQL API.

Currently data is stored using MongoDB (via mongoose), but the plan is to switch to PostgreSQL at some point. User authentication is handled by Passport, bcrypt is used on passwords, and user session management is handled using redis via koa-session.


### Tests

Unit tests live inside Component directories along with their implementation files, usually named either `test.tsx` or `ComponentName.test.tsx`. These get picked up by Jest and executed.

[Cypress](https://www.cypress.io/) is used for integration and end-to-end tests. Integration tests live in `cypress/integration/`, and E2E tests live in a sub-directory, `cypress/integration/e2e`. Cypress picks up test files in these locations and allows them to be run from the Cypress UI.
7 changes: 7 additions & 0 deletions config-overrides.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
webpack: (config, env) => {
// postcss - config is in /postcss.config.js
require('react-app-rewire-postcss')(config);
return config;
},
};
3 changes: 3 additions & 0 deletions cypress.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"ignoreTestFiles": "**/examples/*"
}
2 changes: 2 additions & 0 deletions cypress/fixtures/example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{
}
28 changes: 28 additions & 0 deletions cypress/integration/e2e/1-signup-login-flow_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
describe('Sign up and log out', () => {
// first user that signs up to this app is given admin permissions.
it('Create first user and log out', () => {
cy.app_signup('AdminUser', 'test', '[email protected]', 'Admin User');
cy.contains('Log Out').click();
});

it('Create second user and log out', () => {
cy.app_signup('SecondUser', 'test', '[email protected]', 'Second User');
cy.contains('Log Out').click();
});
});

describe('Log in and log out, verify permissions are correct', () => {
it('Log in admin account, verify admin permissions, log out', () => {
cy.app_login('[email protected]', 'test',);
cy.contains('New Project');
cy.contains('Log Out').click();
cy.url().should('eq', 'http://localhost:3000/');
});

it('Log in user account, verify no admin permissions, log out', () => {
cy.app_login('[email protected]', 'test');
cy.contains('New Project').should('not.exist');
cy.contains('Log Out').click();
cy.url().should('eq', 'http://localhost:3000/');
})
});
190 changes: 190 additions & 0 deletions cypress/integration/e2e/2-admin-actions_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
// util
const typing_noDelay = { delay: 0 }
// constants used throughout the test
const RUN_ID = Date.now();

describe('Admin create/edit/delete flows', () => {
it('Log in as admin and create project', () => {
Cypress.Cookies.defaults({ whitelist: [] });
cy.clearCookies();
// whitelist our session cookies for the rest of the test
Cypress.Cookies.defaults({
whitelist: ['koa:sess', 'koa:sess.sig']
});

cy.app_login('[email protected]', 'test');

cy.contains('New Project').click();
cy.get('[placeholder=Name]').type(`${RUN_ID}_Project`, typing_noDelay);
cy.get('[placeholder=Alias]').type(`${RUN_ID}_P, typing_noDelay`);
cy.get('[type=Submit]').click();

cy.url().should('contain', 'localhost:3000/projectDetails');
});

// ISSUES

it('Create issue', () => {
cy.contains('New Issue').click();
const bodyText = 'First issue contents before edit';
cy.app_fillCreateIssueForm(`${RUN_ID}_Issue-1`, bodyText, 'High');
cy.get('[data-testid=createIssue_submit]').click();

// did we navigate?
cy.url().should('contain', '/issues/');

// verify issue contents
cy.get('.IssueView-content').within(() => {
// author username
cy.root().should('contain', 'AdminUser');
// severity
cy.root().should('contain', 'high');
// body text
cy.root().should('contain', bodyText);
});
});

it('Edit issue', () => {
cy.contains('Edit Issue').click();
const bodyText = 'First issue contents after edit';
cy.app_fillEditIssueForm(`${RUN_ID}_Issue-1 EDITED`, bodyText, 'Low');
cy.contains('Save Issue').click();

// wait for issue details to be rendered, signifying the save completed
cy.get('.Issue-details');

// verify issue contents
cy.get('.IssueView-content').within(() => {
// severity
cy.root().should('contain', 'low');
// body text
cy.root().should('contain', bodyText);
});
});

it('Delete issue', () => {
cy.contains('Delete Issue').click();
// did we navigate? we deleted the only issue, and should be back on
// the project details page.
cy.url().should('contain', '/projectDetails');
});

// TASKS

it('Create task', () => {
// Create an issue to work on
cy.contains('New Issue').click();
cy.app_fillCreateIssueForm(
`${RUN_ID}_Issue-2`,
'Issue for testing tasks and comments.'
);
cy.get('[data-testid=createIssue_submit]').click();

// create task
const taskTitle = 'First Task';
const taskBodyText = 'First task content before edit';
cy.app_fillCreateTaskForm(taskTitle, taskBodyText);
cy.get('[data-testid=createTask_submit]').click();

// verify task appeared in task list, and click on it
cy.get('.IssueView-sidebar').contains(taskTitle).click();
// did we navigate?
cy.url().should('contain', '/tasks');

cy.get('.TaskView').within(() => {
// title
cy.root().should('contain', taskTitle);
// body
cy.root().should('contain', taskBodyText);
});
});

it('Edit task', () => {
cy.contains('Edit Task').click();
const taskTitle = 'First Task EDITED';
const taskBodyText = 'First task content edited';
cy.app_fillEditTaskForm(taskTitle, taskBodyText);
cy.contains('Save Task').click();

// wait for edit button to come back, signifying the save action completed
cy.contains('Edit Task');

cy.get('.TaskView').within(() => {
// title
cy.root().should('contain', taskTitle);
// body
cy.root().should('contain', taskBodyText);
});
});

it('Delete task', () => {
cy.contains('Delete Task').click();

// did we navigate?
cy.url().should('not.contain', '/tasks');

cy.get('.IssueView-sidebar').should('contain', 'No tasks.');
});

// COMMENTS

it('Create issue comment', () => {
const commentBody = 'First Comment';
cy.get('[data-testid="createComment_body"]').type(commentBody, typing_noDelay);
cy.contains('Submit Comment').click();

// verify comment exists
cy.get('.MutableComment-commentDetails').should('contain', commentBody);
});

it('Edit issue comment', () => {
cy.contains('Edit Comment').click();
const commentBody = 'First comment after edit';
cy.get('[data-testid=editComment_body]').type(commentBody, typing_noDelay);
cy.contains('Save Comment').click();

// verify comment was updated
cy.get('.MutableComment-commentDetails').should('contain', commentBody);
});

it('Delete issue comment', () => {
cy.contains('Delete Comment').click();
cy.get('.MutableComment-commentDetails').should('not.exist');
});

it('Create task, create comment in task', () => {
const taskTitle = 'Test Task';
cy.app_fillCreateTaskForm(taskTitle, '');
cy.get('[data-testid=createTask_submit]').click();

// verify task appeared in task list, and click on it
cy.get('.IssueView-sidebar').contains(taskTitle).click();

const commentBody = 'Task Comment';
cy.get('[data-testid="createComment_body"]').type(commentBody, typing_noDelay);
cy.contains('Submit Comment').click();

// verify comment exists
cy.get('.MutableComment-commentDetails').should('contain', commentBody);
});

it('Edit task comment', () => {
cy.contains('Edit Comment').click();

const commentBody = 'Task comment after edit';
cy.get('[data-testid=editComment_body]').type(commentBody, typing_noDelay);
cy.contains('Save Comment').click();

// verify comment was updated
cy.get('.MutableComment-commentDetails').should('contain', commentBody);
});

it('Delete task comment', () => {
cy.contains('Delete Comment').click();
cy.get('.MutableComment-commentDetails').should('not.exist');
});

it('Log out', () => {
cy.contains('Log Out').click();
})
});
Loading

0 comments on commit ce9b45d

Please sign in to comment.