Skip to content

Commit

Permalink
Merge pull request #67 from my-opencode/alpha
Browse files Browse the repository at this point in the history
Merge Alpha to Main
  • Loading branch information
my-opencode authored Aug 16, 2024
2 parents a2c3a43 + 27a9a3c commit 31987de
Show file tree
Hide file tree
Showing 125 changed files with 10,721 additions and 1,665 deletions.
41 changes: 41 additions & 0 deletions .github/workflows/test.integration.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Integration Tests
run-name: ${{ github.actor }} is running integration tests

on:
push:
paths:
- back/**
- docker-entrypoint-initdb.d/**

jobs:
Integration-Tests:
runs-on: ubuntu-22.04
env:
MYSQL_ROOT_USER: root
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: demodb
MYSQL_USER: demouser
MYSQL_PASSWORD: demopswd
MYSQL_TCP_PORT: 3306
MYSQL_HP: --host=localhost --port=3306
steps:
- name: Copy project directory
uses: actions/checkout@v4
- name: Start MySQL
run: sudo /etc/init.d/mysql start && mysql -e 'CREATE DATABASE ${{ env.MYSQL_DATABASE }};' -u${{ env.MYSQL_ROOT_USER }} -p${{ env.MYSQL_ROOT_PASSWORD }} ${{env.MYSQL_HP}}
- name: Create MySQL user without host
run: sudo mysql -e 'CREATE USER ${{ env.MYSQL_USER }} IDENTIFIED BY "${{ env.MYSQL_PASSWORD }}";' -u${{ env.MYSQL_ROOT_USER }} -p${{ env.MYSQL_ROOT_PASSWORD }} ${{env.MYSQL_HP}}
- name: Apply MySQL schema
run: sudo mysql ${{ env.MYSQL_HP }} -u${{ env.MYSQL_ROOT_USER }} -p${{ env.MYSQL_ROOT_PASSWORD }} ${{ env.MYSQL_DATABASE }} < ${{ github.workspace }}/docker-entrypoint-initdb.d/001-database-model.sql
- name: Populate MySQL
run: sudo mysql ${{ env.MYSQL_HP }} -u${{ env.MYSQL_ROOT_USER }} -p${{ env.MYSQL_ROOT_PASSWORD }} ${{ env.MYSQL_DATABASE }} < ${{ github.workspace }}/docker-entrypoint-initdb.d/002-database-state-insert.sql
- run: echo "Running Integration Tests in ${{ runner.os }} for branch ${{ github.ref }}."
- name: Set Node.js
uses: actions/setup-node@v4
with:
node-version: '22.x'
- name: Install dependencies
run: cd back && npm ci
- name: Launch Integration Tests
run: cd back && npm run test:integration
- run: echo "Test completed with status ${{ job.status }}."
25 changes: 25 additions & 0 deletions .github/workflows/test.unit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Unit Tests
run-name: ${{ github.actor }} is running unit tests

on:
push:
paths:
- back/**
- docker-entrypoint-initdb.d/**

jobs:
Unit-Tests:
runs-on: ubuntu-latest
steps:
- run: echo "Running Unit Tests in ${{ runner.os }} for branch ${{ github.ref }}."
- name: Copy project directory
uses: actions/checkout@v4
- name: Set Node.js
uses: actions/setup-node@v4
with:
node-version: '22.x'
- name: Install dependencies
run: cd back && npm ci
- name: Launch Unit Tests
run: cd back && npm run test:unit
- run: echo "Test completed with status ${{ job.status }}."
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
devdoc
back/node_modules
back-dist
front-dist
back/tmp
back/logs
docker-logs
back/docker-logs
tmp
12 changes: 12 additions & 0 deletions Dockerfile.back.dev
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM node:22
ENV NODE_ENV development
ENV PORT 80

WORKDIR /app
COPY ./back/package*.json /app/
RUN npm ci --omit=dev
COPY ./back-dist /app/

RUN chown -R node:node /app
USER node
CMD ["npm","run","built:start"]
75 changes: 72 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,75 @@
# Product Management Demo

This project's goal is to build a product management back end to showcase one's abilities to fulfill requirements but more importantly to showcase one's work ethic and methodology.
This project is a coding test. \
Instructions are in the [README_en-EN.md](./README_en-EN.md) file. The goal to build a product management back end to showcase coding abilities, decision making and methodologies.

Please switch to branch `alpha`.
Prière d'utiliser la branche `alpha`.
# Getting started

- On a Linux+GNU machine, [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) or [Linux VM](https://dev.to/iaadidev/linux-virtualization-simple-guide-for-new-users-2hdh).
- Clone the repository:
```bash
git clone https://github.com/my-opencode/product-management-demo.git
```
- Open this clone directory:
```bash
cd product-management-demo
```
- Confirm that you have [GNU make](https://www.incredibuild.com/integrations/gnu-make) installed:
```bash
make --version
```
- Run:
```bash
make start-all;
```

## Requirements

- [Docker + compose](https://docs.docker.com/compose/install/).

### Requirements for Local install, tests and serving apps

- [Nodejs 22](https://nodejs.org/en/download/package-manager).
- [NPM 10](https://nodejs.org/en/download/package-manager).

## Using the Makefile

For convenience, I provide a `makefile` with commands for most common actions.

> A Makefile is a text file in which we can define named command collections (later referenced as macro for clarity). \
Macros can be called with `make macro-name` in a terminal.

Try `make help` or open the `makefile` for more information about the macros.
If you do not have the ability to run make commands, simply open the file in a text editor to see which commands are used.

> The macros in the Makefile are for bash and have not been tested outside of a GNU+Linux environment.
On Windows or MacOS I strongly recommend using Linux WSL or a VM.

# Run tests

NPM is required

```bash
make test-back
```
or
```bash
cd back
npm ci
npm run test
```

Unit & Integration tests are run automatically on github when changes are pushed to the back or docker-entrypoint-initdb.d sub directories.

# Planification

See how the project has been planned in [001-planification.md](./doc/001-planification.md).

More:
- List of tasks in [008-tasks.md](./doc/008-tasks.md).
- List of core features in [004-core-features.md](./doc/004-core-features.md).
- List of optional features in [005-optional-features.md](./doc/005-optional-features.md).
- List of RC checks in [006-optional-features.md](./doc/006-release-candidate.md).

More:
- Find the OpenAPI schema in [openapi.yaml](./back/openapi.yaml).
4 changes: 4 additions & 0 deletions back/AppSymbols.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const AppSymbols = {
connectionPool: `connectionPool`
};
export default AppSymbols;
27 changes: 27 additions & 0 deletions back/controllers/categories-get-all.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Response, Request, NextFunction, Express } from "express";
import Category from "../models/category";
import Logger from "../lib/winston";
import renderer from "../views/category-list";
const logger = Logger(`controllers/categories-get-all`);

/**
* GET /categories controller
* Sends a {"data":Category[]} response.
* @param {Request} request Express request object
* @param {Response} response Express response object
* @param {NextFunction} next Express next function
*/
export default async function categoriesGetAll(request: Request, response: Response, next: NextFunction) {
logger.log(`verbose`, `Entering`);
try {
const productList = await Category.list(request.app as Express);
const payload = renderer(productList);
logger.log(`debug`, payload);
logger.log(`verbose`, `Exiting`);
response.status(200).send(payload);
} catch (err) {
logger.log(`error`, err instanceof Error ? err.message : err);
logger.log(`debug`, err instanceof Error ? err.stack : `no stack`);
next(err);
}
}
92 changes: 92 additions & 0 deletions back/controllers/categories-get-all.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { describe, it, before, beforeEach, after, mock, Mock } from "node:test";
import Category from "../models/category";
import { CategoryFromDb } from "../models/category";
import { RichApp } from "../types";
type ListFromDatabase = (app: RichApp) => Promise<CategoryFromDb[]>;
import categoriesGetAll from "./categories-get-all";
import * as assert from "node:assert";
import { Request } from "express";

describe(`Categories get all controller`, function () {
let response: any;
let next: any;
beforeEach(function () {
response.send.mock.resetCalls();
response.status.mock.resetCalls();
next.mock.resetCalls();
});
after(function () {
mock.restoreAll();
});

describe(`No error`, function () {
let mockedProductListFromDb: any;

before(function () {
const mockResult = [{ id: 1, name: `a` } as CategoryFromDb];
Category.list = mock.fn(() => Promise.resolve(mockResult));
mockedProductListFromDb = Category.list as Mock<ListFromDatabase>;
response = {
status: mock.fn((statusCode: number) => response),
send: mock.fn(() => response),
set: mock.fn(() => response),
};
assert.strictEqual(response.status.mock.callCount(), 0);
assert.strictEqual(response.send.mock.callCount(), 0);
next = mock.fn(() => { });
assert.strictEqual(next.mock.callCount(), 0);
assert.strictEqual(mockedProductListFromDb.mock.callCount(), 0);
});
it(`should call Category.list`, async function () {
await categoriesGetAll({} as unknown as Request, response, next).finally(() => {
assert.strictEqual(mockedProductListFromDb.mock.callCount(), 1);
assert.strictEqual(next.mock.callCount(), 0);
});
// await categoriesGetAll({} as unknown as Request, response, next);
});

it(`should call response.status`, async function () {
await categoriesGetAll({} as unknown as Request, response, next).finally(() => {
assert.strictEqual(response.status.mock.callCount(), 1);
assert.strictEqual(next.mock.callCount(), 0);
});
// await categoriesGetAll({} as unknown as Request, response, next);
});
it(`should call response.send`, async function () {
await categoriesGetAll({} as unknown as Request, response, next).finally(() => {
assert.strictEqual(response.send.mock.callCount(), 1)
assert.strictEqual(next.mock.callCount(), 0)
});
// await categoriesGetAll({} as unknown as Request, response, next)
});
});

describe(`With error`, function () {
let mockedProductListFromDb: any;

before(function () {
Category.list = mock.fn(() => Promise.reject(`Oops`));
mockedProductListFromDb = Category.list as Mock<ListFromDatabase>;
response = {
status: mock.fn((statusCode: number) => response),
send: mock.fn(() => response),
set: mock.fn(() => response),
};
assert.strictEqual(response.status.mock.callCount(), 0);
assert.strictEqual(response.send.mock.callCount(), 0);
next = mock.fn(() => { });
assert.strictEqual(next.mock.callCount(), 0);
assert.strictEqual(mockedProductListFromDb.mock.callCount(), 0);
});
it(`should call next`, async function () {
await categoriesGetAll({} as unknown as Request, response, next).finally(() => {
assert.strictEqual(mockedProductListFromDb.mock.callCount(), 1)
assert.strictEqual(response.status.mock.callCount(), 0)
assert.strictEqual(response.send.mock.callCount(), 0)
assert.strictEqual(next.mock.callCount(), 1)
});
// await categoriesGetAll({} as unknown as Request, response, next)
});
});

});
28 changes: 28 additions & 0 deletions back/controllers/default-404.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Request, Response } from "express";
import Logger from "../lib/winston";
import renderer from "../views/generic-error";
const logger = Logger(`controllers/404-handler`);

/**
* 404 controller
* Supports application/json and text responses.
* @param {Request} request Express request object
* @param {Response} response Express response object
*/
export default function default404(req: Request, res: Response) {
logger.log(`verbose`, `Entering`);
try {
const message = `Resource "${req.url}" not found.`;
logger.log(`debug`, message);
logger.log(`verbose`, `Exiting`);
res
.status(404)
.send(renderer(message));
} catch (err) {
logger.log(`error`, err instanceof Error ? err.message : err);
logger.log(`debug`, err instanceof Error ? err.stack : `no stack.`);
// not calling renderer in case it caused the error
// because 404 runs after error catcher.
res.status(500).json({ description: `Unexpected error occured.`, errors: [] });
}
}
26 changes: 26 additions & 0 deletions back/controllers/default-404.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Response, Request } from "express";
import { describe, it, before, mock } from "node:test";
import * as assert from "node:assert";
import default404 from "./default-404";

describe(`Default 404 controller`, function () {
let response: any;
before(function () {
response = {
status: mock.fn((statusCode: number) => response),
send: mock.fn(() => response)
};
assert.strictEqual(response.status.mock.callCount(), 0);
assert.strictEqual(response.send.mock.callCount(), 0);
default404({} as unknown as Request, response as unknown as Response);
});

it(`should call status with code`, function () {
assert.strictEqual(response.status.mock.callCount(), 1);
assert.deepStrictEqual(response.status.mock.calls[0].arguments, [404]);
});

it(`should call send`, function () {
assert.strictEqual(response.send.mock.callCount(), 1);
});
});
27 changes: 27 additions & 0 deletions back/controllers/error-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { NextFunction, Request, Response } from "express";
import Logger from "../lib/winston";
import renderer from "../views/generic-error";
const logger = Logger(`controllers/error-handler`);

/**
* Error controller
* Does not handler 404 errors.
* Supports application/json and text responses.
* Sends a {"description":String, "errors":{[fieldName:string]:string}[]} object.
* @param {Request} request Express request object
* @param {Response} response Express response object
* @param {NextFunction} next Express next function
*/
export default function errorHandler(err: Error, req: Request, res: Response, next: NextFunction) {
if (!err) return next();
const message = `Unexpected error. Please contact our support if the error persists.`;
logger.log(`verbose`, `Entering`);
logger.log(`error`, `Caught error for ${req.method} "${req.url}" — \n${err.message} — \nTrace: ${err.stack}`);
if (res.headersSent) {
logger.log(`debug`, `Headers were sent`);
logger.log(`verbose`, `Exiting without status`);
return res.send(message);
}
logger.log(`verbose`, `Exiting`);
res.status(500).send(renderer(message));
}
Loading

0 comments on commit 31987de

Please sign in to comment.