feat(integration): removed `@example` package and combined it with `@integration`
MODIFY
packages/@integration/package.json
packages/@integration/package.json
{
"name": "@marblejs/integration",
"version": "1.0.0-rc.1",
- "description": "Marble.js shared utils and helpers",
- "main": "index.ts",
+ "description": "Marble.js integration tests",
+ "main": "src/index.ts",
+ "scripts": {
+ "start": "ts-node --project tsconfig.json src/index.ts",
+ "watch:dev": "nodemon --watch src --ext ts --ignore '*.spec.ts' --exec yarn start"
+ },
"private": true,
"repository": {
"type": "git",
"url": "https://github.com/marblejs/marble/issues"
},
"homepage": "https://github.com/marblejs/marble#readme",
- "peerDependencies": {
+ "dependencies": {
"@marblejs/core": "^1.0.0-rc.1",
- "@marblejs/middleware-body": "^1.0.0-rc.1"
+ "@marblejs/middleware-body": "^1.0.0-rc.1",
+ "@marblejs/middleware-logger": "^1.0.0-rc.1",
+ "@types/node": "^10.1.2",
+ "rxjs": "~6.2.2",
+ "typescript": "~3.0.3"
},
"devDependencies": {
- "@marblejs/core": "^1.0.0-rc.1",
- "@marblejs/middleware-body": "^1.0.0-rc.1"
+ "nodemon": "^1.17.3",
+ "ts-node": "^7.0.1"
}
}

DELETE
packages/@integration/src/api.integration.spec.ts
-import * as path from 'path';
-import { of, throwError } from 'rxjs';
-import { filter, map, mapTo, switchMap } from 'rxjs/operators';
-import * as request from 'supertest';
-import {
- use,
- internal,
- combineRoutes,
- Effect,
- EffectFactory,
- HttpError,
- HttpStatus,
- httpListener,
- HttpRequest,
-} from '@marblejs/core';
-import { bodyParser$ } from '@marblejs/middleware-body';
-const { readFile, ContentType } = internal;
-
-const MOCKED_USER_LIST = [{ id: 1 }, { id: 2 }];
-const STATIC_PATH = path.resolve(__dirname, '../../../assets');
-
-const authorize$: Effect<HttpRequest> = request$ =>
- request$.pipe(
- filter(req => req.headers.authorization === 'Bearer test'),
- );
-
-const root$ = EffectFactory
- .matchPath('/')
- .matchType('GET')
- .use(req$ => req$
- .pipe(
- mapTo({ status: 200, body: 'root' }),
- ));
-
-const getUserList$ = EffectFactory
- .matchPath('/')
- .matchType('GET')
- .use(req$ => req$
- .pipe(
- switchMap(() => of(MOCKED_USER_LIST)),
- map(users => ({ body: users }))
- ));
-
-const getUserSingle$ = EffectFactory
- .matchPath('/:id')
- .matchType('GET')
- .use(req$ => req$
- .pipe(
- map(({ params, query }) => ({ body: { params, query } }))
- ));
-
-const postUser$ = EffectFactory
- .matchPath('/')
- .matchType('POST')
- .use(req$ => req$
- .pipe(
- use(authorize$),
- map(req => req.body),
- map(response => ({ body: response }))
- ));
-
-const error$ = EffectFactory
- .matchPath('/error')
- .matchType('GET')
- .use(req$ => req$
- .pipe(
- switchMap(() => throwError(new HttpError('test', HttpStatus.METHOD_NOT_ALLOWED, { test: 'test' }))),
- )
- );
-
-const file$ = EffectFactory
- .matchPath('/static/:dir')
- .matchType('GET')
- .use(req$ => req$
- .pipe(
- map(req => req.params!.dir as string),
- switchMap(readFile(STATIC_PATH)),
- map(body => ({ body }))
- ));
-
-const user$ = combineRoutes('/user', [getUserList$, getUserSingle$, postUser$]);
-
-const api$ = combineRoutes('/api/:version', [root$, file$, error$, user$]);
-
-const app = httpListener({
- middlewares: [bodyParser$],
- effects: [api$],
-});
-
-describe('API integration', () => {
- it('returns 404 when route not found: /', async () =>
- request(app)
- .post('/')
- .expect(404));
-
- it('returns status 200: /api/v1', async () =>
- request(app)
- .get('/api/v1')
- .expect(200, '"root"'));
-
- it('returns error response: /api/v1/error', async () =>
- request(app)
- .get('/api/v1/error')
- .expect(HttpStatus.METHOD_NOT_ALLOWED, {
- error: {
- status: HttpStatus.METHOD_NOT_ALLOWED,
- data: { test: 'test' },
- message: 'test',
- }
- }));
-
- it('returns object: /api/v1/user', async () =>
- request(app)
- .get('/api/v1/user')
- .expect(200, MOCKED_USER_LIST));
-
- it('intercepts "query" and "params" from url: /api/v1/user/:id?filter=all', async () =>
- request(app)
- .get('/api/v1/user/123?filter=all')
- .expect(200, {
- params: { id: '123', version: 'v1' },
- query: { filter: 'all' },
- }));
-
- it('parses body and returns echo for secured route: /api/v1/user', async () =>
- request(app)
- .post('/api/v1/user')
- .set('Authorization', 'Bearer test')
- .send({ test: 'test' })
- .expect(200, { test: 'test' }));
-
- it(`returns static file as ${ContentType.TEXT_HTML}: /api/v1/static/index.html`, async () =>
- request(app)
- .get('/api/v1/static/index.html')
- .expect('Content-Type', ContentType.TEXT_HTML)
- .then(res => expect(res.text).toContain('<h1>Test</h1>')));
-
-});

RENAME
packages/@example/src/app.ts
packages/@integration/src/app.ts
RENAME
packages/@example/src/controllers/api.controller.ts
packages/@integration/src/controllers/api.controller.ts
@@ -2,16 +2,27 @@ import { EffectFactory, HttpError, HttpStatus, combineRoutes } from '@marblejs/c
import { throwError } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { user$ } from './user.controller';
+import { static$ } from './static.controller';
const root$ = EffectFactory
.matchPath('/')
.matchType('GET')
.use(req$ => req$.pipe(
- map(req => ({
- body: `API root @ ${req.url}`,
- })),
+ map(req => req.params.version),
+ map(version => ({ body: `API version: ${version}` })),
));
+const notImplemented$ = EffectFactory
+ .matchPath('/error')
+ .matchType('GET')
+ .use(req$ => req$
+ .pipe(
+ switchMap(() => throwError(
+ new HttpError('Route not implemented', HttpStatus.NOT_IMPLEMENTED, { reason: 'Not implemented' })
+ )),
+ )
+ );
+
const notFound$ = EffectFactory
.matchPath('*')
.matchType('*')
@@ -22,6 +33,6 @@ const notFound$ = EffectFactory
));
export const api$ = combineRoutes(
- '/api/v1',
- [ root$, user$, notFound$ ],
+ '/api/:version',
+ [ root$, user$, static$, notImplemented$, notFound$ ],
);

ADD
packages/@integration/src/controllers/static.controller.ts
+import * as path from 'path';
+import { EffectFactory, combineRoutes, internal } from '@marblejs/core';
+import { map, switchMap } from 'rxjs/operators';
+const { readFile } = internal;
+
+const STATIC_PATH = path.resolve(__dirname, '../../../../assets');
+
+const getFile$ = EffectFactory
+ .matchPath('/:dir')
+ .matchType('GET')
+ .use(req$ => req$
+ .pipe(
+ map(req => req.params!.dir as string),
+ switchMap(readFile(STATIC_PATH)),
+ map(body => ({ body }))
+ ));
+
+export const static$ = combineRoutes(
+ '/static',
+ [ getFile$ ],
+);

RENAME
packages/@example/src/controllers/user.controller.ts
packages/@integration/src/controllers/user.controller.ts
@@ -11,6 +11,15 @@ const getUsers$ = EffectFactory
map(users => ({ body: users })),
));
+const getUser$ = EffectFactory
+ .matchPath('/:id')
+ .matchType('GET')
+ .use(req$ => req$.pipe(
+ map(req => req.params.id),
+ switchMap(Dao.getUserById),
+ map(user => ({ body: user })),
+ ));
+
const postUser$ = EffectFactory
.matchPath('/')
.matchType('POST')
@@ -22,5 +31,5 @@ const postUser$ = EffectFactory
export const user$ = combineRoutes('/user', {
middlewares: [authorize$],
- effects: [getUsers$, postUser$],
+ effects: [getUsers$, getUser$, postUser$],
});

RENAME
packages/@example/src/fakes/auth.fake.ts
packages/@integration/src/fakes/auth.fake.ts
RENAME
packages/@example/src/fakes/dao.fake.ts
packages/@integration/src/fakes/dao.fake.ts
@@ -8,6 +8,10 @@ export namespace Dao {
{ id: '3', firstName: 'Adam', lastName: 'Wayne' },
]);
+ export const getUserById = (id: number | string) => of({
+ id, firstName: 'Bob', lastName: 'Collins'
+ });
+
export const postUser = (data: any) => of({
data,
});

RENAME
packages/@example/src/index.ts
packages/@integration/src/index.ts
RENAME
packages/@example/src/middlewares/auth.middleware.ts
packages/@integration/src/middlewares/auth.middleware.ts
ADD
packages/@integration/test/integration.spec.ts
+import { HttpStatus, internal } from '@marblejs/core';
+import * as request from 'supertest';
+import { app } from '../src/app';
+
+const { ContentType } = internal;
+
+describe('API integration', () => {
+ test('POST returns 404 when route not found: /', async () =>
+ request(app)
+ .post('/')
+ .expect(404));
+
+ test('GET returns status 200: /api/v1', async () =>
+ request(app)
+ .get('/api/v1')
+ .expect(200, '"API version: v1"'));
+
+ test('GET returns status 200: /api/v2', async () =>
+ request(app)
+ .get('/api/v2')
+ .expect(200, '"API version: v2"'));
+
+ // @TODO: fix an error with `*` after placeholder path
+ xtest('GET: /api/v1/foo triggers 404 effect', async () =>
+ request(app)
+ .get('/api/v1/foo')
+ .expect(404, {
+ error: {
+ message: 'Route not found',
+ status: 404,
+ }
+ }));
+
+ test('GET returns error response: /api/v1/error', async () =>
+ request(app)
+ .get('/api/v1/error')
+ .expect(HttpStatus.NOT_IMPLEMENTED, {
+ error: {
+ status: HttpStatus.NOT_IMPLEMENTED,
+ data: { reason: 'Not implemented' },
+ message: 'Route not implemented',
+ }
+ }));
+
+ test('GET returns collection: /api/v1/user', async () =>
+ request(app)
+ .get('/api/v1/user')
+ .set('Authorization', 'Bearer FAKE')
+ .expect(200)
+ .then(({ body }) => {
+ expect(body).toHaveLength(3);
+ }));
+
+ test('GET returns single object: /api/v1/user/10', async () =>
+ request(app)
+ .get('/api/v1/user/10')
+ .set('Authorization', 'Bearer FAKE')
+ .expect(200)
+ .then(({ body }) => {
+ expect(body.id).toEqual('10');
+ }));
+
+ test('parses POST body and returns echo for secured route: /api/v1/user', async () =>
+ request(app)
+ .post('/api/v1/user')
+ .set('Authorization', 'Bearer FAKE')
+ .send({ test: 'test' })
+ .expect(200, { data: {test: 'test' } }));
+
+ test(`returns static file as ${ContentType.TEXT_HTML}: /api/v1/static/index.html`, async () =>
+ request(app)
+ .get('/api/v1/static/index.html')
+ .expect('Content-Type', ContentType.TEXT_HTML)
+ .then(res => expect(res.text).toContain('<h1>Test</h1>')));
+
+});

RENAME
packages/@example/tsconfig.json
packages/@integration/tsconfig.json
feat(integration): removed `@example` package and combined it with `@integration`