-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial commit; moving existing progress to new public repo
- Loading branch information
0 parents
commit ce9b45d
Showing
154 changed files
with
23,371 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
|
||
 | ||
|
||
## 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) | ||
|
||
 | ||
|
||
## 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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"ignoreTestFiles": "**/examples/*" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
{ | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/'); | ||
}) | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}) | ||
}); |
Oops, something went wrong.