-
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.
Merge pull request #67 from my-opencode/alpha
Merge Alpha to Main
- Loading branch information
Showing
125 changed files
with
10,721 additions
and
1,665 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,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 }}." |
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,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 }}." |
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 |
---|---|---|
@@ -1,4 +1,9 @@ | ||
devdoc | ||
back/node_modules | ||
back-dist | ||
front-dist | ||
back/tmp | ||
back/logs | ||
docker-logs | ||
back/docker-logs | ||
tmp |
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,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"] |
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 |
---|---|---|
@@ -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). |
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,4 @@ | ||
const AppSymbols = { | ||
connectionPool: `connectionPool` | ||
}; | ||
export default AppSymbols; |
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 @@ | ||
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); | ||
} | ||
} |
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,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) | ||
}); | ||
}); | ||
|
||
}); |
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 @@ | ||
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: [] }); | ||
} | ||
} |
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,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); | ||
}); | ||
}); |
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 @@ | ||
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)); | ||
} |
Oops, something went wrong.