From 0f8089d5f4e242ace39a4c075ff349d410b526eb Mon Sep 17 00:00:00 2001
From: hawkspar <quentin.chevalier@polytechnique.edu>
Date: Sat, 16 Mar 2019 21:13:31 +0100
Subject: [PATCH] Nouveaux tests, refacto Tools, fix generateGid

---
 package-lock.json           |  45 +++-----
 package.json                |   2 +
 src/ldap/export/group.ts    |   2 +-
 src/ldap/export/user.ts     |   2 +-
 src/ldap/internal/basics.ts | 206 +++++++++++++++++++++---------------
 src/ldap/internal/config.ts |   6 +-
 src/ldap/internal/tools.ts  |  22 ++--
 src/ldap/test.js            |   6 --
 src/ldap/test.ts            |  14 +++
 9 files changed, 160 insertions(+), 145 deletions(-)
 delete mode 100644 src/ldap/test.js
 create mode 100644 src/ldap/test.ts

diff --git a/package-lock.json b/package-lock.json
index d7ced64..d460047 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -94,7 +94,7 @@
         },
         "minimist": {
           "version": "1.2.0",
-          "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
           "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
           "dev": true
         },
@@ -4814,8 +4814,7 @@
         "ansi-regex": {
           "version": "2.1.1",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "aproba": {
           "version": "1.2.0",
@@ -4836,14 +4835,12 @@
         "balanced-match": {
           "version": "1.0.0",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "brace-expansion": {
           "version": "1.1.11",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "balanced-match": "^1.0.0",
             "concat-map": "0.0.1"
@@ -4858,20 +4855,17 @@
         "code-point-at": {
           "version": "1.1.0",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "concat-map": {
           "version": "0.0.1",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "console-control-strings": {
           "version": "1.1.0",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "core-util-is": {
           "version": "1.0.2",
@@ -4988,8 +4982,7 @@
         "inherits": {
           "version": "2.0.3",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "ini": {
           "version": "1.3.5",
@@ -5001,7 +4994,6 @@
           "version": "1.0.0",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "number-is-nan": "^1.0.0"
           }
@@ -5016,7 +5008,6 @@
           "version": "3.0.4",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "brace-expansion": "^1.1.7"
           }
@@ -5024,14 +5015,12 @@
         "minimist": {
           "version": "0.0.8",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "minipass": {
           "version": "2.3.5",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "safe-buffer": "^5.1.2",
             "yallist": "^3.0.0"
@@ -5050,7 +5039,6 @@
           "version": "0.5.1",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "minimist": "0.0.8"
           }
@@ -5131,8 +5119,7 @@
         "number-is-nan": {
           "version": "1.0.1",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "object-assign": {
           "version": "4.1.1",
@@ -5144,7 +5131,6 @@
           "version": "1.4.0",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "wrappy": "1"
           }
@@ -5230,8 +5216,7 @@
         "safe-buffer": {
           "version": "5.1.2",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "safer-buffer": {
           "version": "2.1.2",
@@ -5267,7 +5252,6 @@
           "version": "1.0.2",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "code-point-at": "^1.0.0",
             "is-fullwidth-code-point": "^1.0.0",
@@ -5287,7 +5271,6 @@
           "version": "3.0.1",
           "bundled": true,
           "dev": true,
-          "optional": true,
           "requires": {
             "ansi-regex": "^2.0.0"
           }
@@ -5331,14 +5314,12 @@
         "wrappy": {
           "version": "1.0.2",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         },
         "yallist": {
           "version": "3.0.3",
           "bundled": true,
-          "dev": true,
-          "optional": true
+          "dev": true
         }
       }
     },
@@ -10066,7 +10047,7 @@
         "source-map": {
           "version": "0.6.1",
           "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
-          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=",
           "dev": true
         }
       }
diff --git a/package.json b/package.json
index 0f5d90a..d507106 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,7 @@
     "tslint": "tslint --project tsconfig.json",
     "tsfix": "tslint --project tsconfig.json --fix",
     "tsc": "tsc --project tsconfig.json",
+    "ldap_test": "node --require ts-node/register src/ldap/test.ts",
     "test": "mocha --require mocha-graphql-register --require ts-node/register test/**/*.test.ts --timeout 20000"
   },
   "repository": {
@@ -70,6 +71,7 @@
     "@types/connect-flash": "0.0.34",
     "@types/graphql": "^14.0.7",
     "@types/knex": "^0.15.2",
+    "@types/ldapjs": "^1.0.3",
     "@types/mocha": "^5.2.6",
     "@types/node": "^11.9.5",
     "@types/passport": "^1.0.0",
diff --git a/src/ldap/export/group.ts b/src/ldap/export/group.ts
index 60134bd..f70b22f 100644
--- a/src/ldap/export/group.ts
+++ b/src/ldap/export/group.ts
@@ -210,7 +210,7 @@ export class Group {
 
         // gid de base généré à partir du nom standardisé, pas à partir de l'entrée 'gid' !
         try {
-            Tools.generateReadableId("group", data['name']).then(id => {
+            Tools.generateGid(data['name']).then(id => {
                 vals[ldapConfig.group.gid]      = id;
                 vals[ldapConfig.group['name']]  = id;
             });
diff --git a/src/ldap/export/user.ts b/src/ldap/export/user.ts
index e6e22ad..5c22154 100644
--- a/src/ldap/export/user.ts
+++ b/src/ldap/export/user.ts
@@ -119,7 +119,7 @@ export class User {
         // uid de base généré à partir de nom et prénom, plus potentiellement promo et un offset
         // MEF mélange de Promise et de fonction standard
         try {
-            Tools.generateUid("user",data['givenName'],data['lastName'],data['birthdate']).then(id => { vals[ldapConfig.user.uid]=id; } );
+            Tools.generateUid(data['givenName'],data['lastName'],data['birthdate']).then(id => { vals[ldapConfig.user.uid]=id; } );
         }
         catch(err) {
             throw "Erreur lors de la génération d'un hruid pour un nouvel utilisateur.";
diff --git a/src/ldap/internal/basics.ts b/src/ldap/internal/basics.ts
index 7dc9077..4ca0bd2 100644
--- a/src/ldap/internal/basics.ts
+++ b/src/ldap/internal/basics.ts
@@ -7,19 +7,18 @@
  * @memberof LDAP
  */
 
-import ldap from 'ldapjs';
+// Import moche à cause mauvais typage ldapjs
+var ldap:any = require('ldapjs');
 // Toutes les entrées utilisateur sont escapées par sécurité
 import ldapEscape from 'ldap-escape';
 // Fichier de ldapConfig du ldap
 import {ldapConfig, credentialsLdapConfig} from './config';
 
 // Connection au serveur LDAP avec des temps de timeout arbitraires
-var client = ldap.createClient({ url: ldapConfig.server});
+var client = ldap.createClient({ url: ldapConfig.server });
 
 // Interface pratique pour que Typescript comprenne ce qu'est un dictionnaire simple
-interface dic {
-    [Key: string]: string | string[];
-}
+interface dic { [Key: string]: string | string[]; }
 
 //------------------------------------------------------------------------------------------------------------------------
 // Fonctions de base agissant sur le LDAP
@@ -36,48 +35,28 @@ export class Basics {
 
     /**
      * @memberof LDAP
-     * @summary Fonction qui sert à s'identifier sur le LDAP.
-     * @desc Assez important en terme de sécurité, de gestion de conflit, et de droit d'accès. Méthode ldapjs 
-     * (voir [`Client API`](http://ldapjs.org/client.html) méthode bind).
-     * @arg {string} dn - Nom de domaine ; identifiant de l'utilisateur cherchant à se connecter
-     * @arg {string} password - Mot de passe de l'utilisateur cherchant à se connecter
+     * @summary Fonction qui sert à se déconnecter du LDAP, puis s'identifier sur le LDAP avec pleins pouvoirs.
+     * @desc Assez important en terme de sécurité, de gestion de conflit, et de droit d'accès.
+     * Fait appel à une méthode ldapjs (voir [`Client API`](http://ldapjs.org/client.html) méthode bind).
      * @returns {Promise(boolean)} `true` si l'opération s'est bien déroulée, `false` sinon.
      * @static
-     * @async
      */
-    static async bind(dn: string, password: string) : Promise<boolean> {
-        // Escape DN as everywhere in this file, but password is taken as is
-        client.bind(dn, password, res => {
-            // Gestion erreur
-            try         { res; }
-            catch(err)  { throw "Erreur lors de la connection au LDAP.";}
+    static bind() : Promise<boolean> {
+        return new Promise<boolean>((resolve, reject) => {
+            // Se déconnecter dans le doute
+            client.unbind();
+            // Escape DN as everywhere in this file, but password is taken as is
+            client.bind(credentialsLdapConfig.dn, credentialsLdapConfig.password, err => {
+                // Gestion erreur
+                if (err instanceof ldap.LDAPError) {
+                    console.log("Erreur lors de la connection au LDAP : "+err.message);
+                    resolve(false);
+                }
+                resolve(true);
+            });
         });
-        // End with a boolean
-        return true;
     }
 
-    /**
-     * @memberof LDAP
-     * @summary Fonction qui sert à s'identifier sur le LDAP avec plein pouvoirs.
-     * @desc Appelle {@link bind} avec un utilisateur tout puissant.
-     * @returns {Promise(boolean)} `true` si l'opération s'est bien déroulée, `false` sinon.
-     * @static
-     * @async
-     */
-    static async adminBind() : Promise<boolean> { return Basics.bind(credentialsLdapConfig.dn, credentialsLdapConfig.password); }
-
-    /**
-     * @memberof LDAP
-     * @summary Fonction qui sert à se déconnecter du LDAP.
-     * @desc Assez important en terme de sécurité, de gestion de conflit, et de droit d'accès.
-     * Fait appel à {@link Basics.bind} avec deux champs vides.
-     * @returns {Promise(boolean)} `true` si l'opération s'est bien déroulée, `false` sinon.
-     * @static
-     * @async
-     */
-    static async unbind() : Promise<boolean> { return Basics.bind("", ""); }
-    
-
     /**
      * @memberof LDAP
      * @callback entryHandler
@@ -96,10 +75,9 @@ export class Basics {
      * @arg {entryHandler} handler - Wrapper pour gérer les requêtes simples ou multiples
      * @return {void} Utilise handler pour gérer ses résultats au fur et à mesure
      * @static
-     * @async
      */
-    static search(domain: 'group'|'user', attributes: string[], id: string, filter: string, handler : (entry: any) => void) : Promise<void> {
-        let dn ="";
+    static search(domain: 'group'|'user', attributes: string[], id: string, filter: string, handler : (entry: any) => void) : Promise<boolean> {
+        let dn = "";
         if (id != null)     {
             if (domain == "group")  dn += ldapConfig.group.gid;
             else                    dn += ldapConfig.user.uid;
@@ -108,27 +86,33 @@ export class Basics {
         dn += ldapConfig.dn[domain];
         console.log("Searching dn= " + dn + ", filter : " + filter);
         // Interrogation LDAP selon filter
-        let promise = new Promise<void>(function(resolve, reject) {
-            client.search(dn, {            // Must be escaped in case of a malignious false id
+        return new Promise<boolean>(function(resolve, reject) {
+            client.search(dn, {             // Must be escaped in case of a malignious false id
                 "scope": "sub",
-                "filter": filter,      // Must be escaped in case of a malignious search arg
+                "filter": filter,           // Must be escaped in case of a malignious search arg
                 "attributes": attributes
             }, (err, res) => {
                 // Gestion erreur ; pb car pas simple true / autre en sortie
-                if (err) {
-                    throw "Erreur lors de la recherche sur le LDAP.";
+                if (Basics.catch(err)) {
+                    console.log("Erreur lors de la recherche sur le LDAP.");
+                    resolve(false);
                 } else {
                     // Dès que la recherche renvoit une entrée, on stocke les attributs qui nous intéresse
                     res.on('searchEntry', entry => handler(entry));
-                    // Si la recherche renvoie une erreur, on renvoit
-                    res.on('error', resErr => { throw resErr; });
+                    // Si la recherche renvoie une erreur (client ou TCP seulement), on renvoit
+                    res.on('error', err2 => {
+                        console.log(err2);
+                        resolve(false);
+                    });
                     // Quand la recherche est finie on se déconnecte
-                    res.on('end', _ => resolve());
+                    res.on('end', res2 => {
+                        // Si la co avec le LDAP est tombée on relance
+                        if (res2.status == 0) Basics.bind();
+                        resolve(true);
+                    });
                 }
             });
         });
-
-        return promise;
     }
     
     /**
@@ -180,6 +164,35 @@ export class Basics {
         return vals;
     }
 
+    /**
+     * @memberof LDAP
+     * @summary Fonction intermédiaire qui factorise la gestion d'erreur pour toute la classe.
+     * @desc Permet de définir un comportement par défaut pour les méthodes de base add, change et clear pour différents types d'erreur.
+     * @arg {ldap.LDAPError} err - Erreur renvoyée
+     * @returns {Promise(boolean)} `true` si l'opération s'est bien déroulée, `false` sinon.
+     * @static 
+     */
+    static catch(err) : boolean {
+        if (err instanceof ldap.LDAPError) {
+            console.log("L'erreur suivante est survenue : " +err.message);
+            // TBC
+            if (err instanceof ldap.TimeLimitExceededError) {
+                Basics.bind();
+            }
+            else if (err instanceof ldap.ProtocolError) {
+                Basics.bind();
+            }
+            else if (err instanceof ldap.SizeLimitExceededError) {
+                Basics.bind();
+            }
+            else if (err instanceof ldap.InsufficientAccessRightsError) {
+                Basics.bind();
+            }
+            return true;
+        }
+        return false;
+    }
+
     /**
      * @memberof LDAP
      * @summary Fonction qui permet de modifier un élément sur le LDAP. Gestion intelligente de l'appartenance à un binet.
@@ -192,20 +205,26 @@ export class Basics {
      * @arg {string} mod[key] - Nouvelle valeur de l'attribut key. Une nouvelle valeur vide ("") est équivalent à la suppression de cet attribut.
      * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, `false` sinon.
      * @static
-     * @async
      */
-    static async change(domain: 'group'|'user', id: string, op: "add"|"del"|"replace", mod: dic) : Promise<boolean> {
-        let dn = "";
-        if (domain == 'group')  dn += ldapConfig.group.gid;
-        else                    dn += ldapConfig.user.uid;
-        dn += '='+ldapEscape.dn("${txt}", { txt: id })+','+ldapConfig.dn[domain];
-        // Modification LDAP selon dn fourni en argument (pourrait prendre une liste de Changes)
-        client.modify(ldapEscape.dn("${txt}", {txt: dn}), new ldap.Change({
-            operation: op,
-            modification: mod,
-        // Gestion erreur 
-        }), err => { throw "Erreur lors d'une opération de modification sur le LDAP."; });
-        return true;
+    static change(domain: 'group'|'user', id: string, op: "add"|"del"|"replace", mod: dic) : Promise<boolean> {
+        return new Promise<boolean>((resolve, reject) => {
+            let dn = "";
+            if (domain == 'group')  dn += ldapConfig.group.gid;
+            else                    dn += ldapConfig.user.uid;
+            dn += '='+ldapEscape.dn("${txt}", { txt: id })+','+ldapConfig.dn[domain];
+            // Modification LDAP selon dn fourni en argument (pourrait prendre une liste de Changes)
+            client.modify(ldapEscape.dn("${txt}", {txt: dn}), new ldap.Change({
+                operation: op,
+                modification: mod,
+            // Gestion erreur 
+            }), err => {
+                if (Basics.catch(err)) {
+                    console.log("Erreur lors d'une opération élémentaire de modification dans le LDAP (Basics.change)");
+                    resolve(false);
+                }
+                resolve(true);
+            });
+        });
     }
 
     /**
@@ -218,16 +237,22 @@ export class Basics {
      * @arg {Object} vals[key] - Nouvelle valeur pour le champ key
      * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon.
      * @static
-     * @async
      */
-    static async add(domain: 'group'|'user', vals) : Promise<boolean> {
-        let dn = "";
-        if (domain == "group")  dn += ldapConfig.group.gid+"="+ldapEscape.dn("${txt}", { txt: vals[ldapConfig.group.gid] });
-        else                    dn += ldapConfig.user.uid+"="+ldapEscape.dn("${txt}", { txt: vals[ldapConfig.user.uid] });
-        dn += ldapConfig.dn[domain];
-        // Ajout LDAP selon la ldapConfiguration en argument
-        client.add(ldapEscape.dn("${txt}", { txt: dn }), vals, err => { throw "Erreur lors d'une opération d'ajout sur le LDAP."; });
-        return true;
+    static add(domain: 'group'|'user', vals) : Promise<boolean> {
+        return new Promise<boolean>((resolve, reject) => {
+            let dn = "";
+            if (domain == "group")  dn += ldapConfig.group.gid+"="+ldapEscape.dn("${txt}", { txt: vals[ldapConfig.group.gid] });
+            else                    dn += ldapConfig.user.uid+"="+ldapEscape.dn("${txt}", { txt: vals[ldapConfig.user.uid] });
+            dn += ldapConfig.dn[domain];
+            // Ajout LDAP selon la ldapConfiguration en argument
+            client.add(dn, vals, err => {
+                if (Basics.catch(err)) {
+                    console.log("Erreur lors d'une opération élémentaire d'ajout dans le LDAP (Basics.add)");
+                    resolve(false);
+                }
+                resolve(true);
+            });
+        });
     }
 
     /**
@@ -239,21 +264,26 @@ export class Basics {
      * @arg {string} id - Identifiant unique de la cible, passé par ldapEscape dans cette fonction
      * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon
      * @static
-     * @async
      */
-    static async clear(domain: 'group'|'user', id: string) : Promise<boolean> {
-        let dn = "";
-        if (domain == "group")  dn += ldapConfig.group.gid+"="+ldapEscape.dn("${txt}", { txt: id });
-        else                    dn += ldapConfig.user.uid+"="+ldapEscape.dn("${txt}", { txt: id });
-        dn += ldapConfig.dn[domain];
-        // Suppression LDAP
-        client.del(ldapEscape.dn("${txt}", {txt: dn}), err => { throw "Erreur lors d'une opération de suppression sur le LDAP."; });
-        return true;
+    static clear(domain: 'group'|'user', id: string) : Promise<boolean> {
+        return new Promise<boolean>((resolve, reject) => {
+            let dn = "";
+            if (domain == "group")  dn += ldapConfig.group.gid+"="+ldapEscape.dn("${txt}", { txt: id });
+            else                    dn += ldapConfig.user.uid+"="+ldapEscape.dn("${txt}", { txt: id });
+            dn += ldapConfig.dn[domain];
+            // Suppression LDAP
+            client.del(dn, err => {
+                if (Basics.catch(err)) {
+                    console.log("Erreur lors d'une opération élémentaire de suppression dans le LDAP (Basics.clear)");
+                    resolve(false);
+                }
+                resolve(true);
+            });
+        });
     }
 }
 
 // Bind
-Basics.unbind();
-Basics.adminBind();
+Basics.bind();
 
-console.log("Binding with LDAP client completed successfully, looking good !");
\ No newline at end of file
+console.info("Binding with LDAP client completed successfully, looking good !");
\ No newline at end of file
diff --git a/src/ldap/internal/config.ts b/src/ldap/internal/config.ts
index 2e25dc1..b0b662d 100644
--- a/src/ldap/internal/config.ts
+++ b/src/ldap/internal/config.ts
@@ -15,12 +15,12 @@ import dotenv from 'dotenv';
 
 // Chargement de l'environnement
 let path_env = path.resolve(__dirname, '..', '..', '..', './.env');
-console.log(colors.red("Loading .env config file from "+path_env));
+console.info(colors.red("Loading .env config file from "+path_env));
 dotenv.config({ path: path_env });
 
 // Point central ; tous les champs de la BDD sont 'cachés' dans config.json et pas visibles directement
 let path_config = path.resolve(__dirname, '..', '..', '..', './ldap_config.json');
-console.log(colors.cyan("Loading LDAP config file from "+path_config));
+console.info(colors.cyan("Loading LDAP config file from "+path_config));
 export const ldapConfig = JSON.parse(fs.readFileSync(path_config).toString());
 
 // Override config server from environment
@@ -32,7 +32,7 @@ else {
 
 // Gestion des super-identifiants
 let path_credentials = path.resolve(__dirname, '..', '..', '..', 'ldap_credentials.json');
-console.log(colors.green("Loading LDAP credentials from "+path_credentials));
+console.info(colors.green("Loading LDAP credentials from "+path_credentials));
 export const credentialsLdapConfig = JSON.parse(fs.readFileSync(path_credentials).toString());
 
 // Data formats and useful constants
diff --git a/src/ldap/internal/tools.ts b/src/ldap/internal/tools.ts
index 0846ad0..4ade5e8 100644
--- a/src/ldap/internal/tools.ts
+++ b/src/ldap/internal/tools.ts
@@ -471,7 +471,6 @@ export class Tools {
      * @memberof LDAP
      * @summary Cette fonction génère un uid standard, puis le fait évoluer jusqu'à ce qu'il soit unique.
      * @desc Limité à un appel à {@link Tools.ensureUnique} avec les bons paramètres, et quelques opérations sur l'uid pour qu'il soit valide (escape, normalisation).
-     * @param {"group"|"user"} domain - Arbre à parcourir
      * @param {string} givenName - Prénom
      * @param {string} lastName - Nom
      * @param {string} promotion - Année de promotion
@@ -479,12 +478,10 @@ export class Tools {
      * @static
      * @async
      */
-    static async generateUid(domain : "group"|"user", givenName: string, lastName: string, promotion: string) : Promise<string> {
+    static async generateUid(givenName: string, lastName: string, promotion: string) : Promise<string> {
         try {
-            if (domain == "group")  var att=ldapConfig.group.gid;
-            else                    var att=ldapConfig.user.uid;
             // normalize et lowerCase standardisent le format
-            return Tools.ensureUnique((givenName+'.'+lastName).toLowerCase().normalize('UFD'), att, "user", (id: string, n: number) => {
+            return Tools.ensureUnique((givenName+'.'+lastName).toLowerCase().normalize('UFD'), ldapConfig.user.uid, "user", (id: string, n: number) => {
                 if (n=1)        id+='.'+promotion;          // Si prénom.nom existe déjà, on rajoute la promo
                 else if (n=2)   id+='.'+(n-1).toString();   // Puis si prénom.nom.promo existe déjà on passe à nom.prenom.promo .1
                 else if (n>2)   id+=(n-1).toString();       // Ensuite on continue .123, .1234, etc...
@@ -492,33 +489,30 @@ export class Tools {
             });
         }
         catch(err) {
-            throw "Erreur lors de l'assurance de l'unicité d'un human readable unique identifier (hruid).";
+            throw "Erreur lors de l'assurance de l'unicité d'un human readable unique identifier pour un utilisateur (hruid) (Tools.generateUid).";
         }
     }
 
     /**
      * @memberof LDAP
-     * @summary Cette fonction génère un id lisible, puis le fait évoluer jusqu'à ce qu'il soit unique.
-     * @desc Limité à un appel à {@link Tools.ensureUnique} avec les bons paramètres, et quelques opérations sur l'uid pour qu'il soit valide (escape, normalisation).
-     * @param {"group"|"user"} domain - Arbre à parcourir
+     * @summary Cette fonction génère un gid lisible, puis le fait évoluer jusqu'à ce qu'il soit unique.
+     * @desc Limité à un appel à {@link Tools.ensureUnique} avec les bons paramètres, et quelques opérations sur le gid pour qu'il soit valide (escape, normalisation).
      * @param {string} name - Nom
      * @return {Promise(string)} Valeur unique dans le domaine spécifié de l'attribut spécifié
      * @static
      * @async
      */
-    static async generateReadableId(domain : "group"|"user", name: string) : Promise<string> {
+    static async generateGid(name : string) : Promise<string> {
         try {
-            if (domain == "group")  var att=ldapConfig.group.gid;
-            else                    var att=ldapConfig.user.uid;
             // normalize et lowerCase standardisent le format
-            return Tools.ensureUnique(name.toLowerCase().normalize('UFD'), att, domain, (id: string, n: number) => {
+            return Tools.ensureUnique(name.toLowerCase().normalize('UFD'), ldapConfig.group.gid, "group", (id: string, n: number) => {
                 if (n=1)        id+='.'+n.toString();   // Si nom existe déjà, on essaie nom.1
                 else if (n>1)   id+=n.toString();       // Ensuite on continue .12, .123, etc...
                 return id;
             });
         }
         catch(err) {
-            throw "Erreur lors de l'assurance de l'unicité d'un human readable unique identifier (hruid).";
+            throw "Erreur lors de l'assurance de l'unicité d'un human readable unique identifier pour un groupe (hrgid) (Tools.generateGid).";
         }
     }
 
diff --git a/src/ldap/test.js b/src/ldap/test.js
deleted file mode 100644
index 832f1fe..0000000
--- a/src/ldap/test.js
+++ /dev/null
@@ -1,6 +0,0 @@
-var Group = require("../../tsbuild/src/ldap/export/group").Group;
-console.log(Group);
-
-Group.peek("faerix").then(dat => { console.log(dat); });
-
-var User = require("../../tsbuild/src/ldap/export/group");
\ No newline at end of file
diff --git a/src/ldap/test.ts b/src/ldap/test.ts
new file mode 100644
index 0000000..baa8af6
--- /dev/null
+++ b/src/ldap/test.ts
@@ -0,0 +1,14 @@
+import {Group} from './export/group';
+
+async function test_creation() {
+    let dat = await Group.peek("faerix");
+    console.log(dat);
+    dat.gid = "faerix2";
+    Group.create(dat);
+}
+
+import {User} from './export/user';
+
+User.peek("oliver.facklam").then(dat => console.log(dat));
+
+test_creation();
\ No newline at end of file
-- 
GitLab