Skip to content

Commit cadceaa

Browse files
Merge pull request #1159 from codesailor4/feat/Versioning
Feat/versioning
2 parents 1e4f57f + a74a11f commit cadceaa

4 files changed

Lines changed: 512 additions & 0 deletions
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import { BackwardCompatibilityService } from './backward-compatibility.service';
2+
import { ApiVersionEnum } from './api-version.constants';
3+
4+
describe('BackwardCompatibilityService', () => {
5+
let service: BackwardCompatibilityService;
6+
7+
beforeEach(() => {
8+
service = new BackwardCompatibilityService();
9+
});
10+
11+
it('should be defined', () => {
12+
expect(service).toBeDefined();
13+
});
14+
15+
describe('transform', () => {
16+
it('should return the same data if fromVersion equals toVersion', () => {
17+
const data = { id: 1, name: 'Test', email: 'test@example.com' };
18+
19+
const result = service.transform(data, ApiVersionEnum.V2, ApiVersionEnum.V2, 'user');
20+
21+
expect(result).toBe(data);
22+
});
23+
24+
describe('V2 to V1 transformation', () => {
25+
it('should transform user data from V2 to V1 format', () => {
26+
const v2UserData = {
27+
id: 1,
28+
name: 'John Doe',
29+
email: 'john@example.com',
30+
createdAt: '2026-01-01T00:00:00.000Z',
31+
updatedAt: '2026-01-02T00:00:00.000Z',
32+
trustScore: 85
33+
};
34+
35+
const result = service.transform(v2UserData, ApiVersionEnum.V2, ApiVersionEnum.V1, 'user');
36+
37+
expect(result).toEqual({
38+
id: 1,
39+
name: 'John Doe',
40+
email: 'john@example.com'
41+
});
42+
expect(result).not.toHaveProperty('createdAt');
43+
expect(result).not.toHaveProperty('updatedAt');
44+
expect(result).not.toHaveProperty('trustScore');
45+
});
46+
47+
it('should transform property data from V2 to V1 format', () => {
48+
const v2PropertyData = {
49+
id: 1,
50+
address: '123 Main St',
51+
price: 500000,
52+
createdAt: '2026-01-01T00:00:00.000Z',
53+
verified: true
54+
};
55+
56+
const result = service.transform(v2PropertyData, ApiVersionEnum.V2, ApiVersionEnum.V1, 'property');
57+
58+
expect(result).toEqual({
59+
id: 1,
60+
address: '123 Main St',
61+
price: 500000
62+
});
63+
expect(result).not.toHaveProperty('createdAt');
64+
expect(result).not.toHaveProperty('verified');
65+
});
66+
67+
it('should transform arrays of V2 data to V1', () => {
68+
const v2Users = [
69+
{ id: 1, name: 'John', email: 'john@example.com', createdAt: '2026-01-01' },
70+
{ id: 2, name: 'Jane', email: 'jane@example.com', createdAt: '2026-01-02' }
71+
];
72+
73+
const result = service.transform(v2Users, ApiVersionEnum.V2, ApiVersionEnum.V1, 'user');
74+
75+
expect(result).toEqual([
76+
{ id: 1, name: 'John', email: 'john@example.com' },
77+
{ id: 2, name: 'Jane', email: 'jane@example.com' }
78+
]);
79+
});
80+
});
81+
82+
describe('V1 to V2 transformation', () => {
83+
it('should add V2 specific fields to user data when transforming from V1 to V2', () => {
84+
const v1UserData = {
85+
id: 1,
86+
name: 'John Doe',
87+
email: 'john@example.com'
88+
};
89+
90+
const result = service.transform(v1UserData, ApiVersionEnum.V1, ApiVersionEnum.V2, 'user');
91+
92+
expect(result).toHaveProperty('id', 1);
93+
expect(result).toHaveProperty('name', 'John Doe');
94+
expect(result).toHaveProperty('email', 'john@example.com');
95+
expect(result).toHaveProperty('createdAt');
96+
expect(result).toHaveProperty('updatedAt');
97+
});
98+
});
99+
100+
it('should return original data if no transformer exists for the entity type', () => {
101+
const data = { id: 1, someField: 'value', anotherField: 'other' };
102+
103+
const result = service.transform(data, ApiVersionEnum.V2, ApiVersionEnum.V1, 'unknown-entity');
104+
105+
expect(result).toEqual(data);
106+
});
107+
});
108+
109+
describe('register custom transformers', () => {
110+
it('should register and use a custom V2 to V1 transformer', () => {
111+
const customTransformer = jest.fn((data) => ({ customField: data.originalField }));
112+
service.registerV2ToV1Transformer('custom-entity', customTransformer);
113+
114+
const data = { originalField: 'test' };
115+
const result = service.transform(data, ApiVersionEnum.V2, ApiVersionEnum.V1, 'custom-entity');
116+
117+
expect(customTransformer).toHaveBeenCalledWith(data);
118+
expect(result).toEqual({ customField: 'test' });
119+
});
120+
121+
it('should register and use a custom V1 to V2 transformer', () => {
122+
const customTransformer = jest.fn((data) => ({ ...data, newField: 'added' }));
123+
service.registerV1ToV2Transformer('custom-entity', customTransformer);
124+
125+
const data = { originalField: 'test' };
126+
const result = service.transform(data, ApiVersionEnum.V1, ApiVersionEnum.V2, 'custom-entity');
127+
128+
expect(customTransformer).toHaveBeenCalledWith(data);
129+
expect(result).toEqual({ originalField: 'test', newField: 'added' });
130+
});
131+
});
132+
133+
describe('fieldExistsInVersion', () => {
134+
it('should return true for fields that exist in the specified version', () => {
135+
expect(service.fieldExistsInVersion('id', ApiVersionEnum.V1, 'user')).toBe(true);
136+
expect(service.fieldExistsInVersion('name', ApiVersionEnum.V2, 'user')).toBe(true);
137+
expect(service.fieldExistsInVersion('createdAt', ApiVersionEnum.V2, 'user')).toBe(true);
138+
expect(service.fieldExistsInVersion('verified', ApiVersionEnum.V2, 'property')).toBe(true);
139+
});
140+
141+
it('should return false for fields that do not exist in the specified version', () => {
142+
expect(service.fieldExistsInVersion('createdAt', ApiVersionEnum.V1, 'user')).toBe(false);
143+
expect(service.fieldExistsInVersion('trustScore', ApiVersionEnum.V1, 'user')).toBe(false);
144+
expect(service.fieldExistsInVersion('verified', ApiVersionEnum.V1, 'property')).toBe(false);
145+
});
146+
147+
it('should return false for unknown entity types or fields', () => {
148+
expect(service.fieldExistsInVersion('anyField', ApiVersionEnum.V1, 'unknown-entity')).toBe(false);
149+
expect(service.fieldExistsInVersion('unknownField', ApiVersionEnum.V1, 'user')).toBe(false);
150+
});
151+
});
152+
153+
describe('filterFieldsByVersion', () => {
154+
it('should filter an object to only include fields available in the specified version', () => {
155+
const userData = {
156+
id: 1,
157+
name: 'John',
158+
email: 'john@example.com',
159+
createdAt: '2026-01-01',
160+
trustScore: 85
161+
};
162+
163+
const filteredForV1 = service.filterFieldsByVersion(userData, ApiVersionEnum.V1, 'user');
164+
165+
expect(filteredForV1).toEqual({
166+
id: 1,
167+
name: 'John',
168+
email: 'john@example.com'
169+
});
170+
expect(filteredForV1).not.toHaveProperty('createdAt');
171+
expect(filteredForV1).not.toHaveProperty('trustScore');
172+
173+
const filteredForV2 = service.filterFieldsByVersion(userData, ApiVersionEnum.V2, 'user');
174+
expect(filteredForV2).toEqual(userData);
175+
});
176+
});
177+
});
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { DeprecationWarningInterceptor } from './deprecation-warning.interceptor';
2+
import { ExecutionContext, CallHandler, GoneException } from '@nestjs/common';
3+
import { Reflector } from '@nestjs/core';
4+
import { of } from 'rxjs';
5+
import { DEPRECATED_KEY, API_VERSION_KEY } from './api-version.decorator';
6+
import { ApiVersionEnum } from './api-version.constants';
7+
8+
describe('DeprecationWarningInterceptor', () => {
9+
let interceptor: DeprecationWarningInterceptor;
10+
let reflector: Reflector;
11+
let mockExecutionContext: Partial<ExecutionContext>;
12+
let mockCallHandler: Partial<CallHandler>;
13+
let mockRequest: any;
14+
let mockResponse: any;
15+
let mockSetHeader: jest.Mock;
16+
17+
beforeEach(() => {
18+
reflector = new Reflector();
19+
interceptor = new DeprecationWarningInterceptor(reflector);
20+
mockSetHeader = jest.fn();
21+
mockResponse = { setHeader: mockSetHeader };
22+
23+
mockRequest = {};
24+
25+
mockExecutionContext = {
26+
switchToHttp: jest.fn().mockReturnValue({
27+
getRequest: jest.fn().mockReturnValue(mockRequest),
28+
getResponse: jest.fn().mockReturnValue(mockResponse)
29+
}),
30+
getHandler: jest.fn()
31+
};
32+
33+
mockCallHandler = {
34+
handle: jest.fn().mockReturnValue(of({ data: 'test' }))
35+
};
36+
37+
jest.spyOn(reflector, 'get').mockImplementation((key: string) => {
38+
if (key === API_VERSION_KEY) return ApiVersionEnum.V1;
39+
return undefined;
40+
});
41+
});
42+
43+
it('should be defined', () => {
44+
expect(interceptor).toBeDefined();
45+
});
46+
47+
it('should apply deprecation headers for deprecated API versions', (done) => {
48+
interceptor.intercept(mockExecutionContext as ExecutionContext, mockCallHandler as CallHandler).subscribe({
49+
next: () => {
50+
expect(mockSetHeader).toHaveBeenCalledWith('Deprecation', 'true');
51+
expect(mockSetHeader).toHaveBeenCalledWith('Sunset', expect.any(String));
52+
expect(mockSetHeader).toHaveBeenCalledWith('Warning', expect.stringContaining('299 - "'));
53+
expect(mockSetHeader).toHaveBeenCalledWith('X-Deprecation-Notice', expect.stringContaining('Minimum 90-day deprecation window'));
54+
expect(mockSetHeader).toHaveBeenCalledWith('X-Migration-Guide', 'https://docs.propchain.io/migration');
55+
done();
56+
},
57+
error: done
58+
});
59+
});
60+
61+
it('should add _deprecationInfo to response data for deprecated endpoints', (done) => {
62+
mockCallHandler.handle = jest.fn().mockReturnValue(of({}));
63+
64+
interceptor.intercept(mockExecutionContext as ExecutionContext, mockCallHandler as CallHandler).subscribe({
65+
next: (result) => {
66+
expect(result._deprecationInfo).toBeDefined();
67+
expect(result._deprecationInfo.deprecated).toBe(true);
68+
expect(result._deprecationInfo.version).toBe(ApiVersionEnum.V1);
69+
expect(result._deprecationInfo.migrationGuide).toBe('https://docs.propchain.io/migration');
70+
done();
71+
},
72+
error: done
73+
});
74+
});
75+
76+
it('should apply deprecation headers when endpoint is marked with @Deprecated decorator', (done) => {
77+
jest.spyOn(reflector, 'get').mockImplementation((key: string) => {
78+
if (key === DEPRECATED_KEY) return true;
79+
return undefined;
80+
});
81+
82+
interceptor.intercept(mockExecutionContext as ExecutionContext, mockCallHandler as CallHandler).subscribe({
83+
next: () => {
84+
expect(mockSetHeader).toHaveBeenCalledWith('Deprecation', 'true');
85+
done();
86+
},
87+
error: done
88+
});
89+
});
90+
91+
it('should not add deprecation headers for active versions', (done) => {
92+
jest.spyOn(reflector, 'get').mockImplementation((key: string) => {
93+
if (key === API_VERSION_KEY) return ApiVersionEnum.V2;
94+
return undefined;
95+
});
96+
97+
interceptor.intercept(mockExecutionContext as ExecutionContext, mockCallHandler as CallHandler).subscribe({
98+
next: () => {
99+
// Should not set deprecation header for active version v2
100+
expect(mockSetHeader).not.toHaveBeenCalledWith('Deprecation', 'true');
101+
done();
102+
},
103+
error: done
104+
});
105+
});
106+
});

0 commit comments

Comments
 (0)