RxStack is a realtime object-oriented framework which helps you build a micro service web applications on top of other frameworks like
express
andsocketio
by adding an abstraction layer.
- Prerequisites
- Installation
- Project folder and file structure
- NPM Scripts
- Controllers
- Event Listeners
- Console
- Security
- Servers
- Channels
- Databases
- Testing
- References
RxStack
requires Node v8.0.0
and later. On MacOS and other Unix systems the
Node Version Manager is a good way
to quickly install the latest version of NodeJS and keep up it up to date. You'll also need git installed.
After successful installation, the node, npm and git commands should be available on the terminal
and show something similar when running the following commands:
$ node --version
v8.5.0
$ npm --version
6.1.0
$ git --version
git version 2.7.4
Let's clone the pre-configured skeleton application:
$ git clone https://github.com/rxstack/skeleton.git my-project
$ cd my-project
$ npm install
$ npm run dev
If you now try to access localhost in the browser you should see the welcome page or you can access it via websockets:
const io = require('socket.io-client');
const conn = io('ws://localhost:4000', {transports: ['websocket']});
conn.emit('app_index', null, function (response: any) {
console.log(response); // should output Response object
});
src
- all your code lives hereindex.ts
- application entry fileapp
- application related filesenvironments
- configuration files read more.
test
- all tests filesstatic
- all static filestslint.json
- typescript linter configurationtsconfig.json
- typescript configuration
$ npm run dev
- starts the application in development environment withnodemon
and watches for file changes$ npm run compile
- compiles the source code$ npm run watch
- watching for file changes$ npm run clean
- removes thedist
directory$ npm run lint
- lints the source code usingtslint.json
$ npm run mocha
- runs tests usingmocha.opts
$ npm run coverage
- runs tests withnyc
$ npm test
- runs lint, mocha and coverage$ npm run cli
- runs command line applications$ npm run prod
- starts the application in production environment
A controller is a typescript function you create that reads information from the Request
object
and creates and returns a Response
object. The response could be an HTML page, JSON, XML, a file download,
a 404 error or anything else you can dream up.
The controller executes whatever arbitrary logic your application needs to send a response to the client.
A controller is usually a method inside a controller class:
// my-project/src/app/controllers/lucky.controller.ts
import {Injectable} from 'injection-js';
import {Http, Logger, Request, Response, WebSocket} from '@rxstack/core';
@Injectable()
export class LuckyController {
// Logger is injected via constructor method
constructor(private logger: Logger) { }
@Http('GET', '/lucky/number/:max', 'app_lucky_number')
@WebSocket('app_lucky_number')
async number(request: Request): Promise<Response> {
this.logger.debug('Debugging request params: ', request.params.toObject());
const num: number = Math.floor(Math.random() * Math.floor(request.params.get('max')));
return new Response({num});
}
}
This controller is pretty straightforward:
- a
LuckyController
class is created and @Injectable() annotation is applied. - The
number()
method is created and@http()
and@websocket()
annotations are applied in order to register it in theKernel
. - A
Request
object is passed as a method argument. - A promise of
Response
object is returned.
you need to register
LuckyController
in the controller providers:
// my-project/src/app/controllers/APP_CONTROLLER_PROVIDERS.ts
import {ProviderDefinition} from '@rxstack/core';
import {LuckyController} from './lucky-controller';
export const APP_CONTROLLER_PROVIDERS: ProviderDefinition[] = [
{ provide: LuckyController, useClass: LuckyController }
];
In order to view the result of this controller, you need to map a URL to it via a @http()
decorator.
@Http('GET', '/lucky/number/:max', 'app_lucky_number')
then you can access it via http:
curl http://localhost:3000/lucky/number/10
and setting an event name via a @websocket()
decorator.
@WebSocket('app_lucky_number')
then you can access it using socketio-client:
const io = require('socket.io-client');
const conn = io('ws://localhost:4000', {transports: ['websocket']});
conn.emit('app_lucky_number', {params: {max: 10}}, function (response: any) {
console.log(response); // should output Response object
});
If you throw an exception that extends or is an instance of HttpException
,
RxStack
will use the appropriate HTTP status code.
Otherwise, the response will have a 500 HTTP status code:
// my-project/src/app/controllers/lucky.controller.ts
import {Injectable} from 'injection-js';
import {Http, Logger, Request, Response, WebSocket} from '@rxstack/core';
import {BadRequestException} from '@rxstack/exceptions';
@Injectable()
export class LuckyController {
// ...
async number(request: Request): Promise<Response> {
if (parseInt(request.params.get('max')) < 3) {
throw new BadRequestException('Number should be greater than 3.');
}
// ...
}
}
The Request
object is created from the underlying framework incoming request. It lives only in the controller method.
It has several public properties that return all information you need about the request.
Learn more about Request Object.
The Response
object passes information to the underlying framework to construct the response and send it to the client.
Learn more about Response Object.
Uploading files is supported only over http (at the moment) with express server module.
RxStack
comes with ExpressFileUpload Module
During the execution of a RxStack
application, some event notifications are triggered.
Your application can listen to these notifications and respond to them by executing any piece of code.
RxStack
triggers several events related to the kernel while processing the Request.
Third-party modules may also dispatch events, and you can even dispatch custom events from your own code.
The most common way to listen to an event is to register an event listener:
// my-project/src/app/event-listeners/exception.listener.ts
import {Injectable} from 'injection-js';
import {ExceptionEvent, KernelEvents, Response} from '@rxstack/core';
import {HttpException} from '@rxstack/exceptions';
import {Observe} from '@rxstack/async-event-dispatcher';
@Injectable()
export class ExceptionListener {
@Observe(KernelEvents.KERNEL_EXCEPTION)
async onException(event: ExceptionEvent): Promise<void> {
// make sure it is applied only on LuckyController.number
if (event.getRequest().routeName !== 'app_lucky_number') {
return;
}
// You get the exception object from the received event
const exception = event.getException();
const errMsg = `My error says: ${exception.message}`;
// Customize your response object to display the exception details
const response = new Response(errMsg);
if (exception instanceof HttpException) {
response.statusCode = exception.statusCode;
} else {
response.statusCode = 500;
}
// sends the modified response object to the event
event.setResponse(response);
}
}
you need to register
ExceptionListener
in the application providers:
// my-project/src/app/event-listeners/APP_EVENT_LISTENERS_PROVIDERS.ts
import {ProviderDefinition} from '@rxstack/core';
import {ExceptionListener} from './exception.listener';
export const APP_LISTENERS_PROVIDERS: ProviderDefinition[] = [
// ...
{ provide: ExceptionListener, useClass: ExceptionListener }
];
Your console commands can be used for any recurring task, such as cronjobs, imports, or other batch jobs.
To see the build-in command you can run:
$ npm run cli
$ npm run cli debug:http-metadata
- Prints http metadata for an application$ npm run cli debug:web-socket-metadata
- Prints web socket metadata for an application.
In this article you'll learn how to set up your application's security step-by-step, from configuring your application
and how you load users, to denying access and fetching the User
object.
npm install @rxstack/security --save
Add the following configurations to the environment file:
// my-project/src/environments/environment.ts
// ...
security: {
local_authentication: true,
token_extractors: {
authorization_header: {
enabled: true
}
},
ttl: 300,
secret: 'my_secret',
signature_algorithm: 'HS512'
}
and register the module:
// my-project/src/app/APP_OPTIONS.ts
// ...
import {ApplicationOptions} from '@rxstack/core';
import {environment} from '../environments/environment';
import {SecurityModule} from '@rxstack/security';
export const APP_OPTIONS: ApplicationOptions = {
// ...
imports: [
// ...
SecurityModule.configure(environment.security)
]
};
Learn more about security configurations
The easiest (but most limited) way, is to configure RxStack
to load hardcoded users directly from configurations.
This is called an "in memory" provider, but it's better to think of it as an "in configuration" provider:
- Let's create the
User
model:
// my-project/src/app/models/user.ts
import {EncoderAwareInterface, PlainTextPasswordEncoder, User as BaseUser} from '@rxstack/security';
export class User extends BaseUser implements EncoderAwareInterface {
getEncoderName(): string {
return PlainTextPasswordEncoder.ENCODER_NAME;
}
}
We extend User
from @rxstack/security
and tell the model to use a specific password encoder.
Lear more about password encoders
- Let's add users to
environment.ts
file:
// my-project/src/environments/environment.ts
// ...
users: [
{
username: 'admin',
password: 'admin',
roles: ['ROLE_ADMIN']
},
{
username: 'user',
password: 'user',
roles: ['ROLE_USER']
}
]
We added two users with different roles
ROLE_ADMIN
andROLE_USER
.
- Let's register the in-memory user provider in the application providers:
// my_project/src/app/security/APP_SECURITY_PROVIDERS.ts
import {ProviderDefinition, UserInterface} from '@rxstack/core';
import {InMemoryUserProvider, USER_PROVIDER_REGISTRY} from '@rxstack/security';
import {environment} from '../../environments/environment';
import {User} from '../models/user';
export const APP_SECURITY_PROVIDERS: ProviderDefinition[] = [
{
provide: USER_PROVIDER_REGISTRY,
useFactory: () => {
return new InMemoryUserProvider<UserInterface>(
environment.users,
(data: UserInterface) => new User(data.username, data.password, data.roles)
);
},
deps: [],
multi: true
},
];
register the security providers in the application options:
// my-project/src/app/APP_OPTIONS.ts
// ...
import {ApplicationOptions} from '@rxstack/core';
import {APP_SECURITY_PROVIDERS} from './security/providers';
export const APP_OPTIONS: ApplicationOptions = {
// ...
providers: [
// ...
...APP_SECURITY_PROVIDERS
]
};
Learn more about user providers
### Securing a controller As we successfully set up and configured security module, let's create our first secured controller:
// my-project/src/app/controllers/secured.controller.ts
import {Injectable} from 'injection-js';
import {Http, Request, Response, WebSocket} from '@rxstack/core';
import {ForbiddenException} from '@rxstack/exceptions';
@Injectable()
export class SecuredController {
@Http('GET', '/secured/admin', 'app_secured_admin')
@WebSocket('app_secured_admin')
async adminAction(request: Request): Promise<Response> {
if (!request.token.hasRole('ROLE_ADMIN')) {
throw new ForbiddenException();
}
return new Response('secured admin action');
}
}
you need to register
SecuredController
in the application controller providers:
// my-project/src/app/controllers/APP_CONTROLLER_PROVIDERS.ts
import {ProviderDefinition} from '@rxstack/core';
import {SecuredController} from './secured.controller';
export const APP_CONTROLLER_PROVIDERS: ProviderDefinition[] = [
// ...
{ provide: SecuredController, useClass: SecuredController }
];
As you see in the Request
object we retrieve the security token and check if the logged in user has a certain role.
By default @rxstack/security
is using JWT. The token could be generated on a dedicated authentication server
or in the RxStack
application if local_authentication
is enabled.
Let's obtain the token:
curl -X POST \
http://localhost:3000/security/login \
-H 'content-type: application/json' \
-d '{
"username": "admin",
"password": "admin"
}'
or via websockets:
const io = require('socket.io-client');
const conn = io('ws://localhost:4000', {transports: ['websocket']});
conn.emit('security_login', {params: {username: 'admin', password: 'admin'}}, function (response: any) {
console.log(response.content); // should output {"token": "...", "refreshToken": "..."}
});
Token expiration time is set in the
ttl
option in the security module.
As we now have the token we can try to access app_secured_admin
via http:
curl -X GET \
http://localhost:3000/secured/admin \
-H 'authorization: Bearer your-generated-token'
To access secured controller actions via websocket, you first need to authenticate:
// ...
conn.emit('security_authenticate', {params: {bearer: 'your-generated-token'}}, function (response: any) {
console.log(response.statusCode); // should output 204 or 401
// now you can access the secured action
conn.emit('app_secured_admin', null, function (response: any) {
console.log(response.statusCode); // should output 200
});
});
Learn more about local authentication
If you need to restrict the access on application level then you need to create an authentication listener.
Let's add another action to the SecuredController
:
// my-project/src/app/controllers/secured.controller.ts
import {Injectable} from 'injection-js';
import {Http, Request, Response, WebSocket} from '@rxstack/core';
import {ForbiddenException} from '@rxstack/exceptions';
@Injectable()
export class SecuredController {
// ...
@Http('GET', '/secured/user', 'app_secured_user')
@WebSocket('app_secured_user')
async userAction(request: Request): Promise<Response> {
return new Response('secured user action');
}
}
As you see the userAction
is not secured. Let's create the listener:
// my-project/src/app/listeners/authentication.listener.ts
import {Injectable} from 'injection-js';
import {KernelEvents, RequestEvent} from '@rxstack/core';
import {ForbiddenException} from '@rxstack/exceptions';
import {Observe} from '@rxstack/async-event-dispatcher';
@Injectable()
export class AuthenticationListener {
@Observe(KernelEvents.KERNEL_REQUEST)
async onRequest(event: RequestEvent): Promise<void> {
// make sure route/event name starts with "app_secured_"
if (event.getRequest().routeName.search('^app_secured_') === -1) {
return;
}
// checks whether user is authenticated or not
if (!event.getRequest().token.isAuthenticated()) {
throw new ForbiddenException();
}
}
}
you need to register
AuthenticationListener
in the application listener providers:
// my-project/src/app/listener/APP_EVENT_LISTENERS_PROVIDERS.ts
import {ProviderDefinition} from '@rxstack/core';
import {AuthenticationListener} from './authentication.listener';
export const APP_EVENT_LISTENERS_PROVIDERS: ProviderDefinition[] = [
// ...
{ provide: AuthenticationListener, useClass: AuthenticationListener },
];
and now let's try it:
curl -X GET \
http://localhost:3000/secured/user \
-H 'authorization: Bearer your-generated-token'
As you see RxStack
security module provides powerful and flexible authentication system.
Complete security module documentations
The whole point of RxStack
is staying as a platform-agnostic. A framework's architecture is focused on being applicable
to any kind of server-side solution. Build once, use everywhere!
There are two build-in server modules:
ChannelManager
allows you to add connected users to a channel.
You'll be able to notify specific group of users of certain events.
npm install @rxstack/channels --save
and now let's register it in the application common providers:
// my-project/src/app/APP_COMMON_PROVIDERS.ts
import {ProviderDefinition} from '@rxstack/core';
import {ChannelManager} from '@rxstack/channels';
export const APP_COMMON_PROVIDERS: ProviderDefinition[] = [
// ...
{ provide: ChannelManager, useClass: ChannelManager }
];
To add an authenticated user to a channel we need to observe to AuthenticationEvents.SOCKET_AUTHENTICATION_SUCCESS
event,
and on ServerEvents.DISCONNECTED
event we need to remove the user from the channel.
Note: works only with socket servers
Let's create the listener:
// my-project/src/app/event-listeners/channel-manager.listener.ts
import {Injectable} from 'injection-js';
import {ServerEvents, ConnectionEvent} from '@rxstack/core';
import {Observe} from '@rxstack/async-event-dispatcher';
import {AuthenticationEvents, AuthenticationRequestEvent} from '@rxstack/security';
import {ChannelManager} from '@rxstack/channels';
@Injectable()
export class ChannelManagerListener {
constructor(private channelManager: ChannelManager) { }
@Observe(AuthenticationEvents.SOCKET_AUTHENTICATION_SUCCESS)
async onSocketAuthenticationSuccess(event: AuthenticationRequestEvent): Promise<void> {
// any time user is authenticated via websocket connection then he will be added to the "general" channel.
this.channelManager.channel('general').join(event.request.connection);
}
@Observe(ServerEvents.DISCONNECTED)
async onDisconnect(event: ConnectionEvent): Promise<void> {
// if user exists in the "general" channel then on disconnect he will be removed from it
this.channelManager.channel('general').leave(event.connection);
}
}
Do not forget to register the listener in the application event listener providers.
All authenticated users are added/removed from the general
channel.
Let's create a controller action:
// my-project/src/app/controllers/hello.controller.ts
import {Injectable, Injector} from 'injection-js';
import {Http, InjectorAwareInterface, Request, Response, WebSocket} from '@rxstack/core';
import {ChannelManager} from '@rxstack/channels';
import {ForbiddenException} from '@rxstack/exceptions';
@Injectable()
export class HelloController implements InjectorAwareInterface {
private injector: Injector;
setInjector(injector: Injector): void {
this.injector = injector;
}
@WebSocket('app_say_hello')
async sayHelloAction(request: Request): Promise<Response> {
if (!request.token.isAuthenticated()) {
throw new ForbiddenException();
}
const channelManager = this.injector.get(ChannelManager);
const channel = channelManager.channel('general');
channel.send('say_hello', {'message': 'Hello there'}, (conn) => conn !== request.connection);
return new Response({'number_of_connected_users': channel.length});
}
}
The sayHelloAction
will send a message to all users in that channel except for the current user.
Notice: we implemented
InjectorAwareInterface
in order to getChannelManager
from theInjector
Do not forget to register
HelloController
in the application controller providers
Let's say hello
:
conn.emit('app_say_hello', null, function (response: any) {
console.log(response); // should output: {"number_of_connected_users": 2}
});
Let's subscribe to the say_hello
event:
conn.on('say_hello', function (data: any) {
console.log(data); // should output: Hello there
});
Notice: all connected users should be authenticated!
RxStack
doesn't provide a module to work with the database, but you can add any of your choice.
Below you find some database examples:
TypeORM is Object Relational Mapper
Please read the documentations
We assume that mysql is installed and running on your machine.
Let's install dependencies:
$ npm install --save typeorm mysql
Next step is to add the configurations:
// my-project/src/environments/environment.ts
import {configuration} from '@rxstack/configuration';
// ...
typeorm: {
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'demo',
entities: [
configuration.getRootPath() + '/dist/app/entities/*.js'
],
synchronize: true // set it to false in production
}
now we can create the typeorm connection provider:
// my-project/src/app/APP_COMMON_PROVIDERS.ts
import {ProviderDefinition} from '@rxstack/core';
import {Provider} from 'injection-js';
import {environment} from '../environments/environment';
import {createConnection, Connection as TypeormConnection} from 'typeorm';
import {MysqlConnectionOptions} from 'typeorm/driver/mysql/MysqlConnectionOptions';
const typeormProvider = async function(): Promise<Provider> {
const connection: TypeormConnection = await createConnection(<MysqlConnectionOptions>environment.typeorm);
return { provide: TypeormConnection, useValue: connection};
};
export const APP_COMMON_PROVIDERS: ProviderDefinition[] = [
// ...
typeormProvider()
];
Pay attention how we register async providers
Let's create the entity:
// my-project/src/app/entities/cat.ts
import {Column, Entity, PrimaryGeneratedColumn} from 'typeorm';
@Entity()
export class Cat {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
age: number;
@Column()
breed: string;
}
Let's create the service class:
// my-project/src/app/services/cat.service.ts
import {Injectable} from 'injection-js';
import {Cat} from '../entities/cat';
import {Repository} from 'typeorm';
@Injectable()
export class CatService {
constructor(protected repo: Repository<Cat>) { }
async create(data: any): Promise<Cat> {
const createdCat = await this.repo.merge(this.repo.create(), data);
return await this.repo.save(createdCat);
}
async findAll(): Promise<Cat[]> {
return await this.repo.find();
}
}
we need to register the service in the service providers:
// my-project/src/app/services/APP_SERVICE_PROVIDERS.ts
import {ProviderDefinition} from '@rxstack/core';
import {CatService} from './cat.service';
import {Connection as TypeormConnection} from 'typeorm';
import {Cat} from '../entities/cat';
export const APP_SERVICE_PROVIDERS: ProviderDefinition[] = [
{
provide: CatService,
useFactory: (connection: TypeormConnection) => {
return new CatService(connection.getRepository(Cat));
},
deps: [TypeormConnection]
},
];
APP_SERVICE_PROVIDERS
needs to be registered in the application providers
That's it, we can get the service from anywhere:
// ...
const service = this.injector.get(CatService);
const result = await service.create({
'name': 'amanda', 'age': 5, 'breed': 'Birman'
});
Mongoose provides a straight-forward, schema-based solution to model your application data. It includes built-in type casting, validation, query building, business logic hooks and more, out of the box.
Please read the documentations
We assume that mongodb is installed and running on your machine.
Let's install dependencies:
$ npm install --save mongodb mongoose @types/mongoose
Next step is to add the configurations:
// my-project/src/environments/environment.ts
// ...
mongoose: {
uri: 'mongodb://localhost:27017/test',
options: {
useNewUrlParser: true
}
}
now we can create the mongoose connection provider:
// my-project/src/app/APP_COMMON_PROVIDERS.ts
import {ProviderDefinition} from '@rxstack/core';
import {Connection as MongooseConnection} from 'mongoose';
import mongoose = require('mongoose');
import {Provider} from 'injection-js';
import {environment} from '../environments/environment';
mongoose.Promise = global.Promise;
const mongooseProvider = async function(): Promise<Provider> {
const connection: MongooseConnection = mongoose.createConnection(environment.mongoose.uri, environment.mongoose.options);
return { provide: MongooseConnection, useValue: connection};
};
export const APP_COMMON_PROVIDERS: ProviderDefinition[] = [
// ...
mongooseProvider()
];
Pay attention how we register async providers
Let's create the model:
// my-project/src/app/models/cat.interface.ts
export interface CatInterface {
name: string;
age: number;
breed: string;
}
and the schema:
// my-project/src/app/schemas/cat.schema.ts
import * as mongoose from 'mongoose';
export const CatSchema = new mongoose.Schema({
name: String,
age: Number,
breed: String,
});
Let's create the service class:
// my-project/src/app/services/cat-mongoose.service.ts
import {Injectable} from 'injection-js';
import {Model} from 'mongoose';
import {CatInterface} from '../models/cat.interface';
@Injectable()
export class CatMongooseService {
constructor(protected model: Model<any>) { }
async create(data: any): Promise<CatInterface> {
const result = await this.model.create(data);
return result.toObject ? result.toObject() : result;
}
async findAll(): Promise<CatInterface[]> {
return await this.model.find().lean(true).exec();
}
}
we need to register the service in the service providers:
// my-project/src/app/services/APP_SERVICE_PROVIDERS.ts
import {ProviderDefinition} from '@rxstack/core';
import {CatMongooseService} from './cat-mongoose.service';
import {Connection as MongooseConnection} from 'mongoose';
import {CatSchema} from '../schemas/cat.schema';
export const APP_SERVICE_PROVIDERS: ProviderDefinition[] = [
{
provide: CatMongooseService,
useFactory: (connection: MongooseConnection) => {
return new CatMongooseService(connection.model('Cat', CatSchema, 'cats'));
},
deps: [MongooseConnection]
},
];
APP_SERVICE_PROVIDERS
needs to be registered in the application providers
That's it, we can get the service from anywhere:
// ...
const service = this.injector.get(CatMongooseService);
const result = await service.create({
'name': 'amanda', 'age': 5, 'breed': 'Birman'
});
As you see you can integrate any database framework.
Automated tests are an essential part of the fully functional software product. That is very critical to cover at least the most sensitive parts of your system. In order to achieve that goal, we produce a set of different tests like integration tests, unit tests, functional tests, and so on.
Services are often the easiest files to unit test. Let's create a ValueService
:
// my-project/src/app/services/value.service.ts
export class ValueService {
getValue(): string {
return 'real value';
}
}
and test it:
// my-project/test/unit/services/value.service.spec.ts
import {ValueService} from '../../../src/app/services/value.service';
describe('Unit:ValueService', () => {
it('#getValue should return real value', async () => {
const valueService = new ValueService();
valueService.getValue().should.equal('real value');
});
});
Services often depend on other services, but injecting the real service rarely works well as most dependent services are difficult to create and control. Instead you can mock the dependency, use a dummy value, or create a spy on the pertinent service method.
Let's create a MasterService
which depends on ValueService
:
// my-project/src/app/services/value.service.ts
import {ValueService} from './value.service';
export class MasterService {
constructor(private valueService: ValueService) { }
getValue(): string {
return this.valueService.getValue();
}
}
and test it:
// my-project/test/unit/services/master.service.spec.ts
import {MasterService} from '../../../src/app/services/master.service';
import {ValueService} from '../../../src/app/services/value.service';
const sinon = require('sinon');
describe('Unit:MasterService', () => {
it('#getValue should return fake value', async () => {
const valueService = sinon.createStubInstance(ValueService);
valueService.getValue.returns('fake value');
const masterService = new MasterService(valueService);
masterService.getValue().should.equal('fake value');
});
});
These standard testing techniques are great for unit testing services in isolation.
Integration tests determine if independently developed units of software work correctly when they are connected to each other.
To test these services we need to bootstrap the application and pull them from the Injector
and
if needed we can replace services with stubs.
Let's make our MasterService
and ValueService
services injectable:
import {Injectable} from 'injection-js';
@Injectable()
export class MasterService {
// ...
}
@Injectable()
export class ValueService {
// ...
}
and now we need to register them in service providers:
// my-project/src/app/services/APP_SERVICE_PROVIDERS.ts
import {ProviderDefinition} from '@rxstack/core';
import {MasterService} from './master.service';
import {ValueService} from './value.service';
export const APP_SERVICE_PROVIDERS: ProviderDefinition[] = [
// ...
{
provide: MasterService,
useClass: MasterService
},
{
provide: ValueService,
useClass: ValueService
}
];
and now let's test it:
// my-project/test/integration/services/master.service.spec.ts
import 'reflect-metadata';
import {configuration} from '@rxstack/configuration';
configuration.initialize(configuration.getRootPath() + '/src/environments');
import {MasterService} from '../../../src/app/services/master.service';
import {Application} from '@rxstack/core';
import {Injector} from 'injection-js';
import {APP_OPTIONS} from '../../../src/app/APP_OPTIONS';
describe('Integration:MasterService', () => {
// Setup application
const app = new Application(APP_OPTIONS);
let injector: Injector;
let masterService: MasterService;
before(async () => {
await app.start();
injector = app.getInjector();
masterService = injector.get(MasterService);
});
after(async () => {
await app.stop();
});
it('#getValue should return real value', async () => {
masterService.getValue().should.equal('real value');
});
});
sometimes you need to replace the real service with the mock one:
// my-project/test/integration/services/master.service.spec.ts
import 'reflect-metadata';
import {configuration} from '@rxstack/configuration';
configuration.initialize(configuration.getRootPath() + '/src/environments');
import {MasterService} from '../../../src/app/services/master.service';
import {Application} from '@rxstack/core';
import {Injector} from 'injection-js';
import {ValueService} from '../../../src/app/services/value.service';
import * as _ from 'lodash';
import {APP_OPTIONS} from '../../../src/app/APP_OPTIONS';
const sinon = require('sinon');
describe('Integration:MasterService', () => {
// Setup application
const opt = _.cloneDeep(APP_OPTIONS); // clone it otherwise it will affect other tests
const valueService = sinon.createStubInstance(ValueService);
valueService.getValue.returns('fake value');
// replace the real service
opt.providers.push({
provide: ValueService,
useValue: valueService
});
const app = new Application(opt);
let injector: Injector;
let masterService: MasterService;
before(async () => {
await app.start();
injector = app.getInjector();
masterService = injector.get(MasterService);
});
after(async () => {
await app.stop();
});
it('#getValue should return fake value', async () => {
masterService.getValue().should.equal('fake value');
});
});
Functional tests let you check a controller action response:
- Make a request (http or socket)
- Test the response
- Rinse and repeat
As an example, a test could look like this using request-promise and socket.io-client :
You can use you any other http or socket client
import 'reflect-metadata';
import {configuration} from '@rxstack/configuration';
configuration.initialize(configuration.getRootPath() + '/src/environments');
import {APP_OPTIONS} from '../../../src/app/APP_OPTIONS';
import {Injector} from 'injection-js';
import {Application, ServerManager} from '@rxstack/core';
import {IncomingMessage} from 'http';
const rp = require('request-promise');
const io = require('socket.io-client');
describe('Functional:Controllers:HelloController', () => {
// Setup application
const app = new Application(APP_OPTIONS);
let injector: Injector;
let httpHost: string;
let wsHost: string;
let conn: any;
before(async () => {
await app.start();
injector = app.getInjector();
httpHost = injector.get(ServerManager).getByName('express').getHost();
wsHost = injector.get(ServerManager).getByName('socketio').getHost();
conn = io(wsHost, {transports: ['websocket']});
});
after(async () => {
await conn.close();
await app.stop();
});
it('#sayHello over http should return hello', async () => {
const options = {
uri: httpHost + '/hello',
resolveWithFullResponse: true,
json: false
};
await rp(options)
.then((response: IncomingMessage) => {
const headers = response.headers;
headers['x-powered-by'].should.be.equal('Express');
response['statusCode'].should.be.equal(200);
response['content'].should.be.equal('hello');
})
.catch((err: any) => {
// make sure test fails
true.should.be.false;
})
;
});
it('#sayHello over socket should return hello', (done: Function) => {
conn.emit('app_hello', null, function (response: any) {
response['statusCode'].should.be.equal(200);
response['content'].should.be.equal('hello');
done();
});
});
});
Tip: you can test the response content against JSON schema
For more examples please check the test
folder in the @rxstack/skeleton
application.
Licensed under the MIT license.