@tomei/media 0.8.11-dev.1 → 0.10.1-test.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.
Files changed (41) hide show
  1. package/.gitlab-ci.yml +231 -13
  2. package/.husky/commit-msg +1 -1
  3. package/.husky/pre-commit +0 -0
  4. package/.vscode/settings.json +3 -0
  5. package/dist/__tests__/common.service.spec.d.ts +1 -0
  6. package/dist/__tests__/common.service.spec.js +160 -0
  7. package/dist/__tests__/common.service.spec.js.map +1 -0
  8. package/dist/__tests__/medias.repository.spec.d.ts +1 -0
  9. package/dist/__tests__/medias.repository.spec.js +140 -0
  10. package/dist/__tests__/medias.repository.spec.js.map +1 -0
  11. package/dist/__tests__/medias.spec.d.ts +1 -0
  12. package/dist/__tests__/medias.spec.js +347 -0
  13. package/dist/__tests__/medias.spec.js.map +1 -0
  14. package/dist/__tests__/pipes.spec.d.ts +1 -0
  15. package/dist/__tests__/pipes.spec.js +130 -0
  16. package/dist/__tests__/pipes.spec.js.map +1 -0
  17. package/dist/base/base.medias.js +2 -2
  18. package/dist/base/base.medias.js.map +1 -1
  19. package/dist/common/common.service.js +2 -2
  20. package/dist/common/common.service.js.map +1 -1
  21. package/dist/medias.d.ts +2 -2
  22. package/dist/medias.js +16 -10
  23. package/dist/medias.js.map +1 -1
  24. package/dist/medias.repository.js.map +1 -1
  25. package/dist/pipe/append-id.pipe.js +2 -2
  26. package/dist/pipe/append-id.pipe.js.map +1 -1
  27. package/dist/pipe/validate-id.pipe.js +2 -2
  28. package/dist/pipe/validate-id.pipe.js.map +1 -1
  29. package/dist/tsconfig.tsbuildinfo +1 -1
  30. package/jest.config.js +26 -0
  31. package/package.json +38 -34
  32. package/src/__tests__/common.service.spec.ts +203 -0
  33. package/src/__tests__/medias.repository.spec.ts +158 -0
  34. package/src/__tests__/medias.spec.ts +468 -0
  35. package/src/__tests__/pipes.spec.ts +154 -0
  36. package/src/base/base.medias.ts +2 -2
  37. package/src/common/common.service.ts +2 -2
  38. package/src/medias.repository.ts +9 -3
  39. package/src/medias.ts +20 -15
  40. package/src/pipe/append-id.pipe.ts +2 -2
  41. package/src/pipe/validate-id.pipe.ts +2 -2
package/jest.config.js ADDED
@@ -0,0 +1,26 @@
1
+ module.exports = {
2
+ preset: 'ts-jest',
3
+ testEnvironment: 'node',
4
+ roots: ['<rootDir>/src'],
5
+ testMatch: ['**/__tests__/**/*.spec.ts'],
6
+ collectCoverage: true,
7
+ coverageDirectory: 'coverage',
8
+ collectCoverageFrom: [
9
+ 'src/**/*.ts',
10
+ '!src/index.ts',
11
+ '!src/database.ts',
12
+ '!src/**/*.entity.ts',
13
+ '!src/**/*.interface.ts',
14
+ '!src/**/*.dto.ts',
15
+ '!src/**/*.enum.ts',
16
+ '!src/**/*.response.ts',
17
+ '!src/**/*.module.ts',
18
+ '!src/base/base.repository.abstract.ts',
19
+ '!src/helpers/**',
20
+ ],
21
+ coverageThreshold: {
22
+ global: {
23
+ lines: 80,
24
+ },
25
+ },
26
+ };
package/package.json CHANGED
@@ -1,14 +1,16 @@
1
1
  {
2
2
  "name": "@tomei/media",
3
- "version": "0.8.11-dev.1",
3
+ "version": "0.10.1-test.1",
4
4
  "description": "NestJS package for media module",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
7
7
  "start:dev": "tsc -w",
8
8
  "build": "tsc",
9
- "prepare": "husky install",
9
+ "prepare": "husky install || true",
10
10
  "format": "prettier --write \"src/**/*.ts\"",
11
- "lint": "npx eslint . --fix"
11
+ "lint": "npx eslint . --fix",
12
+ "test": "jest",
13
+ "test:coverage": "jest --coverage"
12
14
  },
13
15
  "keywords": [
14
16
  "nestjs",
@@ -22,47 +24,49 @@
22
24
  "url": "https://gitlab.com/tomei-package/media.git"
23
25
  },
24
26
  "peerDependencies": {
25
- "@azure/storage-blob": "^12.26.0",
26
- "@nestjs/common": "^10.4.15",
27
+ "@azure/storage-blob": "^12.28.0",
28
+ "@nestjs/common": "^10.4.20",
27
29
  "@nestjs/config": "^3.3.0",
28
- "@nestjs/core": "^10.4.15",
29
- "@nestjs/platform-express": "^10.4.15",
30
+ "@nestjs/core": "^10.4.20",
31
+ "@nestjs/platform-express": "^10.4.20",
30
32
  "@nestjs/sequelize": "^10.0.1",
31
- "@nestjs/swagger": "^8.1.0",
32
- "@tomei/activity-history": "^0.4.1",
33
- "@tomei/config": "^0.3.21",
34
- "@tomei/general": "^0.21.7",
35
- "@tomei/mailer": "^0.5.21",
36
- "@tomei/sso": "^0.60.4",
37
- "axios": "^1.7.9",
33
+ "@nestjs/swagger": "^8.1.1",
34
+ "@tomei/activity-history": "^0.4.4",
35
+ "@tomei/config": "^0.3.22",
36
+ "@tomei/general": "^0.21.10",
37
+ "@tomei/mailer": "^0.6.2",
38
+ "@tomei/sso": "^0.65.0",
39
+ "axios": "^1.12.2",
38
40
  "class-transformer": "^0.5.1",
39
- "class-validator": "^0.14.1",
40
- "cuid": "^3.0.0",
41
- "multer": "^1.4.5-lts.1",
42
- "sequelize": "^6.37.5",
41
+ "class-validator": "^0.14.2",
42
+ "multer": "^1.4.5-lts.2",
43
+ "sequelize": "^6.37.7",
43
44
  "sequelize-typescript": "^2.1.6"
44
45
  },
45
46
  "devDependencies": {
46
- "@commitlint/config-conventional": "^19.6.0",
47
- "@eslint/js": "^9.17.0",
48
- "@types/express": "^5.0.0",
49
- "@types/multer": "^1.4.12",
50
- "@types/node": "^22.10.5",
47
+ "@commitlint/config-conventional": "^19.8.1",
48
+ "@eslint/js": "^9.36.0",
49
+ "@types/express": "^5.0.3",
50
+ "@types/jest": "^30.0.0",
51
+ "@types/multer": "^1.4.13",
52
+ "@types/node": "^22.18.8",
51
53
  "@types/sequelize": "^4.28.20",
52
- "@typescript-eslint/eslint-plugin": "^8.19.0",
53
- "eslint": "^9.17.0",
54
- "eslint-config-prettier": "^9.1.0",
55
- "eslint-plugin-import": "^2.31.0",
56
- "eslint-plugin-prettier": "^5.2.1",
57
- "globals": "^15.14.0",
54
+ "@typescript-eslint/eslint-plugin": "^8.45.0",
55
+ "eslint": "^9.36.0",
56
+ "eslint-config-prettier": "^9.1.2",
57
+ "eslint-plugin-import": "^2.32.0",
58
+ "eslint-plugin-prettier": "^5.5.4",
59
+ "globals": "^15.15.0",
58
60
  "husky": "^9.1.7",
59
- "lint-staged": "^15.3.0",
60
- "prettier": "^3.4.2",
61
+ "jest": "^30.4.2",
62
+ "lint-staged": "^15.5.2",
63
+ "prettier": "^3.6.2",
64
+ "ts-jest": "^29.4.9",
61
65
  "ts-node": "^10.9.2",
62
- "tsc-watch": "^6.2.1",
66
+ "tsc-watch": "^6.3.1",
63
67
  "tsconfig-paths": "^4.2.0",
64
- "typescript": "^5.7.2",
65
- "typescript-eslint": "^8.19.0"
68
+ "typescript": "^5.9.3",
69
+ "typescript-eslint": "^8.45.0"
66
70
  },
67
71
  "publishConfig": {
68
72
  "access": "public"
@@ -0,0 +1,203 @@
1
+ jest.mock('@nestjs/config', () => ({
2
+ ConfigService: jest.fn().mockImplementation(() => ({
3
+ get: jest.fn().mockReturnValue('http://localhost:3000'),
4
+ })),
5
+ }));
6
+
7
+ jest.mock('@nestjs/common', () => ({
8
+ Injectable: () => (target: any) => target,
9
+ HttpException: class HttpException extends Error {
10
+ constructor(
11
+ public response: any,
12
+ public status: number,
13
+ ) {
14
+ super(typeof response === 'string' ? response : JSON.stringify(response));
15
+ }
16
+ },
17
+ InternalServerErrorException: class InternalServerErrorException extends Error {
18
+ constructor(message: string) {
19
+ super(message);
20
+ }
21
+ },
22
+ }));
23
+
24
+ jest.mock('axios');
25
+ jest.mock('lodash', () => ({
26
+ omit: jest.fn((obj, keys) => {
27
+ const result = { ...obj };
28
+ keys.forEach((k: string) => delete result[k]);
29
+ return result;
30
+ }),
31
+ omitBy: jest.fn((obj, fn) => obj),
32
+ keys: jest.fn((obj) => Object.keys(obj)),
33
+ pick: jest.fn((obj, keys) => {
34
+ const result: any = {};
35
+ keys.forEach((k: string) => {
36
+ if (k in obj) result[k] = obj[k];
37
+ });
38
+ return result;
39
+ }),
40
+ }));
41
+
42
+ jest.mock('@paralleldrive/cuid2', () => ({
43
+ createId: jest.fn().mockReturnValue('new-cuid'),
44
+ }));
45
+
46
+ import axios from 'axios';
47
+ import { CommonService } from '../common/common.service';
48
+
49
+ const mockAxios = axios as jest.Mocked<typeof axios>;
50
+
51
+ const mockConfigService = {
52
+ get: jest.fn().mockReturnValue('http://localhost:3000'),
53
+ } as any;
54
+
55
+ describe('CommonService', () => {
56
+ let service: CommonService;
57
+
58
+ beforeEach(() => {
59
+ jest.clearAllMocks();
60
+ service = new CommonService(mockConfigService);
61
+ });
62
+
63
+ describe('handleAxiosError', () => {
64
+ it('throws HttpException when error has a response', () => {
65
+ const err = { response: { data: 'Bad Request', status: 400 } };
66
+ expect(() => service.handleAxiosError(err)).toThrow();
67
+ });
68
+
69
+ it('throws HttpException when error has a request but no response', () => {
70
+ const err = { request: 'timeout' };
71
+ expect(() => service.handleAxiosError(err)).toThrow();
72
+ });
73
+
74
+ it('throws InternalServerErrorException for generic errors', () => {
75
+ const err = { message: 'Something went wrong' };
76
+ expect(() => service.handleAxiosError(err)).toThrow();
77
+ });
78
+ });
79
+
80
+ describe('addActivityHistory', () => {
81
+ it('posts activity and returns EntityId', async () => {
82
+ mockAxios.post = jest
83
+ .fn()
84
+ .mockResolvedValue({ data: { EntityId: 'entity-123' } });
85
+
86
+ const record = {
87
+ Action: 'Insert',
88
+ EntityValueBefore: {},
89
+ EntityValueAfter: {
90
+ Title: 'Test',
91
+ CreatedById: 'u1',
92
+ CreatedAt: new Date(),
93
+ },
94
+ PerformedById: 'user-001',
95
+ EntityId: 'entity-001',
96
+ };
97
+
98
+ const result = await service.addActivityHistory(
99
+ record,
100
+ 'Media',
101
+ 'create',
102
+ );
103
+ expect(result).toBe('entity-123');
104
+ expect(mockAxios.post).toHaveBeenCalledWith(
105
+ 'http://localhost:3000/activity-histories',
106
+ expect.objectContaining({ EntityId: 'entity-001' }),
107
+ );
108
+ });
109
+
110
+ it('uses update activity label for update method', async () => {
111
+ mockAxios.post = jest
112
+ .fn()
113
+ .mockResolvedValue({ data: { EntityId: 'e2' } });
114
+
115
+ const record = {
116
+ EntityValueBefore: { Title: 'Old' },
117
+ EntityValueAfter: { Title: 'New' },
118
+ PerformedById: 'user-001',
119
+ EntityId: 'entity-002',
120
+ };
121
+
122
+ await service.addActivityHistory(record, 'Media', 'update');
123
+ expect(mockAxios.post).toHaveBeenCalledWith(
124
+ expect.any(String),
125
+ expect.objectContaining({ Action: 'Update' }),
126
+ );
127
+ });
128
+
129
+ it('calls handleAxiosError when axios throws', async () => {
130
+ const axiosError = { response: { data: 'Error', status: 500 } };
131
+ mockAxios.post = jest.fn().mockRejectedValue(axiosError);
132
+
133
+ const record = {
134
+ EntityValueBefore: {},
135
+ EntityValueAfter: {},
136
+ PerformedById: 'u1',
137
+ EntityId: 'e1',
138
+ };
139
+
140
+ await expect(
141
+ service.addActivityHistory(record, 'Media', 'create'),
142
+ ).rejects.toThrow();
143
+ });
144
+ });
145
+
146
+ describe('getList', () => {
147
+ it('returns rows from the response', async () => {
148
+ mockAxios.get = jest
149
+ .fn()
150
+ .mockResolvedValue({ data: { rows: ['item1', 'item2'] } });
151
+
152
+ const result = await service.getList('MediaTypes');
153
+ expect(result).toEqual(['item1', 'item2']);
154
+ expect(mockAxios.get).toHaveBeenCalledWith(
155
+ 'http://localhost:3000/lists/items',
156
+ expect.objectContaining({ params: { ListName: 'MediaTypes' } }),
157
+ );
158
+ });
159
+
160
+ it('calls handleAxiosError when axios throws', async () => {
161
+ mockAxios.get = jest
162
+ .fn()
163
+ .mockRejectedValue({ response: { data: 'err', status: 500 } });
164
+ await expect(service.getList('Any')).rejects.toThrow();
165
+ });
166
+ });
167
+
168
+ describe('addFieldTranslation', () => {
169
+ it('posts field translation', async () => {
170
+ mockAxios.post = jest.fn().mockResolvedValue({});
171
+ const payload = { field: 'Title', value: 'Tajuk' } as any;
172
+
173
+ await service.addFieldTranslation(payload);
174
+ expect(mockAxios.post).toHaveBeenCalledWith(
175
+ 'http://localhost:3000/field-translations',
176
+ payload,
177
+ );
178
+ });
179
+
180
+ it('calls handleAxiosError when axios throws', async () => {
181
+ mockAxios.post = jest.fn().mockRejectedValue({ message: 'fail' });
182
+ await expect(service.addFieldTranslation({} as any)).rejects.toThrow();
183
+ });
184
+ });
185
+
186
+ describe('getMedia', () => {
187
+ it('returns rows from the response', async () => {
188
+ mockAxios.get = jest
189
+ .fn()
190
+ .mockResolvedValue({ data: { rows: [{ MediaId: 'm1' }] } });
191
+ const result = await service.getMedia({
192
+ ObjectId: 'obj1',
193
+ ObjectType: 'Rental',
194
+ } as any);
195
+ expect(result).toEqual([{ MediaId: 'm1' }]);
196
+ });
197
+
198
+ it('calls handleAxiosError when axios throws', async () => {
199
+ mockAxios.get = jest.fn().mockRejectedValue({ message: 'fail' });
200
+ await expect(service.getMedia({} as any)).rejects.toThrow();
201
+ });
202
+ });
203
+ });
@@ -0,0 +1,158 @@
1
+ jest.mock('@nestjs/swagger', () => ({
2
+ ApiProperty: () => () => {},
3
+ ApiPropertyOptional: () => () => {},
4
+ }));
5
+
6
+ jest.mock('sequelize-typescript', () => ({
7
+ Table: () => () => {},
8
+ Column: () => () => {},
9
+ Model: class Model {
10
+ static findByPk = jest.fn();
11
+ static findOne = jest.fn();
12
+ static findAll = jest.fn();
13
+ static findAndCountAll = jest.fn();
14
+ static create = jest.fn();
15
+ get = jest.fn();
16
+ update = jest.fn();
17
+ destroy = jest.fn();
18
+ },
19
+ DataType: {
20
+ STRING: 'STRING',
21
+ TEXT: 'TEXT',
22
+ DATE: 'DATE',
23
+ BOOLEAN: 'BOOLEAN',
24
+ ENUM: () => 'ENUM',
25
+ },
26
+ PrimaryKey: () => () => {},
27
+ ForeignKey: () => () => {},
28
+ BelongsTo: () => () => {},
29
+ HasMany: () => () => {},
30
+ CreatedAt: () => () => {},
31
+ UpdatedAt: () => () => {},
32
+ Default: () => () => {},
33
+ AllowNull: () => () => {},
34
+ }));
35
+
36
+ jest.mock('sequelize', () => ({
37
+ Op: { substring: Symbol('Op.substring') },
38
+ DataTypes: {},
39
+ }));
40
+
41
+ jest.mock('@tomei/general', () => ({
42
+ RepositoryBase: class RepositoryBase {
43
+ constructor(_model: any) {}
44
+ },
45
+ IRepositoryBase: {},
46
+ }));
47
+
48
+ import { MediasRepository } from '../medias.repository';
49
+ import { MediasModel } from '../entities/medias.entity';
50
+
51
+ const mockModel = {
52
+ MediaId: 'media-001',
53
+ get: jest.fn().mockReturnValue({}),
54
+ update: jest.fn(),
55
+ destroy: jest.fn(),
56
+ };
57
+
58
+ describe('MediasRepository', () => {
59
+ let repo: MediasRepository;
60
+
61
+ beforeEach(() => {
62
+ jest.clearAllMocks();
63
+ repo = new MediasRepository();
64
+ });
65
+
66
+ describe('findByPk', () => {
67
+ it('returns a MediasModel when found', async () => {
68
+ (MediasModel.findOne as jest.Mock).mockResolvedValue(mockModel);
69
+ const result = await repo.findByPk('media-001', undefined);
70
+ expect(result).toBe(mockModel);
71
+ expect(MediasModel.findOne).toHaveBeenCalledWith({
72
+ where: { MediaId: 'media-001' },
73
+ transaction: undefined,
74
+ });
75
+ });
76
+
77
+ it('passes the transaction option', async () => {
78
+ const tx = { id: 'tx-1' };
79
+ (MediasModel.findOne as jest.Mock).mockResolvedValue(mockModel);
80
+ await repo.findByPk('media-001', tx);
81
+ expect(MediasModel.findOne).toHaveBeenCalledWith({
82
+ where: { MediaId: 'media-001' },
83
+ transaction: tx,
84
+ });
85
+ });
86
+
87
+ it('returns null when record is not found', async () => {
88
+ (MediasModel.findOne as jest.Mock).mockResolvedValue(null);
89
+ const result = await repo.findByPk('missing-id', undefined);
90
+ expect(result).toBeNull();
91
+ });
92
+
93
+ it('throws when the database call fails', async () => {
94
+ (MediasModel.findOne as jest.Mock).mockRejectedValue(
95
+ new Error('DB error'),
96
+ );
97
+ await expect(repo.findByPk('media-001', undefined)).rejects.toThrow(
98
+ 'DB error',
99
+ );
100
+ });
101
+ });
102
+
103
+ describe('delete', () => {
104
+ it('destroys the record by MediaId', async () => {
105
+ (MediasModel as any).destroy = jest.fn().mockResolvedValue(1);
106
+ await repo.delete('media-001');
107
+ expect((MediasModel as any).destroy).toHaveBeenCalledWith(
108
+ expect.objectContaining({
109
+ where: expect.objectContaining({ MediaId: 'media-001' }),
110
+ }),
111
+ );
112
+ });
113
+
114
+ it('passes the transaction option', async () => {
115
+ const tx = { id: 'tx-2' };
116
+ (MediasModel as any).destroy = jest.fn().mockResolvedValue(1);
117
+ await repo.delete('media-001', tx);
118
+ expect((MediasModel as any).destroy).toHaveBeenCalledWith(
119
+ expect.objectContaining({ transaction: tx }),
120
+ );
121
+ });
122
+
123
+ it('throws when the database call fails', async () => {
124
+ (MediasModel as any).destroy = jest
125
+ .fn()
126
+ .mockRejectedValue(new Error('Delete failed'));
127
+ await expect(repo.delete('media-001')).rejects.toThrow('Delete failed');
128
+ });
129
+ });
130
+
131
+ describe('findAndCountAll', () => {
132
+ it('returns count and rows with options', async () => {
133
+ const mockResult = { count: 2, rows: [mockModel, mockModel] };
134
+ (MediasModel.findAndCountAll as jest.Mock).mockResolvedValue(mockResult);
135
+ const result = await repo.findAndCountAll({ limit: 10, offset: 0 });
136
+ expect(result).toEqual(mockResult);
137
+ expect(MediasModel.findAndCountAll).toHaveBeenCalledWith({
138
+ limit: 10,
139
+ offset: 0,
140
+ });
141
+ });
142
+
143
+ it('returns all records when no options are given', async () => {
144
+ const mockResult = { count: 1, rows: [mockModel] };
145
+ (MediasModel.findAndCountAll as jest.Mock).mockResolvedValue(mockResult);
146
+ const result = await repo.findAndCountAll(undefined);
147
+ expect(result).toEqual(mockResult);
148
+ expect(MediasModel.findAndCountAll).toHaveBeenCalledWith();
149
+ });
150
+
151
+ it('throws when the database call fails', async () => {
152
+ (MediasModel.findAndCountAll as jest.Mock).mockRejectedValue(
153
+ new Error('Query failed'),
154
+ );
155
+ await expect(repo.findAndCountAll({ limit: 5 })).rejects.toThrow();
156
+ });
157
+ });
158
+ });