@hed-hog/core 0.0.373 → 0.0.374

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.
Files changed (107) hide show
  1. package/README.md +83 -1
  2. package/dist/integration-profile/integration-profile.secrets.d.ts.map +1 -1
  3. package/dist/integration-profile/integration-profile.secrets.js +4 -0
  4. package/dist/integration-profile/integration-profile.secrets.js.map +1 -1
  5. package/dist/integration-profile/integration-profile.utils.d.ts +6 -0
  6. package/dist/integration-profile/integration-profile.utils.d.ts.map +1 -1
  7. package/dist/integration-profile/integration-profile.utils.js +1 -1
  8. package/dist/integration-profile/integration-profile.utils.js.map +1 -1
  9. package/dist/oauth/oauth.controller.d.ts +11 -3
  10. package/dist/oauth/oauth.controller.d.ts.map +1 -1
  11. package/dist/oauth/oauth.controller.js +52 -14
  12. package/dist/oauth/oauth.controller.js.map +1 -1
  13. package/dist/oauth/oauth.module.d.ts.map +1 -1
  14. package/dist/oauth/oauth.module.js +4 -0
  15. package/dist/oauth/oauth.module.js.map +1 -1
  16. package/dist/oauth/oauth.service.d.ts +38 -2
  17. package/dist/oauth/oauth.service.d.ts.map +1 -1
  18. package/dist/oauth/oauth.service.js +122 -5
  19. package/dist/oauth/oauth.service.js.map +1 -1
  20. package/dist/oauth/providers/apple.provider.d.ts +34 -0
  21. package/dist/oauth/providers/apple.provider.d.ts.map +1 -0
  22. package/dist/oauth/providers/apple.provider.js +197 -0
  23. package/dist/oauth/providers/apple.provider.js.map +1 -0
  24. package/dist/oauth/providers/facebook.provider.d.ts +1 -1
  25. package/dist/oauth/providers/facebook.provider.d.ts.map +1 -1
  26. package/dist/oauth/providers/facebook.provider.js +3 -2
  27. package/dist/oauth/providers/facebook.provider.js.map +1 -1
  28. package/dist/oauth/providers/google.provider.d.ts +1 -1
  29. package/dist/oauth/providers/google.provider.d.ts.map +1 -1
  30. package/dist/oauth/providers/google.provider.js +3 -2
  31. package/dist/oauth/providers/google.provider.js.map +1 -1
  32. package/dist/oauth/providers/linkedin.provider.d.ts +25 -0
  33. package/dist/oauth/providers/linkedin.provider.d.ts.map +1 -0
  34. package/dist/oauth/providers/linkedin.provider.js +99 -0
  35. package/dist/oauth/providers/linkedin.provider.js.map +1 -0
  36. package/dist/oauth/providers/microsoft-entra-id.provider.js +1 -1
  37. package/dist/oauth/providers/microsoft-entra-id.provider.js.map +1 -1
  38. package/dist/oauth/providers/microsoft.provider.d.ts +1 -1
  39. package/dist/oauth/providers/microsoft.provider.d.ts.map +1 -1
  40. package/dist/oauth/providers/microsoft.provider.js +3 -2
  41. package/dist/oauth/providers/microsoft.provider.js.map +1 -1
  42. package/dist/pdf/pdf.service.d.ts.map +1 -1
  43. package/dist/pdf/pdf.service.js +19 -2
  44. package/dist/pdf/pdf.service.js.map +1 -1
  45. package/dist/setting/setting.service.d.ts.map +1 -1
  46. package/dist/setting/setting.service.js +4 -1
  47. package/dist/setting/setting.service.js.map +1 -1
  48. package/hedhog/data/integration_provider.yaml +16 -0
  49. package/hedhog/data/setting_group.yaml +98 -0
  50. package/hedhog/data/setting_subgroup.yaml +16 -0
  51. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +2 -0
  52. package/hedhog/frontend/app/dashboard/dashboard.css.ejs +11 -11
  53. package/hedhog/frontend/app/integration/profiles/page.tsx.ejs +42 -0
  54. package/hedhog/frontend/app/menu/page.tsx.ejs +1 -1
  55. package/hedhog/frontend/app/users/user-sheet.tsx.ejs +8 -0
  56. package/hedhog/frontend/messages/en.json +35 -0
  57. package/hedhog/frontend/messages/pt.json +35 -0
  58. package/hedhog/table/user_account.yaml +1 -0
  59. package/package.json +3 -3
  60. package/src/access-log/access-log.service.spec.ts +235 -0
  61. package/src/ai-instruction/ai-prompt-builder.service.spec.ts +250 -0
  62. package/src/auth/auth.service.spec.ts +891 -0
  63. package/src/auth/guards/auth.guard.spec.ts +177 -0
  64. package/src/challenge/challenge.service.spec.ts +334 -0
  65. package/src/dashboard/dashboard-core/dashboard-core.service.spec.ts +193 -0
  66. package/src/file/image-optimization.service.spec.ts +283 -0
  67. package/src/file/provider/provider.factory.spec.ts +111 -0
  68. package/src/install/install.service.spec.ts +139 -0
  69. package/src/integration/services/event-subscriber.registry.spec.ts +143 -0
  70. package/src/integration/services/inbox.service.spec.ts +183 -0
  71. package/src/integration/services/integration-link.service.spec.ts +217 -0
  72. package/src/integration/services/outbox.service.spec.ts +290 -0
  73. package/src/integration-profile/integration-credential-crypto.service.spec.ts +289 -0
  74. package/src/integration-profile/integration-profile.secrets.ts +4 -0
  75. package/src/integration-profile/integration-profile.utils.spec.ts +332 -0
  76. package/src/integration-profile/integration-profile.utils.ts +14 -1
  77. package/src/locale/locale.service.spec.ts +78 -0
  78. package/src/mail/mail.service.spec.ts +257 -0
  79. package/src/mcp/guards/mcp-auth.guard.spec.ts +87 -0
  80. package/src/mcp/guards/mcp-permission.guard.spec.ts +64 -0
  81. package/src/mcp/mcp-api-key.service.spec.ts +125 -0
  82. package/src/mcp/mcp-registry.service.spec.ts +96 -0
  83. package/src/mcp/mcp-server.service.spec.ts +187 -0
  84. package/src/menu/menu.service.spec.ts +76 -0
  85. package/src/notification/notification.service.spec.ts +297 -0
  86. package/src/oauth/oauth.controller.ts +53 -10
  87. package/src/oauth/oauth.module.ts +4 -0
  88. package/src/oauth/oauth.service.spec.ts +332 -0
  89. package/src/oauth/oauth.service.ts +156 -1
  90. package/src/oauth/providers/apple.provider.ts +175 -0
  91. package/src/oauth/providers/facebook.provider.ts +3 -2
  92. package/src/oauth/providers/google.provider.ts +3 -2
  93. package/src/oauth/providers/linkedin.provider.ts +96 -0
  94. package/src/oauth/providers/microsoft-entra-id.provider.ts +1 -1
  95. package/src/oauth/providers/microsoft.provider.ts +3 -2
  96. package/src/pdf/pdf.service.ts +20 -2
  97. package/src/role/guards/role.guard.spec.ts +157 -0
  98. package/src/role/role.service.spec.ts +179 -0
  99. package/src/session/session.service.spec.ts +376 -0
  100. package/src/setting/setting.service.spec.ts +316 -0
  101. package/src/setting/setting.service.ts +4 -1
  102. package/src/task/task.service.spec.ts +47 -0
  103. package/src/token/token.service.spec.ts +351 -0
  104. package/src/user/user.service.spec.ts +256 -0
  105. package/src/utils/locale-context.spec.ts +22 -0
  106. package/src/utils/webauthn-origin.spec.ts +73 -0
  107. package/src/webhook-integration/integration-event-catalog.service.spec.ts +149 -0
@@ -0,0 +1,283 @@
1
+ import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals';
2
+
3
+ jest.mock('sharp', () => {
4
+ const instance: any = {
5
+ metadata: jest.fn(),
6
+ resize: jest.fn(),
7
+ webp: jest.fn(),
8
+ jpeg: jest.fn(),
9
+ png: jest.fn(),
10
+ toBuffer: jest.fn(),
11
+ };
12
+ const fn: any = jest.fn(() => instance);
13
+ fn.__instance = instance;
14
+ return { __esModule: true, default: fn };
15
+ });
16
+
17
+ import sharp from 'sharp';
18
+ import { ImageOptimizationService } from './image-optimization.service';
19
+
20
+ const mockSharp = sharp as unknown as jest.Mock & { __instance: any };
21
+
22
+ const DEFAULT_SETTINGS: Record<string, string | undefined> = {
23
+ 'image-compression-enabled': 'false',
24
+ 'image-compression-quality': undefined,
25
+ 'image-compression-types': undefined,
26
+ 'image-webp-conversion-enabled': 'false',
27
+ 'image-webp-convert-types': undefined,
28
+ 'image-max-dimension': undefined,
29
+ };
30
+
31
+ const makeService = (settings: Record<string, string | undefined> = {}) => {
32
+ const merged = { ...DEFAULT_SETTINGS, ...settings };
33
+ const settingService = {
34
+ getSettingValues: jest.fn(async () => merged),
35
+ };
36
+ const service = new ImageOptimizationService(settingService as any);
37
+ return { service, settingService, merged };
38
+ };
39
+
40
+ describe('ImageOptimizationService', () => {
41
+ beforeEach(() => {
42
+ const inst = mockSharp.__instance;
43
+ mockSharp.mockClear();
44
+ inst.metadata.mockReset();
45
+ inst.resize.mockReset().mockReturnValue(inst);
46
+ inst.webp.mockReset().mockReturnValue(inst);
47
+ inst.jpeg.mockReset().mockReturnValue(inst);
48
+ inst.png.mockReset().mockReturnValue(inst);
49
+ inst.toBuffer.mockReset();
50
+ });
51
+
52
+ afterEach(() => {
53
+ jest.clearAllMocks();
54
+ });
55
+
56
+ describe('parseArraySetting (privado)', () => {
57
+ it('faz parse de JSON array válido', () => {
58
+ const { service } = makeService();
59
+ expect((service as any).parseArraySetting('["a","b"]')).toEqual(['a', 'b']);
60
+ });
61
+
62
+ it('retorna [] para undefined', () => {
63
+ const { service } = makeService();
64
+ expect((service as any).parseArraySetting(undefined)).toEqual([]);
65
+ });
66
+
67
+ it('retorna [] para JSON inválido', () => {
68
+ const { service } = makeService();
69
+ expect((service as any).parseArraySetting('{not json')).toEqual([]);
70
+ });
71
+
72
+ it('retorna [] quando o JSON não é um array', () => {
73
+ const { service } = makeService();
74
+ expect((service as any).parseArraySetting('{"a":1}')).toEqual([]);
75
+ });
76
+ });
77
+
78
+ describe('replaceExtension (privado)', () => {
79
+ it('troca a extensão preservando o nome base', () => {
80
+ const { service } = makeService();
81
+ expect((service as any).replaceExtension('photo.jpg', 'webp')).toBe('photo.webp');
82
+ expect((service as any).replaceExtension('a.b.png', 'webp')).toBe('a.b.webp');
83
+ expect((service as any).replaceExtension('noext', 'png')).toBe('noext.png');
84
+ });
85
+ });
86
+
87
+ describe('pngCompressionLevel (privado)', () => {
88
+ it('inverte a escala de quality para 0-9', () => {
89
+ const { service } = makeService();
90
+ expect((service as any).pngCompressionLevel(100)).toBe(0);
91
+ expect((service as any).pngCompressionLevel(80)).toBe(2);
92
+ expect((service as any).pngCompressionLevel(1)).toBe(9);
93
+ });
94
+ });
95
+
96
+ describe('detectImageMimetype', () => {
97
+ it('mapeia o format do sharp para o MIME', async () => {
98
+ const { service } = makeService();
99
+ mockSharp.__instance.metadata.mockResolvedValue({ format: 'jpeg' });
100
+ expect(await service.detectImageMimetype(Buffer.from('x'))).toBe('image/jpeg');
101
+
102
+ mockSharp.__instance.metadata.mockResolvedValue({ format: 'png' });
103
+ expect(await service.detectImageMimetype(Buffer.from('x'))).toBe('image/png');
104
+ });
105
+
106
+ it('retorna null para format desconhecido', async () => {
107
+ const { service } = makeService();
108
+ mockSharp.__instance.metadata.mockResolvedValue({ format: 'xyz' });
109
+ expect(await service.detectImageMimetype(Buffer.from('x'))).toBeNull();
110
+ });
111
+
112
+ it('retorna null quando não há format', async () => {
113
+ const { service } = makeService();
114
+ mockSharp.__instance.metadata.mockResolvedValue({});
115
+ expect(await service.detectImageMimetype(Buffer.from('x'))).toBeNull();
116
+ });
117
+
118
+ it('retorna null quando o sharp lança (buffer não é imagem)', async () => {
119
+ const { service } = makeService();
120
+ mockSharp.__instance.metadata.mockRejectedValue(new Error('bad'));
121
+ expect(await service.detectImageMimetype(Buffer.from('x'))).toBeNull();
122
+ });
123
+ });
124
+
125
+ describe('optimizeBuffer', () => {
126
+ it('não toca em não-imagens e nem lê settings', async () => {
127
+ const { service, settingService } = makeService();
128
+ const buffer = Buffer.from('pdf');
129
+ const result = await service.optimizeBuffer(buffer, 'application/pdf', 'doc.pdf');
130
+ expect(result).toEqual({ buffer, mimetype: 'application/pdf', originalname: 'doc.pdf' });
131
+ expect(settingService.getSettingValues).not.toHaveBeenCalled();
132
+ });
133
+
134
+ it('retorna o original quando não há resize, compressão nem conversão', async () => {
135
+ const { service } = makeService();
136
+ const buffer = Buffer.from('img');
137
+ const result = await service.optimizeBuffer(buffer, 'image/jpeg', 'a.jpg');
138
+ expect(result).toEqual({ buffer, mimetype: 'image/jpeg', originalname: 'a.jpg' });
139
+ expect(mockSharp).not.toHaveBeenCalled();
140
+ });
141
+
142
+ it('converte para webp quando habilitado e o tipo está na lista', async () => {
143
+ const { service } = makeService({
144
+ 'image-webp-conversion-enabled': 'true',
145
+ 'image-webp-convert-types': '["image/jpeg"]',
146
+ 'image-compression-quality': '70',
147
+ });
148
+ const out = Buffer.from('webp-bytes');
149
+ mockSharp.__instance.toBuffer.mockResolvedValue(out);
150
+
151
+ const result = await service.optimizeBuffer(Buffer.from('img'), 'image/jpeg', 'photo.jpg');
152
+
153
+ expect(result).toEqual({
154
+ buffer: out,
155
+ mimetype: 'image/webp',
156
+ originalname: 'photo.webp',
157
+ });
158
+ expect(mockSharp.__instance.webp).toHaveBeenCalledWith({ quality: 70 });
159
+ });
160
+
161
+ it('comprime jpeg mantendo o mesmo mimetype/nome', async () => {
162
+ const { service } = makeService({
163
+ 'image-compression-enabled': 'true',
164
+ 'image-compression-types': '["image/jpeg"]',
165
+ 'image-compression-quality': '60',
166
+ });
167
+ const out = Buffer.from('jpeg-small');
168
+ mockSharp.__instance.toBuffer.mockResolvedValue(out);
169
+
170
+ const result = await service.optimizeBuffer(Buffer.from('img'), 'image/jpeg', 'a.jpg');
171
+
172
+ expect(result).toEqual({ buffer: out, mimetype: 'image/jpeg', originalname: 'a.jpg' });
173
+ expect(mockSharp.__instance.jpeg).toHaveBeenCalledWith({ quality: 60 });
174
+ });
175
+
176
+ it('comprime png usando compressionLevel derivado da quality', async () => {
177
+ const { service } = makeService({
178
+ 'image-compression-enabled': 'true',
179
+ 'image-compression-types': '["image/png"]',
180
+ 'image-compression-quality': '80',
181
+ });
182
+ const out = Buffer.from('png-small');
183
+ mockSharp.__instance.toBuffer.mockResolvedValue(out);
184
+
185
+ const result = await service.optimizeBuffer(Buffer.from('img'), 'image/png', 'a.png');
186
+
187
+ expect(result.mimetype).toBe('image/png');
188
+ // quality 80 -> compressionLevel 2
189
+ expect(mockSharp.__instance.png).toHaveBeenCalledWith({ compressionLevel: 2 });
190
+ });
191
+
192
+ it('converte bmp/tiff para png', async () => {
193
+ const { service } = makeService({
194
+ 'image-compression-enabled': 'true',
195
+ 'image-compression-types': '["image/tiff"]',
196
+ });
197
+ const out = Buffer.from('png-from-tiff');
198
+ mockSharp.__instance.toBuffer.mockResolvedValue(out);
199
+
200
+ const result = await service.optimizeBuffer(Buffer.from('img'), 'image/tiff', 'scan.tiff');
201
+
202
+ expect(result).toEqual({
203
+ buffer: out,
204
+ mimetype: 'image/png',
205
+ originalname: 'scan.png',
206
+ });
207
+ });
208
+
209
+ it('aplica resize por max-dimension com fit inside', async () => {
210
+ const { service } = makeService({ 'image-max-dimension': '1024' });
211
+ const out = Buffer.from('resized');
212
+ mockSharp.__instance.toBuffer.mockResolvedValue(out);
213
+
214
+ const result = await service.optimizeBuffer(Buffer.from('img'), 'image/jpeg', 'a.jpg');
215
+
216
+ expect(mockSharp.__instance.resize).toHaveBeenCalledWith({
217
+ width: 1024,
218
+ height: 1024,
219
+ fit: 'inside',
220
+ withoutEnlargement: true,
221
+ });
222
+ expect(result.buffer).toBe(out);
223
+ });
224
+
225
+ it('o alvo preciso (maxWidth/maxHeight) tem prioridade sobre max-dimension', async () => {
226
+ const { service } = makeService({ 'image-max-dimension': '1024' });
227
+ mockSharp.__instance.toBuffer.mockResolvedValue(Buffer.from('cropped'));
228
+
229
+ await service.optimizeBuffer(Buffer.from('img'), 'image/jpeg', 'a.jpg', {
230
+ maxWidth: 200,
231
+ maxHeight: 100,
232
+ });
233
+
234
+ expect(mockSharp.__instance.resize).toHaveBeenCalledWith({
235
+ width: 200,
236
+ height: 100,
237
+ fit: 'cover',
238
+ });
239
+ });
240
+
241
+ it('formato desconhecido com resize apenas re-encoda o buffer', async () => {
242
+ const { service } = makeService({ 'image-max-dimension': '500' });
243
+ const out = Buffer.from('reencoded');
244
+ mockSharp.__instance.toBuffer.mockResolvedValue(out);
245
+
246
+ const result = await service.optimizeBuffer(Buffer.from('img'), 'image/gif', 'a.gif');
247
+
248
+ expect(result).toEqual({ buffer: out, mimetype: 'image/gif', originalname: 'a.gif' });
249
+ expect(mockSharp.__instance.jpeg).not.toHaveBeenCalled();
250
+ expect(mockSharp.__instance.png).not.toHaveBeenCalled();
251
+ });
252
+
253
+ it('faz fallback para o original quando o sharp lança', async () => {
254
+ const { service } = makeService({
255
+ 'image-compression-enabled': 'true',
256
+ 'image-compression-types': '["image/jpeg"]',
257
+ });
258
+ mockSharp.__instance.toBuffer.mockRejectedValue(new Error('sharp boom'));
259
+ const warnSpy = jest
260
+ .spyOn((service as any).logger, 'warn')
261
+ .mockImplementation(() => undefined);
262
+ const buffer = Buffer.from('img');
263
+
264
+ const result = await service.optimizeBuffer(buffer, 'image/jpeg', 'a.jpg');
265
+
266
+ expect(result).toEqual({ buffer, mimetype: 'image/jpeg', originalname: 'a.jpg' });
267
+ expect(warnSpy).toHaveBeenCalled();
268
+ });
269
+
270
+ it('faz clamp da quality inválida para o default 80', async () => {
271
+ const { service } = makeService({
272
+ 'image-compression-enabled': 'true',
273
+ 'image-compression-types': '["image/jpeg"]',
274
+ 'image-compression-quality': 'not-a-number',
275
+ });
276
+ mockSharp.__instance.toBuffer.mockResolvedValue(Buffer.from('x'));
277
+
278
+ await service.optimizeBuffer(Buffer.from('img'), 'image/jpeg', 'a.jpg');
279
+
280
+ expect(mockSharp.__instance.jpeg).toHaveBeenCalledWith({ quality: 80 });
281
+ });
282
+ });
283
+ });
@@ -0,0 +1,111 @@
1
+ import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals';
2
+
3
+ const s3InitValidation = jest.fn<any>().mockResolvedValue(undefined);
4
+ const localInitValidation = jest.fn<any>().mockResolvedValue(undefined);
5
+ const azureInitValidation = jest.fn<any>().mockResolvedValue(undefined);
6
+ const gcsInitValidation = jest.fn<any>().mockResolvedValue(undefined);
7
+ const spacesInitValidation = jest.fn<any>().mockResolvedValue(undefined);
8
+
9
+ jest.mock('./s3.provider', () => ({
10
+ S3Provider: jest.fn().mockImplementation((setting: any) => ({
11
+ kind: 'S3Provider',
12
+ setting,
13
+ initValidation: s3InitValidation,
14
+ })),
15
+ }));
16
+
17
+ jest.mock('./local.provider', () => ({
18
+ LocalProvider: jest.fn().mockImplementation((setting: any, jwtService: any) => ({
19
+ kind: 'LocalProvider',
20
+ setting,
21
+ jwtService,
22
+ initValidation: localInitValidation,
23
+ })),
24
+ }));
25
+
26
+ jest.mock('./azure.provider', () => ({
27
+ AzureProvider: jest.fn().mockImplementation((setting: any) => ({
28
+ kind: 'AzureProvider',
29
+ setting,
30
+ initValidation: azureInitValidation,
31
+ })),
32
+ }));
33
+
34
+ jest.mock('./gcs.provider', () => ({
35
+ GCSProvider: jest.fn().mockImplementation((setting: any) => ({
36
+ kind: 'GCSProvider',
37
+ setting,
38
+ initValidation: gcsInitValidation,
39
+ })),
40
+ }));
41
+
42
+ jest.mock('./spaces.provider', () => ({
43
+ SpacesProvider: jest.fn().mockImplementation((setting: any) => ({
44
+ kind: 'SpacesProvider',
45
+ setting,
46
+ initValidation: spacesInitValidation,
47
+ })),
48
+ }));
49
+
50
+ import { ProviderFactory } from './provider.factory';
51
+ import { EnumProvider } from './provider.enum';
52
+ import { S3Provider } from './s3.provider';
53
+ import { LocalProvider } from './local.provider';
54
+ import { AzureProvider } from './azure.provider';
55
+ import { GCSProvider } from './gcs.provider';
56
+ import { SpacesProvider } from './spaces.provider';
57
+
58
+ describe('ProviderFactory', () => {
59
+ const setting = { foo: 'bar' };
60
+ const jwtService = { sign: jest.fn() } as any;
61
+
62
+ afterEach(() => jest.clearAllMocks());
63
+
64
+ it('cria S3Provider para EnumProvider.S3 e roda initValidation', async () => {
65
+ const provider = await ProviderFactory.create(EnumProvider.S3, setting, jwtService);
66
+ expect(S3Provider).toHaveBeenCalledWith(setting);
67
+ expect(s3InitValidation).toHaveBeenCalledTimes(1);
68
+ expect((provider as any).kind).toBe('S3Provider');
69
+ });
70
+
71
+ it('cria LocalProvider para EnumProvider.LOCAL passando o jwtService', async () => {
72
+ const provider = await ProviderFactory.create(EnumProvider.LOCAL, setting, jwtService);
73
+ expect(LocalProvider).toHaveBeenCalledWith(setting, jwtService);
74
+ expect(localInitValidation).toHaveBeenCalledTimes(1);
75
+ expect((provider as any).kind).toBe('LocalProvider');
76
+ });
77
+
78
+ it('cria AzureProvider para EnumProvider.AZURE', async () => {
79
+ const provider = await ProviderFactory.create(EnumProvider.AZURE, setting, jwtService);
80
+ expect(AzureProvider).toHaveBeenCalledWith(setting);
81
+ expect(azureInitValidation).toHaveBeenCalledTimes(1);
82
+ expect((provider as any).kind).toBe('AzureProvider');
83
+ });
84
+
85
+ it('cria GCSProvider para EnumProvider.GCS', async () => {
86
+ const provider = await ProviderFactory.create(EnumProvider.GCS, setting, jwtService);
87
+ expect(GCSProvider).toHaveBeenCalledWith(setting);
88
+ expect(gcsInitValidation).toHaveBeenCalledTimes(1);
89
+ expect((provider as any).kind).toBe('GCSProvider');
90
+ });
91
+
92
+ it('cria SpacesProvider para EnumProvider.SPACES', async () => {
93
+ const provider = await ProviderFactory.create(EnumProvider.SPACES, setting, jwtService);
94
+ expect(SpacesProvider).toHaveBeenCalledWith(setting);
95
+ expect(spacesInitValidation).toHaveBeenCalledTimes(1);
96
+ expect((provider as any).kind).toBe('SpacesProvider');
97
+ });
98
+
99
+ it('lança erro para tipo de provider não suportado', async () => {
100
+ await expect(
101
+ ProviderFactory.create('invalid' as EnumProvider, setting, jwtService),
102
+ ).rejects.toThrow('Unsupported provider type: invalid');
103
+ });
104
+
105
+ it('propaga erro lançado por initValidation (ex.: configuração inválida)', async () => {
106
+ s3InitValidation.mockRejectedValueOnce(new Error('missing key'));
107
+ await expect(
108
+ ProviderFactory.create(EnumProvider.S3, setting, jwtService),
109
+ ).rejects.toThrow('missing key');
110
+ });
111
+ });
@@ -0,0 +1,139 @@
1
+ import {
2
+ describe,
3
+ it,
4
+ expect,
5
+ beforeEach,
6
+ afterEach,
7
+ jest,
8
+ } from '@jest/globals';
9
+ import { BadRequestException } from '@nestjs/common';
10
+ import { InstallService } from './install.service';
11
+
12
+ const makeDeps = () => {
13
+ const security = {
14
+ randomOpaque: jest.fn().mockReturnValue('rnd'),
15
+ hashArgon2: jest.fn().mockResolvedValue('hashed'),
16
+ };
17
+ const prisma = {
18
+ setting: { findFirst: jest.fn() },
19
+ user: { count: jest.fn() },
20
+ $transaction: jest.fn(),
21
+ };
22
+ const configService = { get: jest.fn() };
23
+ const settingService = { clearCache: jest.fn() };
24
+ return { security, prisma, configService, settingService };
25
+ };
26
+
27
+ const makeService = (deps = makeDeps()) => ({
28
+ service: new InstallService(
29
+ deps.security as any,
30
+ deps.prisma as any,
31
+ deps.configService as any,
32
+ deps.settingService as any,
33
+ ),
34
+ deps,
35
+ });
36
+
37
+ describe('InstallService', () => {
38
+ afterEach(() => jest.clearAllMocks());
39
+
40
+ describe('normalizeUrl', () => {
41
+ let service: InstallService;
42
+ beforeEach(() => {
43
+ service = makeService().service;
44
+ });
45
+ const call = (v: string) => (service as any).normalizeUrl(v);
46
+
47
+ it('faz trim e remove barra final', () => {
48
+ expect(call(' http://x.com/ ')).toBe('http://x.com');
49
+ expect(call('http://x.com')).toBe('http://x.com');
50
+ expect(call('http://x.com/path/')).toBe('http://x.com/path');
51
+ });
52
+ });
53
+
54
+ describe('base64Encode', () => {
55
+ it('codifica string em base64', () => {
56
+ const { service } = makeService();
57
+ expect((service as any).base64Encode('hello')).toBe(
58
+ Buffer.from('hello').toString('base64'),
59
+ );
60
+ });
61
+ });
62
+
63
+ describe('parseEnv', () => {
64
+ let service: InstallService;
65
+ beforeEach(() => {
66
+ service = makeService().service;
67
+ });
68
+ const call = (v: string) => (service as any).parseEnv(v);
69
+
70
+ it('ignora comentários e linhas sem "="', () => {
71
+ const result = call('# comment\nFOO=bar\ngarbage\nBAZ=1');
72
+ expect(result).toEqual({ FOO: 'bar', BAZ: '1' });
73
+ });
74
+
75
+ it('remove aspas envolventes e desescapa aspas internas', () => {
76
+ const result = call('A="hello world"\nB="a\\"b"');
77
+ expect(result.A).toBe('hello world');
78
+ expect(result.B).toBe('a"b');
79
+ });
80
+
81
+ it('preserva "=" no valor', () => {
82
+ const result = call('URL=postgres://u:p@h/db?x=1');
83
+ expect(result.URL).toBe('postgres://u:p@h/db?x=1');
84
+ });
85
+ });
86
+
87
+ describe('checkInstallation', () => {
88
+ it('true quando setting installed=true (sem contar usuários)', async () => {
89
+ const { service, deps } = makeService();
90
+ deps.prisma.setting.findFirst.mockResolvedValue({ value: 'true' });
91
+ await expect((service as any).checkInstallation()).resolves.toBe(true);
92
+ expect(deps.prisma.user.count).not.toHaveBeenCalled();
93
+ });
94
+
95
+ it('true quando existem usuários', async () => {
96
+ const { service, deps } = makeService();
97
+ deps.prisma.setting.findFirst.mockResolvedValue(null);
98
+ deps.prisma.user.count.mockResolvedValue(2);
99
+ await expect((service as any).checkInstallation()).resolves.toBe(true);
100
+ });
101
+
102
+ it('false quando não instalado e sem usuários', async () => {
103
+ const { service, deps } = makeService();
104
+ deps.prisma.setting.findFirst.mockResolvedValue({ value: 'false' });
105
+ deps.prisma.user.count.mockResolvedValue(0);
106
+ await expect((service as any).checkInstallation()).resolves.toBe(false);
107
+ });
108
+ });
109
+
110
+ describe('check', () => {
111
+ it('retorna estado de instalação', async () => {
112
+ const { service, deps } = makeService();
113
+ deps.prisma.setting.findFirst.mockResolvedValue({ value: 'true' });
114
+ await expect(service.check()).resolves.toEqual({
115
+ success: true,
116
+ installed: true,
117
+ });
118
+ });
119
+ });
120
+
121
+ describe('install', () => {
122
+ it('lança BadRequest quando já instalado', async () => {
123
+ const { service, deps } = makeService();
124
+ deps.prisma.setting.findFirst.mockResolvedValue({ value: 'true' });
125
+ await expect(
126
+ service.install({
127
+ adminUrl: 'http://a',
128
+ apiUrl: 'http://b',
129
+ appName: 'App',
130
+ email: 'e@x.com',
131
+ password: 'pw',
132
+ slogan: 's',
133
+ userName: 'u',
134
+ } as any),
135
+ ).rejects.toThrow(BadRequestException);
136
+ expect(deps.prisma.$transaction).not.toHaveBeenCalled();
137
+ });
138
+ });
139
+ });
@@ -0,0 +1,143 @@
1
+ import { describe, it, expect, beforeEach, jest } from '@jest/globals';
2
+ import { EventSubscriberRegistry } from './event-subscriber.registry';
3
+ import { SubscriberDefinition } from '../types';
4
+
5
+ const makeDef = (
6
+ overrides: Partial<SubscriberDefinition> & { eventName: string },
7
+ ): SubscriberDefinition => ({
8
+ consumerName: 'consumer',
9
+ handler: jest.fn() as any,
10
+ ...overrides,
11
+ });
12
+
13
+ describe('EventSubscriberRegistry', () => {
14
+ let registry: EventSubscriberRegistry;
15
+
16
+ beforeEach(() => {
17
+ registry = new EventSubscriberRegistry();
18
+ });
19
+
20
+ describe('registerHandler / getHandlers', () => {
21
+ it('cria a lista e retorna o handler registrado', () => {
22
+ const def = makeDef({ eventName: 'user.created' });
23
+ registry.registerHandler(def);
24
+ expect(registry.getHandlers('user.created')).toEqual([def]);
25
+ });
26
+
27
+ it('ordena por prioridade (maior primeiro)', () => {
28
+ registry.registerHandler(
29
+ makeDef({ eventName: 'e', consumerName: 'low', priority: 1 }),
30
+ );
31
+ registry.registerHandler(
32
+ makeDef({ eventName: 'e', consumerName: 'high', priority: 10 }),
33
+ );
34
+ registry.registerHandler(
35
+ makeDef({ eventName: 'e', consumerName: 'mid', priority: 5 }),
36
+ );
37
+
38
+ const names = registry.getHandlers('e').map((h) => h.consumerName);
39
+ expect(names).toEqual(['high', 'mid', 'low']);
40
+ });
41
+
42
+ it('trata prioridade ausente como 0', () => {
43
+ registry.registerHandler(
44
+ makeDef({ eventName: 'e', consumerName: 'no-prio' }),
45
+ );
46
+ registry.registerHandler(
47
+ makeDef({ eventName: 'e', consumerName: 'prio', priority: 3 }),
48
+ );
49
+ const names = registry.getHandlers('e').map((h) => h.consumerName);
50
+ expect(names).toEqual(['prio', 'no-prio']);
51
+ });
52
+
53
+ it('retorna array vazio para evento desconhecido', () => {
54
+ expect(registry.getHandlers('nope')).toEqual([]);
55
+ });
56
+ });
57
+
58
+ describe('subscribe / subscribeMany', () => {
59
+ it('subscribe registra com prioridade default 0', () => {
60
+ const handler = jest.fn() as any;
61
+ registry.subscribe('e', 'c', handler);
62
+ const handlers = registry.getHandlers('e');
63
+ expect(handlers).toHaveLength(1);
64
+ expect(handlers[0]).toMatchObject({
65
+ eventName: 'e',
66
+ consumerName: 'c',
67
+ priority: 0,
68
+ });
69
+ });
70
+
71
+ it('subscribeMany registra todos', () => {
72
+ registry.subscribeMany([
73
+ makeDef({ eventName: 'a' }),
74
+ makeDef({ eventName: 'b' }),
75
+ makeDef({ eventName: 'a', consumerName: 'other' }),
76
+ ]);
77
+ expect(registry.getHandlers('a')).toHaveLength(2);
78
+ expect(registry.getHandlers('b')).toHaveLength(1);
79
+ });
80
+ });
81
+
82
+ describe('hasHandlers', () => {
83
+ it('retorna true quando há handlers', () => {
84
+ registry.subscribe('e', 'c', jest.fn() as any);
85
+ expect(registry.hasHandlers('e')).toBe(true);
86
+ });
87
+
88
+ it('retorna false para evento inexistente', () => {
89
+ expect(registry.hasHandlers('missing')).toBe(false);
90
+ });
91
+ });
92
+
93
+ describe('getEventNames / getHandlerCount', () => {
94
+ it('retorna nomes de eventos registrados', () => {
95
+ registry.subscribe('a', 'c', jest.fn() as any);
96
+ registry.subscribe('b', 'c', jest.fn() as any);
97
+ expect(registry.getEventNames().sort()).toEqual(['a', 'b']);
98
+ });
99
+
100
+ it('conta todos os handlers em todos os eventos', () => {
101
+ registry.subscribe('a', 'c1', jest.fn() as any);
102
+ registry.subscribe('a', 'c2', jest.fn() as any);
103
+ registry.subscribe('b', 'c3', jest.fn() as any);
104
+ expect(registry.getHandlerCount()).toBe(3);
105
+ });
106
+
107
+ it('conta zero quando vazio', () => {
108
+ expect(registry.getHandlerCount()).toBe(0);
109
+ });
110
+ });
111
+
112
+ describe('removeByConsumerPrefix', () => {
113
+ it('remove apenas handlers com o prefixo e apaga eventos que ficam vazios', () => {
114
+ registry.subscribe('a', 'agent:x', jest.fn() as any);
115
+ registry.subscribe('a', 'other', jest.fn() as any);
116
+ registry.subscribe('b', 'agent:y', jest.fn() as any);
117
+
118
+ registry.removeByConsumerPrefix('agent:');
119
+
120
+ expect(registry.getHandlers('a').map((h) => h.consumerName)).toEqual([
121
+ 'other',
122
+ ]);
123
+ expect(registry.hasHandlers('b')).toBe(false);
124
+ expect(registry.getEventNames()).toEqual(['a']);
125
+ });
126
+
127
+ it('não altera nada quando nenhum consumer casa com o prefixo', () => {
128
+ registry.subscribe('a', 'keep', jest.fn() as any);
129
+ registry.removeByConsumerPrefix('none:');
130
+ expect(registry.getHandlerCount()).toBe(1);
131
+ });
132
+ });
133
+
134
+ describe('clear', () => {
135
+ it('remove todos os handlers', () => {
136
+ registry.subscribe('a', 'c', jest.fn() as any);
137
+ registry.subscribe('b', 'c', jest.fn() as any);
138
+ registry.clear();
139
+ expect(registry.getHandlerCount()).toBe(0);
140
+ expect(registry.getEventNames()).toEqual([]);
141
+ });
142
+ });
143
+ });