Micro Services

REX de la réalisation d'un service

d'envoi de sms

Les objectifs

  • Microservice
    • inspiration de MGDIS
  • Utiliser au maximum les technos mise en oeuvre
    • pouvoir utiliser le plein potentiel
    • identifier les difficultés
  • Pouvoir extrapoler pour d'autres services
    • notamment les référentiels

Les Technos et outils

Serveur

Base de données

UI

Environnement

Autres outils

  • npm (ou yarn, gestion de package)
  • n (gestion de version de node)
  • nodemon (monitoring de changement des sources)
  • mocha (ou un autre environnement de test)
  • webpack (builder de bundles)

Autres librairies

Gestion des sources

L'environnement de développement

Éviter le syndrôme
"chez moi ça marche"

Les modules npm

  • Ne rien installer en global (cf blog)
    • prohiber l'utilisation du
      # npm install -g xxx
      utiliser exclusivement
      # npm install -save[-dev]
    • certain CLI n'aime pas être que local
      # export PATH=$PATH:./node_modules/.bin
  • exception pour les générateurs qui ne servent que pour initialiser un projet

La base de données

  • Docker pour faire tourner la base de données sans installation (cf dépôt)
  • Des données pour chaque type de déploiement
    • test, développement,...
  • Switch facile (voir automatique) lors des tests
  • Plus léger qu'une VM

La base de données

Utilisation de docker

#!/usr/bin/env bash
mode=`docker ps|grep mongo_myapp|cut -d _ -f 4`
if [[ $mode = test || $mode = dev ]]
then
  echo "kill $mode"
  docker kill mongo_myapp_$mode > /dev/null 2>&1
fi

if [[ $mode = test ]]
then
  docker start mongo_myapp_dev > /dev/null 2>&1
  echo "now it's dev "
else
  docker start mongo_myapp_test > /dev/null 2>&1
  echo "now it's test "
fi
$ docker run -d \
    --name mongo_myapp_dev \
    -p 27017:27017 \
    -e MONGODB_USERNAME=myusernamefordev \
    -e MONGODB_PASSWORD=mypasswordfordev \
    -e MONGODB_DBNAME=mydbfordev \
    mongo_micro

Autres ressources

Le multi tenant

Implémentations possibles

  1. duplication de service
  2. une base par tenant
  3. une collection (table) par tenant
  4. chaque objet contient l'id de son tenant

Configurations possibles

  • 4 > 3
  • client voulant que ses données soient isolées
    ou client gros consommateur
    • 1+2 on duplique le service pour lui et on le met sur une base dédiée 

Le design de l'API

Découverte du service

  • GET /admin retourne l'information générale du service (title + description) et dans l'entête Link une liste d'urls permettant de découvrir le service (les liens ci-après)

  • GET /admin/status retourne l'état courant du service et de ses dépendances (base de données,...)

  • GET /admin/version retourne la version du service

  • GET /admin/license retourne la license d'exploitation du service

  • GET /admin/roles retourne les différents rôles gérés par le service

  • GET /admin/swagger.json retourne la définition swagger de l'API complète du service

Administration des tenants

CRUD RESTful sur les tenants

  • GET /admin/tenants récupération de la liste des tenants

  • POST /admin/tenants création d'un tenant

  • GET /admin/tenants/{tenantId} récupération d'un tenant

  • PUT /admin/tenants/{tenantId} mise à jour d'un tenant

  • PATCH /admin/tenants/{tenantId} mise à jour partiel d'un tenant

  • DELETE /admin/tenants/{tenantId} suppression d'un tenant

     

Configuration des tenants

CRUD RESTful sur les tenants

  • configuration "privée" uniquement accessible à l'administrateur du service
    /admin/tenants/{tenantId}/settings

  • configuration "publique" accessible au manager du tenant
    /{tenantId}/settings

     

     

Extras des tenants

La configuration du tenant accessible au manager peut aussi comprendre :

  • i18n : /{tenantId}/i18n/{lang} : permet de modifier certains textes en fonction du tenant et de la langue
  • authentication : /{tenantId}/authentication : permet de définir les paramètres du système d'authentification pour le tenant
  • roles : /{tenantId}/roles/mapping : permet de définir le mapping entre les groupes issus de l'Account-Management et les rôles du service du tenant
  • status : /{tenantId}/status : permet de connaître l'état de fonctionnement du tenant
  • ...

Swagger

Modéliser l'API

Historique

  • Swagger 2
  • OpenAPI 3

Intérêt

  • Fournit le modèle de l'API proposée par le service 👍👍
  • Permet de générer le squelette du serveur
    • contrôleur & router
    • mock
    • test

Structure d'un projet swagger

  • /api
    • /controllers
    • /helpers
    • /mocks
    • /swagger
      • swagger.yaml
  • /config
    • default.yaml
  • /test
  • app.js
  • package.json

Bagpipe

  • moteur de flux enchaînant des traitements de données
  • configuration > programmation
  • enchaîner ou paralléliser des appels
    • à des webservices
    • à des fonctions personnalisées

swagger utilise

Pipe swagger définit dans default.yaml

# swagger configuration file

# values in the swagger hash are system configuration for swagger-node
swagger:

  fittingsDirs: [ api/fittings ]
  defaultPipe: null
  swaggerControllerPipe: swagger_controllers  # defines the standard processing pipe for controllers

  # values defined in the bagpipes key are the bagpipes pipes and fittings definitions
  # (see https://github.com/apigee-127/bagpipes)
  bagpipes:

    _router:
      name: swagger_router
      mockMode: false
      mockControllersDirs: [ api/mocks ]
      controllersDirs: [ api/controllers ]

    _swagger_validate:
      name: swagger_validator
      validateResponse: true

    # pipe for all swagger-node controllers
    swagger_controllers:
      - onError: json_error_handler
      - cors
      - swagger_security
      - _swagger_validate
      - express_compatibility
      - _router


    # pipe to serve swagger (endpoint is in swagger.yaml)
    swagger_raw:
      name: swagger_raw

Pipe swagger

cors

security

validate

express compatibility

router

security  handler

controllers

Security

On ajoute les contrôleurs d'accès

  • Bearer basé sur jwt
  • Basic basé sur usr+pwd
securityDefinitions:
  Bearer:
    description: |
       For accessing the API a valid JWT token must be passed in all the queries in
       the 'Authorization' header.

       A valid JWT token is generated by the *Account Management* after giving a
       valid user & password.

       The following syntax must be used in the '**Authorization**' header : **Bearer xxxxxx.yyyyyyy.zzzzzz**
    type: apiKey
    name: Authorization
    in: header
  Basic:
    description: For accessing the API a valid username & password must be passed in all the queries header.
    type: basic
const configSwagger = {
  appRoot: __dirname, // required config
  swaggerSecurityHandlers : {
    Bearer: (req, authOrSecDef, authorizationHeader, callback) => { ... },
    Basic: (req, authOrSecDef, authorizationHeader, callback) => { ... }
  }
};
SwaggerExpress.create(configSwagger, (err, swaggerExpress) => {

Controlers

On ajoute les contrôleurs

  • À chaque famille d'opérations un contrôleur
  • À chaque opération une fonction

Ajout d'un middleware

dans le pipe

    _middlewareConfigured:
      name: myMiddleware
      myParameter: 42    

    swagger_controllers:
      - onError: json_error_handler
      - cors
      - myMiddleware
      - _middlewareConfigured
      - swagger_security
      - _swagger_validate
      - express_compatibility
      - _router
module.exports = (cfg) => (context, next) => {
  console.log(cfg);
  //...
  if (err) {
    const error = Error('unknow');
    error.statusCode = 404;
    error.code = "UNKNOW-ERROR";
    next(error);
  }
  else {
    next(); // ou next(null, result);
  }
};
{ name: 'myMiddleware', input: undefined }
{ name: 'myMiddleware', myParameter: 42 }

contexte d'exécution contient notamment

 context.request 
 context.response

La callback à appeler pour passer à la suite du pipe

Ajouts d'autres middlewares

dans le pipe

  • préchargement du tenant
  • désactivation automatique d'un tenant
  • ...
    # pipe for all swagger-node controllers
    swagger_controllers:
      - onError: json_error_handler
      - cors
      - tenantLoader
      - tenantDisabler
      - swagger_security
      - _swagger_validate
      - authorizationSetter
      - express_compatibility
      - _router

Ajouter des paramètres

dans swagger.yaml

/{tenant}/settings:
    x-swagger-router-controller: tenantSettings
    get:
      summary: Get the settings
      description: Get the settings of the tenant
      tags:
        - Management
        - API
        - Tenant
      operationId: getTenantSettings
      x-tenantLoader: tenant
      x-tenantDisabler: true
      security:
        - Bearer: []
        - Basic: []
      x-allowedRoles:
        roles: [adm, mng]
      produces:
        - application/json
      parameters:
        - name: tenant
          in: path
          description: Le nom du tenant
          required: true
          type: string

Préchargement de tenant

  • Charger le tenant lorsque c'est possible
  • Disposer du tenant pour les middlewares suivants
module.exports = () => (context, next) => {
  const swagger = context.request.swagger;
  const tenantPrm = swagger.operation['x-tenantLoader'];
  const tenantId = tenantPrm && swagger.params[tenantPrm] && swagger.params[tenantPrm].value || null;
  if(tenantId !== null) {
    tenants.get(tenantId)
      .then(tenant => {
        createParam(context.request, "tenant", { value: tenant, err: null });
        next();
      })
      .catch(err => {
        error(`unknow tenant "${tenantId}"`);
        createParam(context.request, "tenant", { value: null, err });
        next();
      });
  }
  else {
    next();
  }
};
  /{tenant}/settings:
    get:
      x-tenantLoader: tenant
  /{tutu}/settings:
    get:
      x-tenantLoader: tutu

Express vs. Bagpipe

Différence entre middlewares

const myMiddleware =
(req, res, next) => {
  //...
  next();
}
module.exports = 
(cfg) => (context, next) => {
  //...
  next();
};
const express = require('express');
const app = express();

app.use(myMiddleware);
    _middlewareConfigured:
      name: myMiddleware
      myParameter: true    

    swagger_controllers:
      ...
      - myMiddleware
      - _middlewareConfigured
      ...
const myMiddleware =
(cfg) => (req, res, next) => {
  //...
  next();
}
const cfg = { /*...*/ }
app.use(myMiddleware(cfg));
app.use('/path/', myMiddleware);

Validation des données de l'API

Swagger generator

est-ce si intéressant ?

Le générateur de swagger est-il utile ?

Qu'est-ce que cela apporte au final ?

  • génère le projet du serveur 😊
  • valide les entrées et sortie 😊
    (en fait une simple ligne avec ajv)
  • impose sa CLI 😞
    (pour exécuter et mettre à jour le serveur, nodemon le fait aussi)
  • ​utilise bagpipe 😞
    (système magique pas plus pratique que les middlewares express)
  •  ne sait pas travailler avec la configuration 😞
    (veut modifier la configuration qui est un objet immutable !)
  • ne prend pas encore en charge OpenAPI 3 😞

Qui gagne la guerre des CLI ?

Chaque outils propose son CLI

(swagger, ng, node, mocha,...)

Il faut revenir à la racine : node

=> donc pas de swagger-generate

Ils jouent à Highlander

Retour aux sources

Utiliser Node en direct

Quels middlewares

  • Gérer l'accès par jwt et les roles
  • Effectuer le préchargement du tenant
  • Désactiver automatiquement le tenant

Title Text

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin urna odio, aliquam vulputate faucibus id, elementum lobortis felis. Mauris urna dolor, placerat ac sagittis quis.

Les logs

avec lib debug

Besoins de logs

  • avoir la date
  • identifier la provenance
    • chaque service doit avoir son identifiant
  • avoir différents niveaux
    • gravité
      trace, debug, info, warning, error,...
    • fonctionnel
      user, settings, models,...
$ npm install --save debug

Installation

Exemple de la lib debug

Utilisation

const debug = require('debug')("myapp:db");
const error = require('debug')("myapp:error:db");
//...
const errorHandler = err => {
  error(`Mongoose default connection error: ${err}`);
};
const disconnectHandler = () => {
  debug('Mongoose default connection disconnected');
};
$ DEBUG="myapp:*" node app.js
...
myapp:db Mongoose default connection disconnected +34ms
...
myapp:error:db Mongoose default connection error: unable to connect +121ms
...

Configuration

Comment...

Configuration

Qu'est-ce que c'est ?

  • définir des valeurs de paramètres
  • initialiser le service

Exemple de la lib config

Cette librairie permet de :

  • gérer les différents environnements
    • test
    • dev
    • prod
    • machine
  • hiérarchique
    (système de poupées russes)
  • json ou yaml
    (le second est préférable car on peut y mettre pleins de commentaires)
const debug = require('debug')("myapp:info");
const express = require('express');
const config = require('config');

const app = express();
const port = config.get('port');
app.listen(port);
debug(`try this: curl http://127.0.0.1:${port}/`);
# port du service
port: 10010
{
  "port": 80
}

/config/default.yaml

/config/test.json

/app.js

$ DEBUG="myapp:*" node app.js

myapp:info try this: curl http://127.0.0.1:10010/ +34ms
^C

$ DEBUG="myapp:*" node test
...
myapp:info try this: curl http://127.0.0.1:80/ +32ms
...

Choisir la configuration

  • Depuis le code
process.env.NODE_ENV = 'test';
NODE_ENV=generateKey node test/api/helpers/security/generateAuthorization.js
  • Depuis la ligne de commande

Variables d'environnement

  • Simplicité pour s'adapter à ... l'environnement
  • Particulièrement adapté aux containers docker
const db_name = process.env.HOST || "127.0.0.1";
//...
process.env.NODE_ENV = 'test';

Utilisation

Dans le code

$ docker run -d \
    --name my_app_run \
    -e HOST=mymicroservice.com \
    my_app

Pour lancer le container

Stratégie de configuration

Port du serveur

  1. variable d'environnement PORT
    ou si pas définie
  2. variable de configuration port
    ou si pas définie
  3. "10010"

 

Host du serveur

  1. variable d'environnement HOST
    ou si pas définie
  2. variable de configuration host
    ou si pas définie
  3. "127.0.0.1"​ 
//...
const host = process.env.HOST || config.get('host') || "127.0.0.1";
const port = process.env.PORT || config.get('port') || "10010";
app.listen(port);
debug(`try this: curl http://${host}:${port}/`);
//...

Mix Var.Env. & Config.

Directement dans le code

{
  "port": "PORT",
  "host": "HOST"
}

Mix Var.Env. & Config.

Dans la config : custom-environment-variables.json

//...
const host = config.get('host') || "127.0.0.1";
const port = config.get('port') || "10010";
app.listen(port);
debug(`try this: curl http://${host}:${port}/`);
//...

Dans le code (rien ne change)

Ne pas oublier

  • l'ordre de priorité
    • lorsque plusieurs façons d'initialiser
    • lors de la définition des fichiers de configuration
  • fournir des valeurs par défaut
    • qui vont bien
    • ou qui arrête le service proprement (ou ne le démarre pas)

La programmation réactive

avec Promise et RxJS

Promesse

  • "une valeur/appel asynchrone"
  • permet de gérer les appels asynchrones
  • évite le "callback hell"
  • Promise objet natif dans ES6
  • sera replacé par async await (mais pas tout de suite)
const promise = new Promise((resolve, reject) => {
  // quelque chose d'asynchrone
  setTimeout(() => {
    if (/* est-ce que tout est ok ?*/) {
      resolve("ok");
    }
    else {
      reject(Error("erreur"));
    }
  }, 3000);
});

promise.then(result => {
  console.log(result);
}, err => {
  console.log(err);
});

RxJS

  • "une série de valeurs asynchrone"
  • utilise les concepts de la programmation fonctionnelle
    • map, filter, concat, interval, merge, count, debounce, delay, distinct,...
  • objets de base
    • Observable (produit des valeurs asynchrones)
    • Observer (consomme des valeurs)
    • Subject ( = Observer U Observable)
      seront des objets natifs dans ES7
  • utilisé par angular

Persistance

Snapshot/Incrémentale

Snapshot

On ne garde que le dernier état de la ressource

  • simple
  • la ressource est immédiatement disponible
  • permet peu de choses
  • requêtes simples

Incrémentale

On stocke toutes les modifications réalisées sur la ressource

  • complet : qui, quoi, quand
  • besoin de reconstituer la ressource
  • permet de retracer l'historique
  • requêtes complexes

Rappel sur JSONPatch

  • JSONPatch permet de décrire des opérations de changement d'un json
  • JSONPointer permet de décrire un "chemin" sur un nœud du json
{
  "biscuits": [
    { "name": "Digestive" },
    { "name": "Choco Leibniz" }
  ],
  "foo/bar~": "baz",
  "": "empty key"
}
""                  =>  racine de l'objet
"/"                 =>  "empty key"
"/biscuits"         =>  tableau
"/biscuits/1/name"  =>  "Choco Leibniz"
"/foo~1bar~0"       =>  "baz"
"/biscuits/-"       =>  fin du tableau, utile pour ajouter à la fin

Rappel sur JSONPatch

Liste des opérations

{ "op": "add", "path": "/biscuits/1", "value": { "name": "Ginger Nut" } }
{ "op": "remove", "path": "/biscuits/0" }
{ "op": "replace", "path": "/biscuits/0/name", "value": "Chocolate Digestive" }
{ "op": "copy", "from": "/biscuits/0", "path": "/best_biscuit" }
{ "op": "move", "from": "/biscuits", "path": "/cookies" }
{ "op": "test", "path": "/best_biscuit/name", "value": "Choco Leibniz" }
  • ​add
     
  • remove
     
  • replace
     
  • copy
     
  • move
     
  • test

Utilisation de mongoose

Avec node la librairie de référence pour mongo est mongoose

Elle permet :

  • d'associer un schéma à un type d'objet
  • d'effectuer les requêtes

Utilisation de mongoose

Stocker un object de type :

{
  "id": "un id unique type uuid",
  "des": "valeurs",
  "quelconques": "comme des ",
  "chiffres": 0
}

Snapshot

Incrémentale

const entitySchema = new mongoose.Schema({
  id:  {
    type:     String,
    required: true,
  },
  des: {
    type: String
  },
  quelconques: {
    type: mongoose.Schema.Types.Mixed
  },
  chiffres: {
    type: mongoose.Schema.Types.Number
  }
});
const objectPatch = new mongoose.Schema({
  id: {
    type:     String,
    required: true,
  },
  //ownerId: {
  //  type:     String,
  //  required: true,
  //},
  userId: {
    type:     String,
    required: true,
  },
  date: {
    type:     mongoose.Schema.Types.Date,
    required: true,
  },
  patch: [
    mongoose.Schema.Types.Mixed
  ]
});

Utilisation de mongoose

Déclaration du schéma

 (générique)

Snapshot

Incrémentale

const EntityModel = 
  mongoose.model(
    'entity', 
    entitySchema , 
    'entities'
  );
const objectPatchModel = 
  (name, collection) => 
    mongoose.model(
      name, 
      objectPatch , 
      collection || name
    );

Utilisation de mongoose

Déclaration du modèle

 (générique)

Snapshot

Incrémentale

{
  "_id": ObjectId("5a61...df5f"),
  "id": "un id unique type uuid",
  "des": "valeurs",
  "quelconques": "comme des ",
  "chiffres": 42
}
{
  "_id": ObjectId("5a61...df5f"),
  "id": "un id unique type uuid",
  "userId": "162c5c24-...718b",
  "date": ISODate("20...T...78Z"),
  "patch": [
    { 
      "op": "replace", 
      "path": "/chiffres", 
      "value": 42 
    }
  ]
}

Utilisation de mongoose

Ce qui est sstocké lors d'un changement de la valeur de chiffres en 42

Requête

Snapshot

const getAll = () => 
  EntityModel
    .find({});
const get = id => 
  EntityModel
    .findOne({ id })
    .then(entity => {
      if (entity === null) 
        throw Error(`NOT_FOUND no such entity ${id}`);
      return entity;
    });

Requête

Snapshot

const update = (entity) => 
  EntityModel
    .findOneAndUpdate({ id: entity.id }, entity)
    .then(e => {
      if (e === null) 
        throw Error(`NOT_FOUND no such entity ${entity.id}`);
      return e;
    });
const create = (id, newEntity) =>
  EntityModel.findOne({ id: newEntity.id }, {id: 1})
    .then(entity => {
      if (entity !== null) {
        throw Error(`ALREADY_EXISTS the entity "${newEntity.id}" already exists`);
      }
      debug(`create the entity "${newEntity.id}"`);
      return EntityModel.create(newEntity);
    });

Requête

Incrémentale

const get = (id) => 
  objectPatchModel
    .aggregate()
    .match({ id })         // recherche tous les patches sur l'entité
    .sort({ date: 1 })     //trie les résultats par date
    .unwind("$patch")      //applati tous les patch
    .group({ _id: "$id", patch: { $push:"$patch" } }) // regroupe tous les patch en un tableau
    .exec())
  .then(result => {
    if(result === null || result.length === 0) {
      throw error(NOT_FOUND, message: `no such entity ${id}`);
    }
    //reconstruction de l'entité
    return jsonpatch.applyPatch({ id }, result[0].patch).newDocument;
  });

Requête

Incrémentale

const getAll = (id) => 
  objectPatchModel
    .aggregate()
    .match({})         // recherche tous les patches sur l'entité
    .sort({ date: 1 })     //trie les résultats par date
    .unwind("$patch")      //applati tous les patch
    .group({ _id: "$id", patch: { $push:"$patch" } }) // regroupe tous les patch en un tableau
    .exec())
  .then(result => {
    const entities = [];
    for(let r of result) {
      entities.push(jsonpatch.applyPatch({ id: r._id }, r.patch).newDocument);
    }
    return entities;
  });

Requête

Incrémentale

const createPatch = (id, userId, prevEntity, newEntity) => {
  const patch = jsonpatch.compare(prevEntity, newEntity);
  return patch.length === 0 ?
    Promise.resolve() :
    objectPatchModel.create({
      id,
      userId,
      date: new Date(),
      patch,
    })
    .then((entry) => {});
};

const update = (userId, newEntity) => 
  get(newEntity.id || "")
  .then(prevEntity => {
    return createPatch(newEntity.id, userId, prevEntity, newEntity);
  });

Requête

Incrémentale

const createPatch = (id, userId, prevEntity, newEntity) => {
  const patch = jsonpatch.compare(prevEntity, newEntity);
  return patch.length === 0 ?
    Promise.resolve() :
    objectPatchModel.create({
      id,
      userId,
      date: new Date(),
      patch,
    })
    .then((entry) => {});
};

const patch = (entityId, userId, patch) => get(entityId || "")
  .then(prevEntity => {
    if (prevEntity === null) {
      throw error(NOT_FOUND, `unknow horse`);
    }
    if (!validateJsonPatch(patch)) {
      throw Error('BAD_REQUEST, bad patch format');
    }
    const newEntity = jsonpatch.applyPatch(prevEntity, patch).newDocument;
    //TODO check entity schema

    return createPatch(entityId, userId, prevEntity, newEntity);
  });

Test

Tests unitaires

Environnement de test

Pour que les tests soient reproductibles, l'environnement de test doit être immuable

  • utiliser le même jeu de données d'un test à l'autre
    • nettoyer et pré-remplir la base de données si nécessaire mongorestore
    • image docker avec les fichiers nécessaires)
  • utiliser une configuration spécifique aux tests
    • pas interdit d'avoir plusieurs jeux de tests

Structure de test

  • description
describe('controllers admin general', function() {
it('should return general information', function() {
  • test
describe('Array', function() {
  describe('#indexOf()', function() {
    it('should return -1 when the value is not present', function() {
      [1,2,3].indexOf(5).should.equal(-1);
      [1,2,3].indexOf(0).should.equal(-1);
    });
  });
});
  • exemple
Array
  #indexOf()
    ✓ should return -1 when the value is not present

Structure de test

  • hooks pour le cycle de vie
// Avant chaque test
beforeEach(function() { /* initialiser quelque chose */ }

// Après chaque test
afterEach(function() { /* terminer quelque chose */ }

// Avant le test
before(function() { /* initialiser quelque chose */ }

// Après le test
after(function() { /* terminer quelque chose */ }
beforeEach(function(done) {
  // initialiser quelque chose asynchrone
  done();
});
beforeEach(function() {
  // initialiser quelque chose synchrone
});
  • synchrone/asynchrone

Écriture de tests

  • supertest
const server = require('../../../app');
const request = require('supertest');

it('test API', function(done) {
  request(server)
    .get('url')
    .set('Header', 'value')
    .expect('Header', value')
    .expect(200) // ou 3.., 4.., 5..
    .end(function(err, res) {
      // tests ...
      done();
    });
});

Écriture de tests

  • should
const should = require('should');
//...
    .end(function(err, res) {
      should.not.exist(err);
      should.exist(res);
      done();
    });
//...
res.body.should.match({
  name: /notifSender/,
  version: v => v.should.match({
      number: /^\d*\.\d*\.\d*$/,
      build_type: /^(debug|release)$/,
      build_number: /^\d*$/
    })
});
should.not.exist(err);
res.body.should.have.match({
  status: REGEX_STATUS,
  dependencies: d => d.should
      .be.instanceof(Array)
      .and
      .matchEach({
        id: id => 
          id.should.be.a.String,
        description: descr => 
          descr.should.be.a.String,
        status: REGEX_STATUS,
      })
});
res.body.should.have.properties('title', 'description');

Les entêtes de réponse

  • Sensibilité à la casse
    Les tests sont case sensitive or les headers sont case insensitive
    => ajout d'une méthode de test
     
    • Au lieu de
      res.header.should.have.property('link').match(REGEX_LINK);

    • On utilisera
      propertyCI(res.header.should.have, 'Link').match(REGEX_LINK);

module.exports = (assertion, name) => {
  name = name.toLowerCase();
  let obj = assertion.obj;
  for(let p in obj) {
    if(obj.hasOwnProperty(p) && name === p.toLowerCase()) {
      assertion.obj = obj[p];
      return assertion;
    }
  }

  assertion.assert(false);
  return assertion;
};
res.header.should.have.property('link').match(REGEX_LINK);
propertyCI(res.header.should.have, 'Link').match(REGEX_LINK);

Exemple réel de test d'API

describe('controllers admin general', function() {

  describe('admin', function() {

    describe('GET /admin', function() {

      it('should return general information', function(done) {

        request(server)
          .get('/admin')
          .set('Accept', 'application/json')
          .expect('Content-Type', /json/)
          .expect(200)
          .end((err, res) => {
            should.not.exist(err);
            res.body.should.have.properties('title', 'description');
            propertyCI(res.header.should.have, 'Link').match(REGEX_LINK);
            done();
          });
      });

    });
    
    ...
controllers admin general
    admin
      GET /admin
        ✓ should return general information
      ...

Que tester ?

  • tester
    • API
    • middlewares
    • helpers
    • ...
  • tester différents cas de figure
    • le fonctionnement nominal
      ok quand il y a les bons paramètres
    • la gestion des cas d'erreurs
      erreur quand il y a de mauvais paramètres

Outils

Plusieurs systèmes existent

  • moteurs : mocha, jasmine, karma,...
  • assertions : should, sinon, must,...
  • requêteurs : supertest, http,...

 

difficile de s'y retrouver

Les questions

auxquelles il faut répondre

JS ou TS ?

  • JS de base (ES5)
  • ECMAScript 6 (ES6)
  • TypeScript (TS)

 

  • le typage évite les erreurs en amont
    • clarifie les contrats de méthodes et fonction
    • améliore la qualité du code écrit
    • Angular utilise TS
  • tout typer est compliqué
    • utilisation de .d.ts pour ajouter des types aux .js
    • ​​mettre any partout supprime l'intérêt du typage

=> partir sur un langage et s'y tenir

ng ou react ?

ng

  • modulaire (bien pour les gros projets, le chargement dynamique permet de ne pas saturer le navigateur)
  • apprentissage difficile (syntaxe absconse, débogage non trivial)
  • impose sa CLI (difficile d'ajouter ng après coup)

react

  • pas modulaire (on peut faire proprement)
  • apprentissage aisé (quand on connait js)
  • simple librairie js (facile à ajouter à un projet node existant moyennant un webpack)

À l'avenir il sera plus dur de trouver des compétences en ng qu'en react (un peut comme avec zend et symfony)

Webhook

Design

Version Configuration MGDIS

"webhooks": [
  {
    "topic": "PUT+*/entities/(:reference)",
    "callback": "url/to/call/the/webhook/{reference}",
    "method": "PUT",
    "headers": {
      "Authorization": "Basic YWRtaW46Q…zFXMGVtS1E="
    },
    "options": {
      ...
    }  
  },
  ...
]

Webhooks dynamiques

  • Créer une API RESTful de CRUD
  • Le format doit (devrait) être le même que pour la configuration
  • les options peuvent contenir
    • une date de péremption
    • un nombre d'utilisation
    • un système de formatage de données (xslt)

les websockets

les websockets

  • Intallation de ws
npm install --save ws
  • echo.websocket.org
const WebSocket = require('ws');

const ws = new WebSocket('wss://echo.websocket.org/', {
  origin: 'https://websocket.org'
});

ws.on('open', function open() {
  console.log('connected');
  ws.send(Date.now());
});

ws.on('close', function close() {
  console.log('disconnected');
});

ws.on('message', function incoming(data) {
  console.log(`Roundtrip time: ${Date.now() - data} ms`);

  setTimeout(function timeout() {
    ws.send(Date.now());
  }, 500);
});

Mise à jour récurrente

  • API fournissant le status (sans websocket)
const StatusBuilder = require('../helpers/statusBuilder');

function status(req, res) {
  new StatusBuilder()
    .addDependencie(require('../helpers/dbStatus'))
    .getStatus()
    .then(status => res.json(status));
}

Mise à jour récurrente

  • le service de récupération du status (sans websocket)
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Status } from '../models/status';
import io from 'socket.io-client';

const SERVER_URL = 'http://localhost:10010';

@Injectable()
export class HttpService {

  constructor(
    private url: string,
    private http: HttpClient
  ) { }

  public get(defaultValue?: Status) : Observable<Status> {
    return this.http.get<Status>(this.url)
    .pipe(
      catchError(this.handleError(defaultValue))
    );
  }

  private handleError(result?: T) {
      return (error: any): Observable<T> => {
        if(result === undefined) throw error;
        // Let the app keep running by returning an empty result.
        return of(result as T);
      };
    }
}

Mise à jour récurrente

  • appel récurrent au service (sans websocket)
...
export class AppStatusComponent implements OnInit, AfterViewInit, OnDestroy {

  status : Status = new Status("Non disponible");
  private interval: any = null;

  constructor(private statusService: StatusService) { }

  fetchStatus(): void {
    this.statusService.get(new Status( "Service notifSender"))
      .subscribe(status => { this.status = status; });
  }

  ngOnInit() {
    this.fetchStatus();
    this.interval = interval(10000);
  }

  ngAfterViewInit(): void {
    this.interval.subscribe(() => {
      this.fetchStatus();
    });
  }

  ngOnDestroy(): void {
    this.interval.unsubscribe();
  }
}

les websockets

  • envoyer le status régulièrement
const io = require('socket.io');
const StatusBuilder = require('../helpers/statusBuilder');

module.exports =
  server => {
    const wss = io(server);
    let prevStatus = null;

    wss.on('connect', function connection(ws) {
      wss.emit('status', prevStatus);
    });


    setInterval(function timeout() {
      new StatusBuilder()
        .addDependencie(require('../helpers/dbStatus'))
        .getStatus()
        .then(status => {
          if (prevStatus === null  || prevStatus.status !== status.status) {
            prevStatus = status;
            wss.emit('status', status);
          }
        });
    }, 1000);

  };

les websockets

  • le service de mise à jour
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Status } from '../models/status';
import io from 'socket.io-client';

const SERVER_URL = 'http://localhost:10010';

@Injectable()
export class SocketService {
  private socket;

  public initSocket(): void {
    this.socket = io(SERVER_URL);
  }

  public onMessage(): Observable<Status> {
    return new Observable<Status>(observer => {
      this.socket.on('status', (data: Status) => observer.next(data));
    });
  }
}

les websockets

  • L'affcihage
...
export class AppStatusWSComponent implements OnInit {


  status : Status = new Status("Non disponible");

  constructor(private statusService: SocketService) { }

  fetchStatus(): void {
    this.statusService.initSocket();
    this.statusService.onMessage()
      .subscribe(status => { this.status = status; });
  }

  ngOnInit() {
    this.fetchStatus();
  }
}

Ce qui manque

dans la suite

Atelier logiciel

  • Pas d'analyse de code avec sonarqube
  • Pas d'automatisation dans gitlab

Déploiement

  • Déploiement docker
  • Automatisation du déploiement

Analyse de log

  • Utiliser un système de logs type ELK
    • ElasticSearch
    • Logstash
    • Kibana
  • Permet de naviguer dans les logs
Made with Slides.com