@open3cl/engine 1.6.1 → 1.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/10_besoin_fr.spec.js +122 -0
- package/10_clim.spec.js +116 -0
- package/11_besoin_ecs.spec.js +69 -0
- package/11_ecs.spec.js +282 -0
- package/11_nadeq.spec.js +37 -0
- package/12.4_pac.spec.js +131 -0
- package/13.2_generateur_combustion.js +2 -0
- package/13.2_generateur_combustion.spec.js +537 -0
- package/13.2_generateur_combustion_bouilleur.js +4 -1
- package/13.2_generateur_combustion_bouilleur.spec.js +74 -0
- package/13.2_generateur_combustion_ch.spec.js +323 -0
- package/13.2_generateur_combustion_chaudiere.spec.js +85 -0
- package/13.2_generateur_pac.spec.js +64 -0
- package/13_rendement_distribution_ecs.spec.js +38 -0
- package/14_generateur_ecs.js +4 -4
- package/14_generateur_ecs.spec.js +745 -0
- package/15_conso_aux.spec.js +225 -0
- package/16.2_production_enr.spec.js +188 -0
- package/16_conso_eclairage.spec.js +62 -0
- package/2021_04_13_confort_ete.spec.js +159 -0
- package/2021_04_13_qualite_isolation.js +1 -1
- package/2021_04_13_qualite_isolation.spec.js +286 -0
- package/3.1_b.js +1 -1
- package/3.1_b.spec.js +367 -0
- package/3.2.1_mur.spec.js +812 -39
- package/3.2.2_plancher_bas.spec.js +643 -78
- package/3.2.3_plancher_haut.spec.js +452 -0
- package/3.3.1.4_porte.spec.js +114 -0
- package/3.3_baie_vitree.spec.js +449 -295
- package/3.4_pont_thermique.spec.js +890 -0
- package/3_deperdition.spec.js +397 -0
- package/4_ventilation.js +1 -1
- package/4_ventilation.spec.js +406 -0
- package/5_conso_ventilation.spec.js +186 -0
- package/6.2_surface_sud_equivalente.spec.js +138 -0
- package/8_intermittence.spec.js +33 -0
- package/9_besoin_ch.js +2 -2
- package/9_besoin_ch.spec.js +381 -0
- package/9_chauffage.js +4 -0
- package/9_chauffage.spec.js +478 -92
- package/9_conso_ch.spec.js +502 -248
- package/9_emetteur_ch.spec.js +339 -0
- package/9_generateur_ch.spec.js +467 -81
- package/apport_et_besoin.spec.js +126 -0
- package/conso.js +1 -1
- package/conso.spec.js +471 -82
- package/core/assets/domain/add-additionnal-ue-values-tables.spec.js +53 -0
- package/core/assets/domain/synchronize-solicitations-tables.spec.js +25 -0
- package/core/assets/domain/synchronize-valeur-tables.spec.js +48 -0
- package/core/conf/infrastructure/application.config.spec.js +35 -0
- package/core/file/infrastructure/adapter/csv-parser.store.spec.js +31 -13
- package/core/file/infrastructure/adapter/file.store.spec.js +148 -12
- package/core/tv/infrastructure/assets/additional-ue-values.spec.js +36 -0
- package/core/util/infrastructure/object-util.spec.js +20 -0
- package/core/util/logger/log-service.js +16 -7
- package/core/util/logger/log-service.spec.js +101 -0
- package/dpe-sanitizer.service.spec.js +138 -0
- package/engine.js +4 -1
- package/engine.spec.js +854 -0
- package/enums.spec.js +46 -0
- package/features/dpe/domain/models/type-generateur.model.js +1 -2
- package/features/dpe/domain/models/type-generateur.model.spec.js +25 -0
- package/features/dpe/domain/models/type-habitation.model.spec.js +41 -0
- package/features/dpe/domain/models/type-stockage.model.spec.js +18 -0
- package/features/dpe/domain/models/type-ventilation.model.spec.js +19 -0
- package/features/dpe/infrastructure/ch/chTv.store.spec.js +4 -4
- package/features/dpe/infrastructure/enveloppe/baieVitreeTv.store.js +14 -2
- package/features/dpe/infrastructure/enveloppe/baieVitreeTv.store.spec.js +16 -0
- package/features/dpe/infrastructure/tv.store.js +28 -9
- package/features/dpe/infrastructure/tv.store.spec.js +44 -0
- package/features/engine/domain/apport_et_besoin/apport-et-besoin.service.spec.js +2 -1
- package/features/engine/domain/apport_et_besoin/ch/besoin-ch.service.spec.js +18 -0
- package/features/engine/domain/apport_et_besoin/ch/perte-ch-recup.service.spec.js +33 -1
- package/features/engine/domain/apport_et_besoin/ecs/besoin-ecs.service.spec.js +21 -1
- package/features/engine/domain/apport_et_besoin/ecs/perte-ecs-recup.service.spec.js +41 -1
- package/features/engine/domain/apport_et_besoin/froid/besoin-froid.service.spec.js +44 -1
- package/features/engine/domain/apport_et_besoin/surface-sud-equivalente.service.spec.js +2 -1
- package/features/engine/domain/ch/generateur-ch.service.js +2 -0
- package/features/engine/domain/ch/generateur-ch.service.spec.js +117 -2
- package/features/engine/domain/ch/installation-ch.service.spec.js +14 -0
- package/features/engine/domain/conso/froid/conso-froid.service.spec.js +82 -1
- package/features/engine/domain/constants.spec.js +14 -0
- package/features/engine/domain/contexte.builder.spec.js +21 -0
- package/features/engine/domain/ecs/generateur-ecs.service.spec.js +11 -0
- package/features/engine/domain/ecs/installation-ecs.service.spec.js +55 -2
- package/features/engine/domain/engine.service.spec.js +227 -4
- package/features/engine/domain/enveloppe/baie_vitree/deperdition-baie-vitree.service.spec.js +124 -1
- package/features/engine/domain/enveloppe/deperdition-enveloppe.service.spec.js +270 -2
- package/features/engine/domain/enveloppe/deperdition.service.spec.js +141 -0
- package/features/engine/domain/enveloppe/espace_tampon/espace-tampon.service.spec.js +2 -1
- package/features/engine/domain/enveloppe/mur/deperdition-mur.service.spec.js +117 -1
- package/features/engine/domain/enveloppe/plancher_bas/deperdition-plancher-bas.service.spec.js +134 -1
- package/features/engine/domain/enveloppe/plancher_haut/deperdition-plancher-haut.service.spec.js +126 -1
- package/features/engine/domain/enveloppe/pont_thermique/deperdition-pont-thermique.service.spec.js +18 -1
- package/features/engine/domain/enveloppe/porte/deperdition-porte.service.spec.js +2 -1
- package/features/engine/domain/enveloppe/ventilation/deperdition-ventilation.service.spec.js +67 -1
- package/features/normalizer/domain/dpe-normalizer.service.js +1 -1
- package/features/normalizer/domain/dpe-normalizer.service.spec.js +53 -2
- package/ficheTechnique.spec.js +26 -0
- package/index.spec.js +54 -0
- package/package.json +5 -2
- package/preprocessor/index.js +20 -0
- package/preprocessor/index.spec.js +15 -0
- package/tv.spec.js +38 -0
- package/utils.js +43 -1
- package/utils.spec.js +518 -2
- package/output.js +0 -1
|
@@ -51,4 +51,52 @@ describe('SynchronizeValeurTables unit tests', () => {
|
|
|
51
51
|
);
|
|
52
52
|
});
|
|
53
53
|
});
|
|
54
|
+
|
|
55
|
+
test('ajoute les valeurs Ue supplémentaires quand la table `ue` est présente', () => {
|
|
56
|
+
const fileStore = new FileStore();
|
|
57
|
+
const appConfig = new ApplicationConfig();
|
|
58
|
+
const synchronizeC1Tables = new SynchronizeC1Tables(fileStore, appConfig);
|
|
59
|
+
const synchronizeSolicitationTables = new SynchronizeSolicitationsTables(fileStore, appConfig);
|
|
60
|
+
const synchronizeDpeGesLimitValuesTables = new SynchronizeDpeGesLimitValuesTables(
|
|
61
|
+
fileStore,
|
|
62
|
+
appConfig
|
|
63
|
+
);
|
|
64
|
+
const addAdditionnalUeValuesTables = new AddAdditionnalUeValuesTables(fileStore, appConfig);
|
|
65
|
+
const synchronizeValeurTables = new SynchronizeValeurTables(
|
|
66
|
+
fileStore,
|
|
67
|
+
appConfig,
|
|
68
|
+
synchronizeSolicitationTables,
|
|
69
|
+
synchronizeDpeGesLimitValuesTables,
|
|
70
|
+
addAdditionnalUeValuesTables,
|
|
71
|
+
synchronizeC1Tables
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
vi.spyOn(ApplicationConfig.prototype, 'ademeValeurTablesFileUrl', 'get').mockReturnValue(
|
|
75
|
+
'http://localhost/file.xlsx'
|
|
76
|
+
);
|
|
77
|
+
vi.spyOn(ApplicationConfig.prototype, 'assetsOutputFolder', 'get').mockReturnValue(
|
|
78
|
+
'src/assets'
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// Le classeur téléchargé contient une table `ue` : la branche d'ajout des
|
|
82
|
+
// valeurs Ue supplémentaires doit être empruntée.
|
|
83
|
+
vi.spyOn(fileStore, 'downloadXlsxFileAndConvertToJson').mockResolvedValue({
|
|
84
|
+
ue: [{ type_adjacence: 'ext', ue: '1.5' }]
|
|
85
|
+
});
|
|
86
|
+
vi.spyOn(fileStore, 'writeFileToLocalSystem').mockResolvedValue(null);
|
|
87
|
+
vi.spyOn(synchronizeC1Tables, 'execute').mockResolvedValue({});
|
|
88
|
+
vi.spyOn(synchronizeSolicitationTables, 'execute').mockResolvedValue({});
|
|
89
|
+
vi.spyOn(synchronizeDpeGesLimitValuesTables, 'execute').mockResolvedValue({});
|
|
90
|
+
const addUeSpy = vi
|
|
91
|
+
.spyOn(addAdditionnalUeValuesTables, 'execute')
|
|
92
|
+
.mockReturnValue({ ue: 'valeurs-ue-completees' });
|
|
93
|
+
|
|
94
|
+
return synchronizeValeurTables.execute().then(() => {
|
|
95
|
+
// La fonction d'ajout reçoit les valeurs fusionnées contenant la table ue.
|
|
96
|
+
expect(addUeSpy).toHaveBeenCalledWith(expect.objectContaining({ ue: expect.any(Array) }));
|
|
97
|
+
// Le fichier écrit contient le résultat renvoyé par l'ajout Ue.
|
|
98
|
+
const contenu = vi.mocked(fileStore.writeFileToLocalSystem).mock.calls[0][1];
|
|
99
|
+
expect(contenu).toContain('valeurs-ue-completees');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
54
102
|
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test } from 'vitest';
|
|
2
|
+
import { ApplicationConfig } from './application.config.js';
|
|
3
|
+
|
|
4
|
+
/** @type {ApplicationConfig} */
|
|
5
|
+
let config;
|
|
6
|
+
|
|
7
|
+
describe('ApplicationConfig', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
config = new ApplicationConfig();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('expose la version et l’URL du dépôt ADEME', () => {
|
|
13
|
+
expect(config.ademeRepositoryVersion).toBe('fix-pont-thermique-et-warning-audit-copro');
|
|
14
|
+
expect(config.ademeRepositoryUrl).toBe('https://gitlab.com/observatoire-dpe/observatoire-dpe');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('construit l’URL du fichier des tables d’enums à partir de l’URL et de la version', () => {
|
|
18
|
+
expect(config.ademeEnumTablesFileUrl).toBe(
|
|
19
|
+
`${config.ademeRepositoryUrl}/-/raw/${config.ademeRepositoryVersion}/modele_donnee/enum_tables.xlsx?ref_type=tags&inline=false`
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('construit l’URL du fichier des tables de valeurs', () => {
|
|
24
|
+
expect(config.ademeValeurTablesFileUrl).toBe(
|
|
25
|
+
`${config.ademeRepositoryUrl}/-/raw/${config.ademeRepositoryVersion}/modele_donnee/valeur_tables.xlsx?ref_type=tags&inline=false`
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('expose les chemins des fichiers locaux de tables de valeurs', () => {
|
|
30
|
+
expect(config.solicitationsExtFilePath).toBe('src/tv/18.2_sollicitations_ext.ods');
|
|
31
|
+
expect(config.c1FilePath).toBe('src/tv/18.5_c1.ods');
|
|
32
|
+
expect(config.dpeGesLimitValuesFilePath).toBe('src/tv/dpe_ges_limit_values.ods');
|
|
33
|
+
expect(config.assetsOutputFolder).toBe('src');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -1,28 +1,46 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
2
|
import CsvParserStore from './csv-parser.store.js';
|
|
3
|
-
import {
|
|
4
|
-
import { resolve } from 'node:path';
|
|
3
|
+
import { Readable } from 'node:stream';
|
|
5
4
|
|
|
6
5
|
describe('CsvParserStore', () => {
|
|
7
6
|
/** @type {CsvParserStore} **/
|
|
8
7
|
let store;
|
|
9
8
|
|
|
10
|
-
/** @type {ReadStream} **/
|
|
11
|
-
let stream;
|
|
12
|
-
|
|
13
9
|
beforeEach(() => {
|
|
14
10
|
store = new CsvParserStore();
|
|
15
11
|
});
|
|
16
12
|
|
|
17
13
|
describe('parseFromStream', () => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
14
|
+
// Flux CSV construit en mémoire pour garder le test isolé (aucune dépendance à un fichier réel).
|
|
15
|
+
it('convertit chaque ligne du flux en objet à partir des en-têtes', async () => {
|
|
16
|
+
const stream = Readable.from(['numero_dpe,etiquette\nDPE-1,A\nDPE-2,F\n']);
|
|
17
|
+
|
|
18
|
+
const data = await store.parseFromStream(stream, { headers: true }, (row) => row);
|
|
19
|
+
|
|
20
|
+
expect(data).toStrictEqual([
|
|
21
|
+
{ numero_dpe: 'DPE-1', etiquette: 'A' },
|
|
22
|
+
{ numero_dpe: 'DPE-2', etiquette: 'F' }
|
|
23
|
+
]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('applique la fonction de transformation à chaque ligne', async () => {
|
|
27
|
+
const stream = Readable.from(['numero_dpe,etiquette\nDPE-1,A\n']);
|
|
28
|
+
|
|
29
|
+
const data = await store.parseFromStream(stream, { headers: true }, (row) => ({
|
|
30
|
+
id: row.numero_dpe
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
expect(data).toStrictEqual([{ id: 'DPE-1' }]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('rejette la promesse lorsque la transformation lève une erreur', async () => {
|
|
37
|
+
const stream = Readable.from(['numero_dpe,etiquette\nDPE-1,A\n']);
|
|
38
|
+
|
|
39
|
+
await expect(
|
|
40
|
+
store.parseFromStream(stream, { headers: true }, () => {
|
|
41
|
+
throw new Error('ligne invalide');
|
|
42
|
+
})
|
|
43
|
+
).rejects.toThrow('ligne invalide');
|
|
26
44
|
});
|
|
27
45
|
});
|
|
28
46
|
});
|
|
@@ -1,5 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Isolation : on mocke le module `fs` pour ne pas dépendre du système de fichiers
|
|
4
|
+
// (lecture d'un vrai `.ods`) ni écrire réellement sur le disque.
|
|
5
|
+
vi.mock('fs', () => ({
|
|
6
|
+
readFile: vi.fn((path, cb) => cb(null, Buffer.from(new ArrayBuffer(1)))),
|
|
7
|
+
writeFile: vi.fn((path, content, opts, cb) => cb(null))
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
// Isolation : on mocke `xlsx` pour piloter le contenu du classeur retourné et
|
|
11
|
+
// tester la logique propre au fichier (fusion de cellules `!merges`) sans
|
|
12
|
+
// dépendre du parsing réel d'un binaire.
|
|
13
|
+
vi.mock('xlsx', () => ({
|
|
14
|
+
read: vi.fn(() => ({ SheetNames: ['Sheet1'], Sheets: { Sheet1: {} } })),
|
|
15
|
+
utils: {
|
|
16
|
+
// Colonnes : 0 -> A, 1 -> B, ...
|
|
17
|
+
encode_col: vi.fn((c) => String.fromCharCode(65 + c)),
|
|
18
|
+
// Lignes : 0 -> 1, 1 -> 2, ... (numérotation 1-based du tableur)
|
|
19
|
+
encode_row: vi.fn((r) => String(r + 1)),
|
|
20
|
+
sheet_to_json: vi.fn(() => [])
|
|
21
|
+
}
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
const { readFile, writeFile } = await import('fs');
|
|
25
|
+
const XLSX = await import('xlsx');
|
|
26
|
+
const { FileStore } = await import('./file.store.js');
|
|
3
27
|
|
|
4
28
|
global.fetch = vi.fn(() =>
|
|
5
29
|
Promise.resolve({
|
|
@@ -7,25 +31,137 @@ global.fetch = vi.fn(() =>
|
|
|
7
31
|
})
|
|
8
32
|
);
|
|
9
33
|
|
|
10
|
-
describe('FileStore
|
|
11
|
-
|
|
12
|
-
|
|
34
|
+
describe('FileStore', () => {
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
vi.mocked(readFile).mockReset();
|
|
37
|
+
vi.mocked(writeFile).mockReset();
|
|
38
|
+
vi.mocked(XLSX.read).mockReset();
|
|
39
|
+
vi.mocked(XLSX.utils.sheet_to_json).mockReset();
|
|
40
|
+
vi.mocked(XLSX.utils.encode_col).mockClear();
|
|
41
|
+
vi.mocked(XLSX.utils.encode_row).mockClear();
|
|
42
|
+
|
|
43
|
+
// Comportements par défaut « nominaux ».
|
|
44
|
+
vi.mocked(readFile).mockImplementation((path, cb) => cb(null, Buffer.from(new ArrayBuffer(1))));
|
|
45
|
+
vi.mocked(writeFile).mockImplementation((path, content, opts, cb) => cb(null));
|
|
46
|
+
vi.mocked(XLSX.read).mockReturnValue({
|
|
47
|
+
SheetNames: ['Sheet1'],
|
|
48
|
+
Sheets: { Sheet1: {} }
|
|
49
|
+
});
|
|
50
|
+
vi.mocked(XLSX.utils.sheet_to_json).mockReturnValue([]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('downloadXlsxFileAndConvertToJson', () => {
|
|
54
|
+
test('télécharge le fichier puis le convertit en JSON par feuille', async () => {
|
|
55
|
+
const fileStore = new FileStore();
|
|
56
|
+
|
|
57
|
+
const output = await fileStore.downloadXlsxFileAndConvertToJson('http://localhost:8080');
|
|
13
58
|
|
|
14
|
-
return fileStore.downloadXlsxFileAndConvertToJson('http://localhost:8080').then((output) => {
|
|
15
59
|
expect(output).toEqual({ Sheet1: [] });
|
|
60
|
+
// Le buffer téléchargé est lu par XLSX avec les bonnes options.
|
|
61
|
+
expect(fetch).toHaveBeenCalledWith('http://localhost:8080');
|
|
62
|
+
expect(XLSX.read).toHaveBeenCalledWith(expect.anything(), {
|
|
63
|
+
type: 'string',
|
|
64
|
+
raw: false
|
|
65
|
+
});
|
|
16
66
|
});
|
|
17
67
|
});
|
|
18
68
|
|
|
19
|
-
|
|
20
|
-
|
|
69
|
+
describe('readLocalOdsFileAndConvertToJson', () => {
|
|
70
|
+
test('lit le fichier local et le convertit en JSON par feuille', async () => {
|
|
71
|
+
const fileStore = new FileStore();
|
|
72
|
+
|
|
73
|
+
const output = await fileStore.readLocalOdsFileAndConvertToJson('file.ods');
|
|
21
74
|
|
|
22
|
-
return fileStore.readLocalOdsFileAndConvertToJson('file.ods').then((output) => {
|
|
23
75
|
expect(output).toEqual({ Sheet1: [] });
|
|
76
|
+
expect(readFile).toHaveBeenCalledWith('file.ods', expect.any(Function));
|
|
77
|
+
// La lecture locale utilise l'option `type: 'buffer'`.
|
|
78
|
+
expect(XLSX.read).toHaveBeenCalledWith(expect.anything(), {
|
|
79
|
+
type: 'buffer',
|
|
80
|
+
raw: false
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("journalise et avale l'erreur si la lecture échoue (branche rejet + catch)", async () => {
|
|
85
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
86
|
+
vi.mocked(readFile).mockImplementation((path, cb) => cb(new Error('lecture impossible')));
|
|
87
|
+
|
|
88
|
+
const fileStore = new FileStore();
|
|
89
|
+
const output = await fileStore.readLocalOdsFileAndConvertToJson('absent.ods');
|
|
90
|
+
|
|
91
|
+
// Le catch avale l'erreur : la promesse se résout sur `undefined`.
|
|
92
|
+
expect(output).toBeUndefined();
|
|
93
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
94
|
+
'Could not parse file: absent.ods, reason: lecture impossible'
|
|
95
|
+
);
|
|
96
|
+
// XLSX ne doit pas être appelé en cas d'erreur de lecture.
|
|
97
|
+
expect(XLSX.read).not.toHaveBeenCalled();
|
|
98
|
+
|
|
99
|
+
consoleErrorSpy.mockRestore();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('writeFileToLocalSystem', () => {
|
|
104
|
+
test('écrit le contenu en UTF-8 et résout la promesse', async () => {
|
|
105
|
+
const fileStore = new FileStore();
|
|
106
|
+
|
|
107
|
+
await expect(
|
|
108
|
+
fileStore.writeFileToLocalSystem('sortie.json', 'contenu')
|
|
109
|
+
).resolves.toBeUndefined();
|
|
110
|
+
|
|
111
|
+
expect(writeFile).toHaveBeenCalledWith(
|
|
112
|
+
'sortie.json',
|
|
113
|
+
'contenu',
|
|
114
|
+
{ encoding: 'utf-8' },
|
|
115
|
+
expect.any(Function)
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("rejette la promesse si l'écriture échoue (branche rejet)", async () => {
|
|
120
|
+
const erreur = new Error('disque plein');
|
|
121
|
+
vi.mocked(writeFile).mockImplementation((path, content, opts, cb) => cb(erreur));
|
|
122
|
+
|
|
123
|
+
const fileStore = new FileStore();
|
|
124
|
+
|
|
125
|
+
await expect(fileStore.writeFileToLocalSystem('sortie.json', 'contenu')).rejects.toBe(erreur);
|
|
24
126
|
});
|
|
25
127
|
});
|
|
26
128
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
129
|
+
describe('conversion des feuilles avec cellules fusionnées', () => {
|
|
130
|
+
test('propage la valeur de la première cellule sur les lignes fusionnées', async () => {
|
|
131
|
+
const firstCell = { v: 'valeur fusionnée' };
|
|
132
|
+
const feuille = {
|
|
133
|
+
// Fusion de A1 à A3 (3 lignes, colonne 0).
|
|
134
|
+
'!merges': [{ s: { c: 0, r: 0 }, e: { c: 0, r: 2 } }],
|
|
135
|
+
A1: firstCell
|
|
136
|
+
};
|
|
137
|
+
const classeur = { SheetNames: ['Sheet1'], Sheets: { Sheet1: feuille } };
|
|
138
|
+
vi.mocked(XLSX.read).mockReturnValue(classeur);
|
|
139
|
+
|
|
140
|
+
const fileStore = new FileStore();
|
|
141
|
+
await fileStore.downloadXlsxFileAndConvertToJson('http://localhost:8080');
|
|
142
|
+
|
|
143
|
+
// nbMergedRows = 2 : les cellules A2 et A3 reçoivent la valeur de A1.
|
|
144
|
+
expect(feuille.A2).toBe(firstCell);
|
|
145
|
+
expect(feuille.A3).toBe(firstCell);
|
|
146
|
+
// La feuille (mutée) est ensuite transformée en JSON.
|
|
147
|
+
expect(XLSX.utils.sheet_to_json).toHaveBeenCalledWith(feuille, {
|
|
148
|
+
raw: false
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("ne modifie pas la feuille en l'absence de fusion", async () => {
|
|
153
|
+
const feuille = { A1: { v: 'x' } };
|
|
154
|
+
vi.mocked(XLSX.read).mockReturnValue({
|
|
155
|
+
SheetNames: ['Sheet1'],
|
|
156
|
+
Sheets: { Sheet1: feuille }
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const fileStore = new FileStore();
|
|
160
|
+
await fileStore.downloadXlsxFileAndConvertToJson('http://localhost:8080');
|
|
161
|
+
|
|
162
|
+
// Aucune cellule supplémentaire n'est créée.
|
|
163
|
+
expect(Object.keys(feuille)).toEqual(['A1']);
|
|
164
|
+
expect(XLSX.utils.encode_col).not.toHaveBeenCalled();
|
|
165
|
+
});
|
|
30
166
|
});
|
|
31
167
|
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import { UPB_ADDITIONAL_VALUES } from './additional-ue-values.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `UPB_ADDITIONAL_VALUES` complète la table de valeurs Ue pour les
|
|
6
|
+
* déperditions des planchers bas. On vérifie la cohérence structurelle des
|
|
7
|
+
* entrées sans figer l'intégralité des valeurs numériques.
|
|
8
|
+
*/
|
|
9
|
+
describe('Valeurs Ue additionnelles (UPB_ADDITIONAL_VALUES)', () => {
|
|
10
|
+
test('est un tableau non vide', () => {
|
|
11
|
+
expect(Array.isArray(UPB_ADDITIONAL_VALUES)).toBe(true);
|
|
12
|
+
expect(UPB_ADDITIONAL_VALUES.length).toBeGreaterThan(0);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('chaque entrée possède les métadonnées d’adjacence attendues', () => {
|
|
16
|
+
UPB_ADDITIONAL_VALUES.forEach((entree) => {
|
|
17
|
+
expect(entree).toHaveProperty('type_adjacence_plancher');
|
|
18
|
+
expect(entree).toHaveProperty('enum_type_adjacence_id');
|
|
19
|
+
expect(entree).toHaveProperty('type_adjacence');
|
|
20
|
+
expect(entree).toHaveProperty('enum_periode_construction_id');
|
|
21
|
+
expect(entree).toHaveProperty('periode_construction');
|
|
22
|
+
expect(Array.isArray(entree.values)).toBe(true);
|
|
23
|
+
expect(entree.values.length).toBeGreaterThan(0);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('chaque valeur contient les clés 2s_p, upb et ue exploitables numériquement', () => {
|
|
28
|
+
UPB_ADDITIONAL_VALUES.forEach((entree) => {
|
|
29
|
+
entree.values.forEach((valeur) => {
|
|
30
|
+
expect(valeur).toHaveProperty('2s_p');
|
|
31
|
+
expect(Number.isNaN(parseFloat(valeur.upb))).toBe(false);
|
|
32
|
+
expect(Number.isNaN(parseFloat(valeur.ue))).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -1,7 +1,27 @@
|
|
|
1
1
|
import { ObjectUtil } from './object-util.js';
|
|
2
2
|
import { describe, expect, test } from 'vitest';
|
|
3
3
|
|
|
4
|
+
const identite = (x) => x;
|
|
5
|
+
|
|
4
6
|
describe('ObjectUtil unit tests', () => {
|
|
7
|
+
test("retourne la primitive telle quelle quand la racine n'est ni objet ni tableau", () => {
|
|
8
|
+
// Branche terminale `: obj` : une primitive n'est pas transformée.
|
|
9
|
+
expect(ObjectUtil.deepObjectTransform('brut', identite, identite)).toBe('brut');
|
|
10
|
+
expect(ObjectUtil.deepObjectTransform(42, identite, identite)).toBe(42);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('conserve une valeur nulle sans tenter de la parcourir récursivement', () => {
|
|
14
|
+
// Branche `val !== null` : `null` est de type "object" mais ne doit pas
|
|
15
|
+
// être exploré récursivement.
|
|
16
|
+
const resultat = ObjectUtil.deepObjectTransform(
|
|
17
|
+
{ a: null },
|
|
18
|
+
(key) => key.toUpperCase(),
|
|
19
|
+
identite
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
expect(resultat).toEqual({ A: null });
|
|
23
|
+
});
|
|
24
|
+
|
|
5
25
|
test('should be able to deeply transform an object keys and values', () => {
|
|
6
26
|
expect(
|
|
7
27
|
ObjectUtil.deepObjectTransform(
|
|
@@ -2,6 +2,21 @@ import { createLogger, format, transports } from 'winston';
|
|
|
2
2
|
|
|
3
3
|
const { combine, timestamp, prettyPrint, colorize, errors, printf } = format;
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Formate une ligne de log. Si une pile (`stack`) est présente, elle est
|
|
7
|
+
* ajoutée à la suite du message. Fonction pure exportée pour être testée
|
|
8
|
+
* unitairement.
|
|
9
|
+
* @param {{level: string, message: string, timestamp: string, stack?: string}} info
|
|
10
|
+
* @return {string}
|
|
11
|
+
*/
|
|
12
|
+
export const formatLogLine = ({ level, message, timestamp, stack }) => {
|
|
13
|
+
if (stack) {
|
|
14
|
+
// print log trace
|
|
15
|
+
return `${timestamp} ${level}: ${message} - ${stack}`;
|
|
16
|
+
}
|
|
17
|
+
return `${timestamp} ${level}: ${message}`;
|
|
18
|
+
};
|
|
19
|
+
|
|
5
20
|
// Disable traditional console logs
|
|
6
21
|
const copyLog = console.log;
|
|
7
22
|
const copyWarn = console.warn;
|
|
@@ -15,13 +30,7 @@ export const logger = createLogger({
|
|
|
15
30
|
colorize(),
|
|
16
31
|
timestamp(),
|
|
17
32
|
prettyPrint(),
|
|
18
|
-
printf(
|
|
19
|
-
if (stack) {
|
|
20
|
-
// print log trace
|
|
21
|
-
return `${timestamp} ${level}: ${message} - ${stack}`;
|
|
22
|
-
}
|
|
23
|
-
return `${timestamp} ${level}: ${message}`;
|
|
24
|
-
})
|
|
33
|
+
printf(formatLogLine)
|
|
25
34
|
),
|
|
26
35
|
transports: [new transports.Console()]
|
|
27
36
|
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from 'vitest';
|
|
2
|
+
import { formatLogLine, logger, setLoggerOff, setLoggerOn } from './log-service.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Sauvegarde des méthodes console d'origine : `setLoggerOff`/`setLoggerOn`
|
|
6
|
+
* réaffectent les globales `console.*`, on restaure après chaque test pour
|
|
7
|
+
* garantir l'isolation.
|
|
8
|
+
*/
|
|
9
|
+
const consoleOrigine = {
|
|
10
|
+
log: console.log,
|
|
11
|
+
warn: console.warn,
|
|
12
|
+
debug: console.debug,
|
|
13
|
+
error: console.error
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
describe('Service de logs (log-service)', () => {
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
console.log = consoleOrigine.log;
|
|
19
|
+
console.warn = consoleOrigine.warn;
|
|
20
|
+
console.debug = consoleOrigine.debug;
|
|
21
|
+
console.error = consoleOrigine.error;
|
|
22
|
+
logger.silent = false;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('expose un logger winston avec les méthodes de niveau usuelles', () => {
|
|
26
|
+
expect(logger).toBeDefined();
|
|
27
|
+
expect(typeof logger.info).toBe('function');
|
|
28
|
+
expect(typeof logger.warn).toBe('function');
|
|
29
|
+
expect(typeof logger.error).toBe('function');
|
|
30
|
+
expect(logger.level).toBe('info');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('setLoggerOff rend le logger silencieux et neutralise log/warn/debug', () => {
|
|
34
|
+
setLoggerOff();
|
|
35
|
+
|
|
36
|
+
expect(logger.silent).toBe(true);
|
|
37
|
+
expect(console.log).not.toBe(consoleOrigine.log);
|
|
38
|
+
expect(console.warn).not.toBe(consoleOrigine.warn);
|
|
39
|
+
expect(console.debug).not.toBe(consoleOrigine.debug);
|
|
40
|
+
// Par défaut, console.error n'est pas neutralisé
|
|
41
|
+
expect(console.error).toBe(consoleOrigine.error);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('setLoggerOff(true) neutralise aussi console.error', () => {
|
|
45
|
+
setLoggerOff(true);
|
|
46
|
+
|
|
47
|
+
expect(console.error).not.toBe(consoleOrigine.error);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('setLoggerOn réactive le logger et restaure les méthodes console', () => {
|
|
51
|
+
setLoggerOff(true);
|
|
52
|
+
setLoggerOn(true);
|
|
53
|
+
|
|
54
|
+
expect(logger.silent).toBe(false);
|
|
55
|
+
expect(console.log).toBe(consoleOrigine.log);
|
|
56
|
+
expect(console.warn).toBe(consoleOrigine.warn);
|
|
57
|
+
expect(console.debug).toBe(consoleOrigine.debug);
|
|
58
|
+
expect(console.error).toBe(consoleOrigine.error);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('les méthodes console neutralisées sont des fonctions no-op', () => {
|
|
62
|
+
setLoggerOff();
|
|
63
|
+
|
|
64
|
+
expect(() => console.log('ignoré')).not.toThrow();
|
|
65
|
+
expect(console.log('ignoré')).toBeUndefined();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('toutes les méthodes console neutralisées (log/warn/debug/error) sont des no-op', () => {
|
|
69
|
+
setLoggerOff(true);
|
|
70
|
+
|
|
71
|
+
// On invoque chaque no-op pour vérifier l'absence d'effet/erreur.
|
|
72
|
+
expect(console.log('x')).toBeUndefined();
|
|
73
|
+
expect(console.warn('x')).toBeUndefined();
|
|
74
|
+
expect(console.debug('x')).toBeUndefined();
|
|
75
|
+
expect(console.error('x')).toBeUndefined();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('formatLogLine formate un message simple sans pile (branche sans stack)', () => {
|
|
79
|
+
const ligne = formatLogLine({
|
|
80
|
+
level: 'info',
|
|
81
|
+
message: 'message simple',
|
|
82
|
+
timestamp: '2026-09-04T00:00:00'
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Sans pile, aucun séparateur de trace n'est ajouté.
|
|
86
|
+
expect(ligne).toBe('2026-09-04T00:00:00 info: message simple');
|
|
87
|
+
expect(ligne).not.toContain(' - ');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('formatLogLine ajoute la pile quand elle est présente (branche stack)', () => {
|
|
91
|
+
const ligne = formatLogLine({
|
|
92
|
+
level: 'error',
|
|
93
|
+
message: 'boom',
|
|
94
|
+
timestamp: '2026-09-04T00:00:00',
|
|
95
|
+
stack: 'Error: boom\n at fichier.js:1:1'
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// La présence d'une pile insère le séparateur " - " suivi de la trace.
|
|
99
|
+
expect(ligne).toBe('2026-09-04T00:00:00 error: boom - Error: boom\n at fichier.js:1:1');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Dépendances mockées pour isoler `DpeSanitizerService` :
|
|
5
|
+
* - `lodash-es` : `has`/`set` contrôlés pour observer la création des collections
|
|
6
|
+
* manquantes ;
|
|
7
|
+
* - `ObjectUtil.deepObjectTransform` : mocké pour capturer la fonction de
|
|
8
|
+
* transformation des valeurs et la tester unitairement, sans parcourir un vrai DPE.
|
|
9
|
+
*/
|
|
10
|
+
vi.mock('lodash-es', () => ({
|
|
11
|
+
has: vi.fn(),
|
|
12
|
+
set: vi.fn()
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock('./core/util/infrastructure/object-util.js', () => ({
|
|
16
|
+
ObjectUtil: {
|
|
17
|
+
deepObjectTransform: vi.fn()
|
|
18
|
+
}
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
const { default: DpeSanitizerService } = await import('./dpe-sanitizer.service.js');
|
|
22
|
+
const { has, set } = await import('lodash-es');
|
|
23
|
+
const { ObjectUtil } = await import('./core/util/infrastructure/object-util.js');
|
|
24
|
+
|
|
25
|
+
/** Liste des collections dont l'absence doit être compensée par un tableau vide. */
|
|
26
|
+
const COLLECTIONS = [
|
|
27
|
+
'logement.enveloppe.mur_collection.mur',
|
|
28
|
+
'logement.enveloppe.plancher_bas_collection.plancher_bas',
|
|
29
|
+
'logement.enveloppe.plancher_haut_collection.plancher_haut',
|
|
30
|
+
'logement.ventilation_collection.ventilation',
|
|
31
|
+
'logement.climatisation_collection.climatisation',
|
|
32
|
+
'logement.enveloppe.baie_vitree_collection.baie_vitree',
|
|
33
|
+
'logement.enveloppe.porte_collection.porte',
|
|
34
|
+
'logement.enveloppe.pont_thermique_collection.pont_thermique'
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
let service;
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
vi.mocked(has).mockReset();
|
|
41
|
+
vi.mocked(set).mockReset();
|
|
42
|
+
vi.mocked(ObjectUtil.deepObjectTransform).mockReset();
|
|
43
|
+
vi.mocked(ObjectUtil.deepObjectTransform).mockImplementation((dpe) => dpe);
|
|
44
|
+
service = new DpeSanitizerService();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('DpeSanitizerService.execute - collections manquantes', () => {
|
|
48
|
+
test('crée un tableau vide pour chaque collection absente', () => {
|
|
49
|
+
vi.mocked(has).mockReturnValue(false);
|
|
50
|
+
const dpe = {};
|
|
51
|
+
|
|
52
|
+
service.execute(dpe);
|
|
53
|
+
|
|
54
|
+
expect(set).toHaveBeenCalledTimes(COLLECTIONS.length);
|
|
55
|
+
for (const path of COLLECTIONS) {
|
|
56
|
+
expect(set).toHaveBeenCalledWith(dpe, path, []);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('ne recrée pas les collections déjà présentes', () => {
|
|
61
|
+
vi.mocked(has).mockReturnValue(true);
|
|
62
|
+
|
|
63
|
+
service.execute({});
|
|
64
|
+
|
|
65
|
+
expect(set).not.toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('délègue la transformation profonde et retourne son résultat', () => {
|
|
69
|
+
vi.mocked(has).mockReturnValue(true);
|
|
70
|
+
const transforme = { transforme: true };
|
|
71
|
+
vi.mocked(ObjectUtil.deepObjectTransform).mockReturnValue(transforme);
|
|
72
|
+
|
|
73
|
+
const resultat = service.execute({ brut: true });
|
|
74
|
+
|
|
75
|
+
expect(ObjectUtil.deepObjectTransform).toHaveBeenCalledOnce();
|
|
76
|
+
expect(resultat).toBe(transforme);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Récupère la fonction de transformation des valeurs passée en 3e argument de
|
|
82
|
+
* `deepObjectTransform` après un appel à `execute`.
|
|
83
|
+
*/
|
|
84
|
+
function getValueTransform() {
|
|
85
|
+
vi.mocked(has).mockReturnValue(true);
|
|
86
|
+
service.execute({});
|
|
87
|
+
return vi.mocked(ObjectUtil.deepObjectTransform).mock.calls[0][2];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
describe('DpeSanitizerService - transformation des valeurs', () => {
|
|
91
|
+
test('encapsule dans un tableau les nœuds simples à cardinalité multiple', () => {
|
|
92
|
+
const valueTransform = getValueTransform();
|
|
93
|
+
const generateur = { id: 1 };
|
|
94
|
+
// `generateur_ecs` fait partie des nœuds à transformer en tableau
|
|
95
|
+
expect(valueTransform(generateur, 'generateur_ecs')).toEqual([generateur]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('ne ré-encapsule pas un nœud déjà sous forme de tableau', () => {
|
|
99
|
+
const valueTransform = getValueTransform();
|
|
100
|
+
const deja = [{ id: 1 }];
|
|
101
|
+
// déjà un tableau => traité comme une valeur non numérique et renvoyé tel quel
|
|
102
|
+
expect(valueTransform(deja, 'generateur_ecs')).toBe(deja);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("convertit les valeurs d'énumération en chaîne de caractères", () => {
|
|
106
|
+
const valueTransform = getValueTransform();
|
|
107
|
+
expect(valueTransform(12, 'enum_type_generateur_ch_id')).toBe('12');
|
|
108
|
+
expect(valueTransform(5, 'original_enum_type_generateur_ch_id')).toBe('5');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('une énumération nulle donne une valeur indéfinie (optional chaining)', () => {
|
|
112
|
+
const valueTransform = getValueTransform();
|
|
113
|
+
expect(valueTransform(null, 'enum_type_generateur_ch_id')).toBeUndefined();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('remplace les valeurs vides ou nulles par une chaîne vide', () => {
|
|
117
|
+
const valueTransform = getValueTransform();
|
|
118
|
+
expect(valueTransform('', 'surface_paroi_opaque')).toBe('');
|
|
119
|
+
expect(valueTransform(null, 'surface_paroi_opaque')).toBe('');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('conserve les tableaux vides tels quels', () => {
|
|
123
|
+
const valueTransform = getValueTransform();
|
|
124
|
+
const vide = [];
|
|
125
|
+
expect(valueTransform(vide, 'mur')).toBe(vide);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('conserve les chaînes non numériques telles quelles', () => {
|
|
129
|
+
const valueTransform = getValueTransform();
|
|
130
|
+
expect(valueTransform('maison', 'description')).toBe('maison');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('convertit les chaînes numériques en nombres', () => {
|
|
134
|
+
const valueTransform = getValueTransform();
|
|
135
|
+
expect(valueTransform('40.94', 'surface_paroi_opaque')).toBe(40.94);
|
|
136
|
+
expect(valueTransform('12', 'quantite')).toBe(12);
|
|
137
|
+
});
|
|
138
|
+
});
|