@flusys/nestjs-localization 6.0.2 → 6.1.1
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/cjs/controllers/language.controller.spec.js +154 -0
- package/cjs/controllers/translation-key.controller.spec.js +70 -0
- package/cjs/controllers/translation.controller.spec.js +169 -0
- package/cjs/docs/localization-swagger.config.spec.js +30 -0
- package/cjs/entities/index.spec.js +18 -0
- package/cjs/modules/localization.module.spec.js +132 -0
- package/cjs/services/language.service.spec.js +151 -0
- package/cjs/services/localization-config.service.spec.js +80 -0
- package/cjs/services/localization-datasource.provider.spec.js +85 -0
- package/cjs/services/translation-key.service.spec.js +282 -0
- package/cjs/services/translation.service.spec.js +426 -0
- package/fesm/controllers/language.controller.spec.js +150 -0
- package/fesm/controllers/translation-key.controller.spec.js +66 -0
- package/fesm/controllers/translation.controller.spec.js +165 -0
- package/fesm/docs/localization-swagger.config.spec.js +26 -0
- package/fesm/entities/index.spec.js +14 -0
- package/fesm/modules/localization.module.spec.js +128 -0
- package/fesm/services/language.service.spec.js +147 -0
- package/fesm/services/localization-config.service.spec.js +76 -0
- package/fesm/services/localization-datasource.provider.spec.js +81 -0
- package/fesm/services/translation-key.service.spec.js +278 -0
- package/fesm/services/translation.service.spec.js +422 -0
- package/package.json +3 -3
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
const _testing = require("@nestjs/testing");
|
|
6
|
+
const _modules = require("@flusys/nestjs-shared/modules");
|
|
7
|
+
const _repositorymock = require("@test-utils/mocks/repository.mock");
|
|
8
|
+
const _languageservice = require("./language.service");
|
|
9
|
+
const _localizationdatasourceprovider = require("./localization-datasource.provider");
|
|
10
|
+
function buildLanguage(overrides = {}) {
|
|
11
|
+
return {
|
|
12
|
+
id: 'language-uuid-1',
|
|
13
|
+
code: 'en',
|
|
14
|
+
name: 'English',
|
|
15
|
+
nativeName: 'English',
|
|
16
|
+
direction: 'ltr',
|
|
17
|
+
isActive: true,
|
|
18
|
+
isDefault: false,
|
|
19
|
+
serial: 1,
|
|
20
|
+
isReadonly: false,
|
|
21
|
+
createdAt: new Date(),
|
|
22
|
+
updatedAt: new Date(),
|
|
23
|
+
deletedAt: null,
|
|
24
|
+
createdById: null,
|
|
25
|
+
updatedById: null,
|
|
26
|
+
deletedById: null,
|
|
27
|
+
...overrides
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
describe('LanguageService', ()=>{
|
|
31
|
+
let service;
|
|
32
|
+
let mockRepo;
|
|
33
|
+
beforeEach(async ()=>{
|
|
34
|
+
mockRepo = (0, _repositorymock.createMockRepository)();
|
|
35
|
+
const mockDataSourceProvider = (0, _repositorymock.createMockDataSourceProvider)(mockRepo);
|
|
36
|
+
const module = await _testing.Test.createTestingModule({
|
|
37
|
+
providers: [
|
|
38
|
+
_languageservice.LanguageService,
|
|
39
|
+
{
|
|
40
|
+
provide: 'CACHE_INSTANCE',
|
|
41
|
+
useValue: {}
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
provide: _modules.UtilsService,
|
|
45
|
+
useValue: {}
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
provide: _localizationdatasourceprovider.LocalizationDataSourceProvider,
|
|
49
|
+
useValue: mockDataSourceProvider
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
}).compile();
|
|
53
|
+
service = await module.resolve(_languageservice.LanguageService);
|
|
54
|
+
});
|
|
55
|
+
describe('getDefaultLanguage', ()=>{
|
|
56
|
+
it('returns the language flagged as default', async ()=>{
|
|
57
|
+
const defaultLanguage = buildLanguage({
|
|
58
|
+
id: 'default-lang',
|
|
59
|
+
isDefault: true,
|
|
60
|
+
code: 'en'
|
|
61
|
+
});
|
|
62
|
+
mockRepo.findOne.mockResolvedValue(defaultLanguage);
|
|
63
|
+
const result = await service.getDefaultLanguage();
|
|
64
|
+
expect(mockRepo.findOne).toHaveBeenCalledWith({
|
|
65
|
+
where: {
|
|
66
|
+
isDefault: true,
|
|
67
|
+
deletedAt: expect.anything()
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
expect(result).toEqual(defaultLanguage);
|
|
71
|
+
});
|
|
72
|
+
it('returns null when no language is marked default', async ()=>{
|
|
73
|
+
mockRepo.findOne.mockResolvedValue(null);
|
|
74
|
+
const result = await service.getDefaultLanguage();
|
|
75
|
+
expect(result).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
describe('getActiveLanguages', ()=>{
|
|
79
|
+
it('returns active, non-deleted languages ordered by serial then name', async ()=>{
|
|
80
|
+
const languages = [
|
|
81
|
+
buildLanguage({
|
|
82
|
+
code: 'en',
|
|
83
|
+
serial: 1
|
|
84
|
+
}),
|
|
85
|
+
buildLanguage({
|
|
86
|
+
code: 'bn',
|
|
87
|
+
serial: 2
|
|
88
|
+
})
|
|
89
|
+
];
|
|
90
|
+
mockRepo.find.mockResolvedValue(languages);
|
|
91
|
+
const result = await service.getActiveLanguages();
|
|
92
|
+
expect(mockRepo.find).toHaveBeenCalledWith({
|
|
93
|
+
where: {
|
|
94
|
+
isActive: true,
|
|
95
|
+
deletedAt: expect.anything()
|
|
96
|
+
},
|
|
97
|
+
order: {
|
|
98
|
+
serial: 'ASC',
|
|
99
|
+
name: 'ASC'
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
expect(result).toHaveLength(2);
|
|
103
|
+
});
|
|
104
|
+
it('returns an empty array when no languages are active', async ()=>{
|
|
105
|
+
mockRepo.find.mockResolvedValue([]);
|
|
106
|
+
const result = await service.getActiveLanguages();
|
|
107
|
+
expect(result).toEqual([]);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
describe('setAsDefault', ()=>{
|
|
111
|
+
it('unsets the previous default and sets the requested language as default', async ()=>{
|
|
112
|
+
const newDefault = buildLanguage({
|
|
113
|
+
id: 'lang-2',
|
|
114
|
+
isDefault: true,
|
|
115
|
+
code: 'bn'
|
|
116
|
+
});
|
|
117
|
+
mockRepo.findOne.mockResolvedValue(newDefault);
|
|
118
|
+
const result = await service.setAsDefault('lang-2');
|
|
119
|
+
expect(mockRepo.update).toHaveBeenNthCalledWith(1, {
|
|
120
|
+
isDefault: true
|
|
121
|
+
}, {
|
|
122
|
+
isDefault: false
|
|
123
|
+
});
|
|
124
|
+
expect(mockRepo.update).toHaveBeenNthCalledWith(2, {
|
|
125
|
+
id: 'lang-2'
|
|
126
|
+
}, {
|
|
127
|
+
isDefault: true
|
|
128
|
+
});
|
|
129
|
+
expect(mockRepo.findOne).toHaveBeenCalledWith({
|
|
130
|
+
where: {
|
|
131
|
+
id: 'lang-2'
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
expect(result).toEqual(newDefault);
|
|
135
|
+
});
|
|
136
|
+
it('returns null when the id does not correspond to any language after the update', async ()=>{
|
|
137
|
+
mockRepo.findOne.mockResolvedValue(null);
|
|
138
|
+
const result = await service.setAsDefault('non-existent-id');
|
|
139
|
+
expect(result).toBeNull();
|
|
140
|
+
});
|
|
141
|
+
it('still unsets other defaults first even when the target id is unknown (no exception thrown)', async ()=>{
|
|
142
|
+
mockRepo.findOne.mockResolvedValue(null);
|
|
143
|
+
await service.setAsDefault('unknown-id');
|
|
144
|
+
expect(mockRepo.update).toHaveBeenCalledWith({
|
|
145
|
+
isDefault: true
|
|
146
|
+
}, {
|
|
147
|
+
isDefault: false
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
const _localizationconfigservice = require("./localization-config.service");
|
|
6
|
+
function buildOptions(overrides = {}) {
|
|
7
|
+
return {
|
|
8
|
+
bootstrapAppConfig: undefined,
|
|
9
|
+
config: undefined,
|
|
10
|
+
...overrides
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
describe('LocalizationConfigService', ()=>{
|
|
14
|
+
describe('isCompanyFeatureEnabled', ()=>{
|
|
15
|
+
it('always returns false — localization module has no company scoping', ()=>{
|
|
16
|
+
const service = new _localizationconfigservice.LocalizationConfigService(buildOptions());
|
|
17
|
+
expect(service.isCompanyFeatureEnabled()).toBe(false);
|
|
18
|
+
});
|
|
19
|
+
it('returns false even when bootstrapAppConfig enables the company feature elsewhere', ()=>{
|
|
20
|
+
const service = new _localizationconfigservice.LocalizationConfigService(buildOptions({
|
|
21
|
+
bootstrapAppConfig: {
|
|
22
|
+
databaseMode: 'single',
|
|
23
|
+
enableCompanyFeature: true
|
|
24
|
+
}
|
|
25
|
+
}));
|
|
26
|
+
expect(service.isCompanyFeatureEnabled()).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
describe('getDatabaseMode', ()=>{
|
|
30
|
+
it('returns the configured database mode', ()=>{
|
|
31
|
+
const service = new _localizationconfigservice.LocalizationConfigService(buildOptions({
|
|
32
|
+
bootstrapAppConfig: {
|
|
33
|
+
databaseMode: 'multi-tenant'
|
|
34
|
+
}
|
|
35
|
+
}));
|
|
36
|
+
expect(service.getDatabaseMode()).toBe('multi-tenant');
|
|
37
|
+
});
|
|
38
|
+
it('defaults to "single" when bootstrapAppConfig is missing', ()=>{
|
|
39
|
+
const service = new _localizationconfigservice.LocalizationConfigService(buildOptions({
|
|
40
|
+
bootstrapAppConfig: undefined
|
|
41
|
+
}));
|
|
42
|
+
expect(service.getDatabaseMode()).toBe('single');
|
|
43
|
+
});
|
|
44
|
+
it('defaults to "single" when databaseMode is not set on bootstrapAppConfig', ()=>{
|
|
45
|
+
const service = new _localizationconfigservice.LocalizationConfigService(buildOptions({
|
|
46
|
+
bootstrapAppConfig: {}
|
|
47
|
+
}));
|
|
48
|
+
expect(service.getDatabaseMode()).toBe('single');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
describe('isMultiTenant', ()=>{
|
|
52
|
+
it('returns true when database mode is multi-tenant', ()=>{
|
|
53
|
+
const service = new _localizationconfigservice.LocalizationConfigService(buildOptions({
|
|
54
|
+
bootstrapAppConfig: {
|
|
55
|
+
databaseMode: 'multi-tenant'
|
|
56
|
+
}
|
|
57
|
+
}));
|
|
58
|
+
expect(service.isMultiTenant()).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
it('returns false when database mode is single', ()=>{
|
|
61
|
+
const service = new _localizationconfigservice.LocalizationConfigService(buildOptions({
|
|
62
|
+
bootstrapAppConfig: {
|
|
63
|
+
databaseMode: 'single'
|
|
64
|
+
}
|
|
65
|
+
}));
|
|
66
|
+
expect(service.isMultiTenant()).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
describe('getOptions', ()=>{
|
|
70
|
+
it('returns the exact options object it was constructed with', ()=>{
|
|
71
|
+
const options = buildOptions({
|
|
72
|
+
config: {
|
|
73
|
+
defaultLanguageCode: 'en'
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
const service = new _localizationconfigservice.LocalizationConfigService(options);
|
|
77
|
+
expect(service.getOptions()).toBe(options);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
const _localizationdatasourceprovider = require("./localization-datasource.provider");
|
|
6
|
+
const _entities = require("../entities");
|
|
7
|
+
function buildConfigService(overrides = {}) {
|
|
8
|
+
const options = {
|
|
9
|
+
bootstrapAppConfig: {
|
|
10
|
+
databaseMode: 'single'
|
|
11
|
+
},
|
|
12
|
+
config: {
|
|
13
|
+
defaultDatabaseConfig: {
|
|
14
|
+
type: 'mysql',
|
|
15
|
+
host: 'localhost',
|
|
16
|
+
port: 3306,
|
|
17
|
+
username: 'root',
|
|
18
|
+
password: 'password',
|
|
19
|
+
database: 'flusys_test'
|
|
20
|
+
},
|
|
21
|
+
tenantDefaultDatabaseConfig: undefined,
|
|
22
|
+
tenants: []
|
|
23
|
+
},
|
|
24
|
+
...overrides
|
|
25
|
+
};
|
|
26
|
+
return {
|
|
27
|
+
getOptions: jest.fn().mockReturnValue(options)
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
describe('LocalizationDataSourceProvider', ()=>{
|
|
31
|
+
afterEach(()=>{
|
|
32
|
+
// Reset the isolated static caches this provider owns so tests don't leak state between runs.
|
|
33
|
+
_localizationdatasourceprovider.LocalizationDataSourceProvider.tenantConnections = new Map();
|
|
34
|
+
_localizationdatasourceprovider.LocalizationDataSourceProvider.singleDataSource = null;
|
|
35
|
+
_localizationdatasourceprovider.LocalizationDataSourceProvider.tenantsRegistry = new Map();
|
|
36
|
+
_localizationdatasourceprovider.LocalizationDataSourceProvider.initialized = false;
|
|
37
|
+
_localizationdatasourceprovider.LocalizationDataSourceProvider.connectionLocks = new Map();
|
|
38
|
+
_localizationdatasourceprovider.LocalizationDataSourceProvider.singleConnectionLock = null;
|
|
39
|
+
});
|
|
40
|
+
it('constructs and forwards bootstrap/database config to the base class', ()=>{
|
|
41
|
+
const configService = buildConfigService();
|
|
42
|
+
const provider = new _localizationdatasourceprovider.LocalizationDataSourceProvider(configService, undefined);
|
|
43
|
+
expect(configService.getOptions).toHaveBeenCalled();
|
|
44
|
+
expect(provider.getDatabaseMode()).toBe('single');
|
|
45
|
+
expect(provider.isMultiTenant()).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
it('reflects multi-tenant mode from bootstrapAppConfig', ()=>{
|
|
48
|
+
const configService = buildConfigService({
|
|
49
|
+
bootstrapAppConfig: {
|
|
50
|
+
databaseMode: 'multi-tenant'
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
const provider = new _localizationdatasourceprovider.LocalizationDataSourceProvider(configService, undefined);
|
|
54
|
+
expect(provider.isMultiTenant()).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
it('owns isolated static caches, separate from the base MultiTenantDataSourceService', ()=>{
|
|
57
|
+
// Each DataSource Provider must override BOTH static cache properties and the
|
|
58
|
+
// methods that read them — otherwise modules would share connections (see FLUSYS_NEST/CLAUDE.md).
|
|
59
|
+
expect(_localizationdatasourceprovider.LocalizationDataSourceProvider.tenantConnections).not.toBe(Object.getPrototypeOf(_localizationdatasourceprovider.LocalizationDataSourceProvider).tenantConnections);
|
|
60
|
+
});
|
|
61
|
+
it('createDataSourceFromConfig builds a DataSource using all localization entities', async ()=>{
|
|
62
|
+
const configService = buildConfigService();
|
|
63
|
+
const provider = new _localizationdatasourceprovider.LocalizationDataSourceProvider(configService, undefined);
|
|
64
|
+
const fakeDataSource = {
|
|
65
|
+
isInitialized: true
|
|
66
|
+
};
|
|
67
|
+
const superSpy = jest.spyOn(Object.getPrototypeOf(Object.getPrototypeOf(provider)), 'createDataSourceFromConfig').mockResolvedValue(fakeDataSource);
|
|
68
|
+
const config = {
|
|
69
|
+
type: 'mysql',
|
|
70
|
+
host: 'localhost',
|
|
71
|
+
port: 3306,
|
|
72
|
+
username: 'root',
|
|
73
|
+
password: 'password',
|
|
74
|
+
database: 'flusys_test'
|
|
75
|
+
};
|
|
76
|
+
const result = await provider.createDataSourceFromConfig(config);
|
|
77
|
+
expect(superSpy).toHaveBeenCalledWith(config, [
|
|
78
|
+
_entities.Language,
|
|
79
|
+
_entities.TranslationKey,
|
|
80
|
+
_entities.Translation
|
|
81
|
+
]);
|
|
82
|
+
expect(result).toBe(fakeDataSource);
|
|
83
|
+
superSpy.mockRestore();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
const _testing = require("@nestjs/testing");
|
|
6
|
+
const _modules = require("@flusys/nestjs-shared/modules");
|
|
7
|
+
const _common = require("@nestjs/common");
|
|
8
|
+
const _repositorymock = require("@test-utils/mocks/repository.mock");
|
|
9
|
+
const _loggedusermock = require("@test-utils/mocks/logged-user.mock");
|
|
10
|
+
const _translationkeyservice = require("./translation-key.service");
|
|
11
|
+
const _localizationdatasourceprovider = require("./localization-datasource.provider");
|
|
12
|
+
function buildTranslationKey(overrides = {}) {
|
|
13
|
+
return {
|
|
14
|
+
id: 'tkey-uuid-1',
|
|
15
|
+
module: 'auth',
|
|
16
|
+
key: 'auth.login.success',
|
|
17
|
+
defaultMessage: 'Login successful',
|
|
18
|
+
description: null,
|
|
19
|
+
variables: null,
|
|
20
|
+
isActive: true,
|
|
21
|
+
isReadonly: false,
|
|
22
|
+
serial: 1,
|
|
23
|
+
createdAt: new Date(),
|
|
24
|
+
updatedAt: new Date(),
|
|
25
|
+
deletedAt: null,
|
|
26
|
+
createdById: null,
|
|
27
|
+
updatedById: null,
|
|
28
|
+
deletedById: null,
|
|
29
|
+
...overrides
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
describe('TranslationKeyService', ()=>{
|
|
33
|
+
let service;
|
|
34
|
+
let mockRepo;
|
|
35
|
+
let qb;
|
|
36
|
+
let queryRunner;
|
|
37
|
+
beforeEach(async ()=>{
|
|
38
|
+
mockRepo = (0, _repositorymock.createMockRepository)();
|
|
39
|
+
qb = mockRepo.createQueryBuilder();
|
|
40
|
+
const mockDataSourceProvider = (0, _repositorymock.createMockDataSourceProvider)(mockRepo);
|
|
41
|
+
// executeInTransaction()/delete() always grab a QueryRunner off repository.manager.connection
|
|
42
|
+
// before running the before*Operation hooks, so every test (success or rejection) needs one.
|
|
43
|
+
queryRunner = {
|
|
44
|
+
connect: jest.fn(),
|
|
45
|
+
startTransaction: jest.fn(),
|
|
46
|
+
commitTransaction: jest.fn(),
|
|
47
|
+
rollbackTransaction: jest.fn(),
|
|
48
|
+
release: jest.fn(),
|
|
49
|
+
manager: {
|
|
50
|
+
save: jest.fn(),
|
|
51
|
+
softDelete: jest.fn(),
|
|
52
|
+
update: jest.fn(),
|
|
53
|
+
delete: jest.fn(),
|
|
54
|
+
restore: jest.fn()
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
mockRepo.manager.connection.createQueryRunner.mockReturnValue(queryRunner);
|
|
58
|
+
const mockUtilsService = {
|
|
59
|
+
clearCache: jest.fn(),
|
|
60
|
+
getCacheKey: jest.fn().mockReturnValue('cache-key'),
|
|
61
|
+
trackCacheKey: jest.fn()
|
|
62
|
+
};
|
|
63
|
+
const module = await _testing.Test.createTestingModule({
|
|
64
|
+
providers: [
|
|
65
|
+
_translationkeyservice.TranslationKeyService,
|
|
66
|
+
{
|
|
67
|
+
provide: 'CACHE_INSTANCE',
|
|
68
|
+
useValue: {
|
|
69
|
+
get: jest.fn(),
|
|
70
|
+
set: jest.fn()
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
provide: _modules.UtilsService,
|
|
75
|
+
useValue: mockUtilsService
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
provide: _localizationdatasourceprovider.LocalizationDataSourceProvider,
|
|
79
|
+
useValue: mockDataSourceProvider
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
}).compile();
|
|
83
|
+
service = await module.resolve(_translationkeyservice.TranslationKeyService);
|
|
84
|
+
});
|
|
85
|
+
describe('findByModule', ()=>{
|
|
86
|
+
it('returns active, non-deleted keys for the module ordered by serial then key', async ()=>{
|
|
87
|
+
const keys = [
|
|
88
|
+
buildTranslationKey({
|
|
89
|
+
key: 'auth.login.success',
|
|
90
|
+
serial: 1
|
|
91
|
+
})
|
|
92
|
+
];
|
|
93
|
+
mockRepo.find.mockResolvedValue(keys);
|
|
94
|
+
const result = await service.findByModule('auth');
|
|
95
|
+
expect(mockRepo.find).toHaveBeenCalledWith({
|
|
96
|
+
where: {
|
|
97
|
+
module: 'auth',
|
|
98
|
+
isActive: true,
|
|
99
|
+
deletedAt: expect.anything()
|
|
100
|
+
},
|
|
101
|
+
order: {
|
|
102
|
+
serial: 'ASC',
|
|
103
|
+
key: 'ASC'
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
expect(result).toEqual(keys);
|
|
107
|
+
});
|
|
108
|
+
it('returns an empty array for an unknown module', async ()=>{
|
|
109
|
+
mockRepo.find.mockResolvedValue([]);
|
|
110
|
+
const result = await service.findByModule('does-not-exist');
|
|
111
|
+
expect(result).toEqual([]);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
describe('getModules', ()=>{
|
|
115
|
+
it('returns distinct module names', async ()=>{
|
|
116
|
+
// `getRawMany` isn't part of the shared MockQueryBuilder — attach it for this raw-query path.
|
|
117
|
+
const getRawMany = jest.fn().mockResolvedValue([
|
|
118
|
+
{
|
|
119
|
+
module: 'auth'
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
module: 'shared'
|
|
123
|
+
}
|
|
124
|
+
]);
|
|
125
|
+
qb.getRawMany = getRawMany;
|
|
126
|
+
const result = await service.getModules();
|
|
127
|
+
expect(result).toEqual([
|
|
128
|
+
'auth',
|
|
129
|
+
'shared'
|
|
130
|
+
]);
|
|
131
|
+
});
|
|
132
|
+
it('returns an empty array when there are no translation keys yet', async ()=>{
|
|
133
|
+
const getRawMany = jest.fn().mockResolvedValue([]);
|
|
134
|
+
qb.getRawMany = getRawMany;
|
|
135
|
+
const result = await service.getModules();
|
|
136
|
+
expect(result).toEqual([]);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
describe('beforeInsertOperation (via insert) — duplicate key validation', ()=>{
|
|
140
|
+
it('allows insert when no key with the same module+key exists', async ()=>{
|
|
141
|
+
qb.getOne.mockResolvedValue(null);
|
|
142
|
+
const created = buildTranslationKey();
|
|
143
|
+
mockRepo.create.mockReturnValue({});
|
|
144
|
+
queryRunner.manager.save.mockResolvedValue(created);
|
|
145
|
+
await expect(service.insert({
|
|
146
|
+
module: 'auth',
|
|
147
|
+
key: 'auth.login.success',
|
|
148
|
+
defaultMessage: 'Login successful'
|
|
149
|
+
}, (0, _loggedusermock.buildMockUser)())).resolves.toBeDefined();
|
|
150
|
+
});
|
|
151
|
+
it('rejects with BadRequestException + messageKey when the module+key combination already exists', async ()=>{
|
|
152
|
+
qb.getOne.mockResolvedValue(buildTranslationKey());
|
|
153
|
+
await expect(service.insert({
|
|
154
|
+
module: 'auth',
|
|
155
|
+
key: 'auth.login.success',
|
|
156
|
+
defaultMessage: 'Login successful'
|
|
157
|
+
}, (0, _loggedusermock.buildMockUser)())).rejects.toMatchObject({
|
|
158
|
+
response: expect.objectContaining({
|
|
159
|
+
messageKey: 'translation.key.duplicate.key.in.module'
|
|
160
|
+
})
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
it('rejects with BadRequestException for duplicate module+key in insertMany', async ()=>{
|
|
164
|
+
qb.getOne.mockResolvedValueOnce(null).mockResolvedValueOnce(buildTranslationKey({
|
|
165
|
+
key: 'auth.logout.success'
|
|
166
|
+
}));
|
|
167
|
+
await expect(service.insertMany([
|
|
168
|
+
{
|
|
169
|
+
module: 'auth',
|
|
170
|
+
key: 'auth.login.success',
|
|
171
|
+
defaultMessage: 'A'
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
module: 'auth',
|
|
175
|
+
key: 'auth.logout.success',
|
|
176
|
+
defaultMessage: 'B'
|
|
177
|
+
}
|
|
178
|
+
], (0, _loggedusermock.buildMockUser)())).rejects.toBeInstanceOf(_common.BadRequestException);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
describe('beforeUpdateOperation (via update)', ()=>{
|
|
182
|
+
it('rejects with ForbiddenException when modifying key/module of a readonly translation key', async ()=>{
|
|
183
|
+
const existing = buildTranslationKey({
|
|
184
|
+
id: 'tkey-1',
|
|
185
|
+
isReadonly: true,
|
|
186
|
+
key: 'system.ready'
|
|
187
|
+
});
|
|
188
|
+
mockRepo.find.mockResolvedValue([
|
|
189
|
+
existing
|
|
190
|
+
]);
|
|
191
|
+
await expect(service.update({
|
|
192
|
+
id: 'tkey-1',
|
|
193
|
+
key: 'system.changed'
|
|
194
|
+
}, (0, _loggedusermock.buildMockUser)())).rejects.toBeInstanceOf(_common.ForbiddenException);
|
|
195
|
+
await expect(service.update({
|
|
196
|
+
id: 'tkey-1',
|
|
197
|
+
key: 'system.changed'
|
|
198
|
+
}, (0, _loggedusermock.buildMockUser)())).rejects.toMatchObject({
|
|
199
|
+
response: expect.objectContaining({
|
|
200
|
+
messageKey: 'translation.key.readonly.update.forbidden'
|
|
201
|
+
})
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
it('allows updating non key/module fields of a readonly translation key', async ()=>{
|
|
205
|
+
const existing = buildTranslationKey({
|
|
206
|
+
id: 'tkey-1',
|
|
207
|
+
isReadonly: true,
|
|
208
|
+
key: 'system.ready'
|
|
209
|
+
});
|
|
210
|
+
mockRepo.find.mockResolvedValue([
|
|
211
|
+
existing
|
|
212
|
+
]);
|
|
213
|
+
mockRepo.findOne.mockResolvedValue(existing);
|
|
214
|
+
queryRunner.manager.save.mockResolvedValue(existing);
|
|
215
|
+
await expect(service.update({
|
|
216
|
+
id: 'tkey-1',
|
|
217
|
+
defaultMessage: 'Updated readonly message'
|
|
218
|
+
}, (0, _loggedusermock.buildMockUser)())).resolves.toBeDefined();
|
|
219
|
+
});
|
|
220
|
+
it('rejects with BadRequestException when the new key/module already exists on another row', async ()=>{
|
|
221
|
+
const existing = buildTranslationKey({
|
|
222
|
+
id: 'tkey-1',
|
|
223
|
+
isReadonly: false,
|
|
224
|
+
key: 'auth.old',
|
|
225
|
+
module: 'auth'
|
|
226
|
+
});
|
|
227
|
+
mockRepo.find.mockResolvedValue([
|
|
228
|
+
existing
|
|
229
|
+
]);
|
|
230
|
+
qb.getOne.mockResolvedValue(buildTranslationKey({
|
|
231
|
+
id: 'tkey-other',
|
|
232
|
+
key: 'auth.new'
|
|
233
|
+
}));
|
|
234
|
+
await expect(service.update({
|
|
235
|
+
id: 'tkey-1',
|
|
236
|
+
key: 'auth.new'
|
|
237
|
+
}, (0, _loggedusermock.buildMockUser)())).rejects.toMatchObject({
|
|
238
|
+
response: expect.objectContaining({
|
|
239
|
+
messageKey: 'translation.key.duplicate.key.in.module'
|
|
240
|
+
})
|
|
241
|
+
});
|
|
242
|
+
// excludeId must be used so the row being updated doesn't collide with itself
|
|
243
|
+
expect(qb.andWhere).toHaveBeenCalledWith('tk.id != :excludeId', {
|
|
244
|
+
excludeId: 'tkey-1'
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
describe('beforeDeleteOperation (via delete)', ()=>{
|
|
249
|
+
it('rejects with ForbiddenException + messageKey when any target id is readonly', async ()=>{
|
|
250
|
+
const readonlyKey = buildTranslationKey({
|
|
251
|
+
id: 'tkey-1',
|
|
252
|
+
isReadonly: true,
|
|
253
|
+
key: 'system.core'
|
|
254
|
+
});
|
|
255
|
+
mockRepo.find.mockResolvedValue([
|
|
256
|
+
readonlyKey
|
|
257
|
+
]);
|
|
258
|
+
const dto = {
|
|
259
|
+
id: [
|
|
260
|
+
'tkey-1'
|
|
261
|
+
],
|
|
262
|
+
type: 'delete'
|
|
263
|
+
};
|
|
264
|
+
await expect(service.delete(dto, (0, _loggedusermock.buildMockUser)())).rejects.toMatchObject({
|
|
265
|
+
response: expect.objectContaining({
|
|
266
|
+
messageKey: 'translation.key.readonly.delete.forbidden'
|
|
267
|
+
})
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
it('allows delete when none of the targeted ids are readonly', async ()=>{
|
|
271
|
+
mockRepo.find.mockResolvedValue([]);
|
|
272
|
+
const dto = {
|
|
273
|
+
id: [
|
|
274
|
+
'tkey-1'
|
|
275
|
+
],
|
|
276
|
+
type: 'delete'
|
|
277
|
+
};
|
|
278
|
+
await expect(service.delete(dto, (0, _loggedusermock.buildMockUser)())).resolves.toBeNull();
|
|
279
|
+
expect(queryRunner.manager.softDelete).toHaveBeenCalled();
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
});
|