Kevin Grady
09/22/2023, 7:12 PMERROR (65379): pact@12.1.0: Test failed for the following reasons:
Mock server failed with the following mismatches:
0) The following request was incorrect:
POST /productentitlements/{exampleID1}/tenants/{exampleID2}
After doing some digging into the node_modules pact files we were able to console.log out something more helpful:
console.log
mismatches: [
{
actual: '',
expected: 'application/x.avidxchange.productentitlements+json;version=1.0.0',
key: 'Content-Type',
mismatch: "Mismatch with header 'Content-Type': Expected value 'application/x.avidxchange.productentitlements+json;version=1.0.0' at index 1 but was missing (actual has 1 value(s))",
type: 'HeaderMismatch'
}
]
at generateMockServerError (../../../node_modules/@pact-foundation/src/v3/display.ts:135:3)
This implies that our Content-Type is somehow being removed at some point after the test is run. Has anyone seen an issue like this? We tried following the path the headers take through the compiled pact code but were unable to find a place where the Content-Type was removed.
One thing to note is that same header is being passed with all requests, including the GETs which are passing. It seems to be something related to requests that contained a body, but I don’t have any evidence to say that’s what it is beyond correlation.Matt (pactflow.io / pact-js / pact-go)
Kevin Grady
10/02/2023, 6:30 PMimport { HttpModule } from '@nestjs/axios';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { Test } from '@nestjs/testing';
import { PactV3 } from '@pact-foundation/pact';
import { like, regex } from '@pact-foundation/pact/src/dsl/matchers';
import { camelCaseObjectKeys } from '@ui-coe/shared/bff/util';
import { lastValueFrom } from 'rxjs';
import { HttpConfigService } from '../../../services/http-config.service';
import {
CreateOrganizationDto,
IListWrapperAPI,
IOrganizationAPI,
OrganizationAddressAPI,
UpdateAddressDto,
UpdateOrganizationDto,
} from '../models';
import { OrganizationService } from './organization.service';
import { PACT_MOCK_SEVER_CONFIG_HOST, PACT_MOCK_SEVER_CONFIG_PORT } from '@ui-coe/shared/bff/types';
const mockProvider = new PactV3({
consumer: 'AvidPay Network',
provider: 'Organization.Api',
dir: './pact/orgaization/',
host: PACT_MOCK_SEVER_CONFIG_HOST, // "127.0.0.1"
port: PACT_MOCK_SEVER_CONFIG_PORT, // 65535
});
const requestHeaders = {
'Content-Type': 'application/x.avidxchange.accounting+json;version=1.0.0',
Accept: 'application/x.avidxchange.accounting+json;version=1.0.0',
Authorization:
'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiIwMHUxMnR5bWNnMHk3OHJUejFkNyIsInRlbmFudElkIjoiY3Zoa3pqMndjZGZjNGlnZWkwcTIiLCJuYmYiOjE2ODE5MTE4MjYsImV4cCI6MTcxMzQ0NzgyNiwiaWF0IjoxNjgxOTExODI2LCJpc3MiOiJBY2NvdW50aW5nQXV0aFNlcnZlciIsImF1ZCI6IkFjY291bnRpbmcifQ.QNcP0fo0fmIm3UQ85tgpQzz6AHlk621JrCtt_XRHLPY',
'x-tenant-id': '7qvmnw5nfupmb6j9gp6m',
};
const responseHeaders = {
'Content-Type': regex({
generate: 'application/x.avidxchange.accounting+json;charset=utf-8;version=1.0.0',
matcher:
'application\\/x\\.avidxchange\\.accounting\\+json;charset=utf-8;version=([0-9]\\.?){3}',
}),
};
describe('Organization Service', () => {
let service: OrganizationService;
const mockServerUrl = `http://${PACT_MOCK_SEVER_CONFIG_HOST}:${PACT_MOCK_SEVER_CONFIG_PORT}`;
beforeEach(async () => {
const app = await Test.createTestingModule({
imports: [ConfigModule, HttpModule],
providers: [
OrganizationService,
HttpConfigService,
{
provide: 'MOCK_ENV',
useValue: false,
},
{
provide: ConfigService,
useValue: {
get: jest.fn((key: string) => {
if (key === 'ACCOUNTING_BASE_URL') return mockServerUrl;
}),
},
},
],
}).compile();
service = app.get<OrganizationService>(OrganizationService);
});
describe('createOrganization', () => {
const reqBody: CreateOrganizationDto = {
organization_name: 'Stark Industries',
organization_code: 'SI',
source_system: 'Swagger-UI',
};
const returnData: IOrganizationAPI = {
organization_id: 'upcd981lg8z84tozng3w',
organization_name: 'Stark Industries',
organization_code: 'SI',
is_active: 'true',
created_timestamp: '2023-04-28T00:00:00.000Z',
created_by_user_id: '7nsxqpkecdggnk3i1wqj',
last_modified_timestamp: '2023-04-28T00:00:00.000Z',
last_modified_by_user_id: '7nsxqpkecdggnk3i1wqj',
};
it('should return the correct data from POST call', async () => {
await mockProvider.addInteraction({
states: [],
uponReceiving: 'a request to create an organization',
withRequest: {
method: 'POST',
path: '/accounting/organization',
headers: requestHeaders,
body: like(reqBody),
},
willRespondWith: {
status: 201,
headers: responseHeaders,
body: like(returnData),
},
});
await mockProvider.executeTest(async () => {
const response = await lastValueFrom(service.createOrganization(requestHeaders, reqBody));
expect(response).toStrictEqual(camelCaseObjectKeys(returnData));
});
});
});
describe('getOrganization', () => {
const returnData: IListWrapperAPI<IOrganizationAPI> = {
items_requested: 10,
items_returned: 10,
items_total: 10,
offset: 0,
items: [],
};
it('should return the correct data from GET call', async () => {
await mockProvider.addInteraction({
states: [
{
description: 'a list of tenants exists',
parameters: {},
},
],
uponReceiving: 'a request to get organizations',
withRequest: {
method: 'GET',
path: '/accounting/organization',
headers: requestHeaders,
},
willRespondWith: {
status: 200,
headers: responseHeaders,
body: like(returnData),
},
});
await mockProvider.executeTest(async () => {
const response = await lastValueFrom(service.getOrganizations(requestHeaders, {}));
expect(response).toStrictEqual(camelCaseObjectKeys(returnData));
});
});
});
describe('getOrganizationById', () => {
const id = 'upcd981lg8z84tozng3w';
const returnData: IOrganizationAPI = {
organization_id: 'upcd981lg8z84tozng3w',
organization_name: 'Stark Industries',
organization_code: 'SI',
is_active: 'true',
created_timestamp: '2023-04-28T00:00:00.000Z',
created_by_user_id: '7nsxqpkecdggnk3i1wqj',
last_modified_timestamp: '2023-04-28T00:00:00.000Z',
last_modified_by_user_id: '7nsxqpkecdggnk3i1wqj',
};
it('should return the correct data from getOrganizationById call', async () => {
await mockProvider.addInteraction({
states: [
{
description: 'a organization exists with id ' + id + '',
parameters: { organization_id: id, is_active: 'true' },
},
],
uponReceiving: 'a request to get organizations',
withRequest: {
method: 'GET',
path: '/accounting/organization/' + id,
headers: requestHeaders,
},
willRespondWith: {
status: 200,
headers: responseHeaders,
body: like(returnData),
},
});
await mockProvider.executeTest(async () => {
const response = await lastValueFrom(service.getOrganizationById(id, requestHeaders));
expect(response).toStrictEqual(camelCaseObjectKeys(returnData));
});
});
});
describe('updateOrganization', () => {
const id = 'upcd981lg8z84tozng3w';
const reqBody: UpdateOrganizationDto = {
organization_name: 'New Stark Industries',
};
const returnData: IOrganizationAPI = {
organization_id: 'upcd981lg8z84tozng3w',
organization_name: 'New Stark Industries',
organization_code: 'SI',
is_active: 'true',
created_timestamp: '2023-04-28T00:00:00.000Z',
created_by_user_id: '7nsxqpkecdggnk3i1wqj',
last_modified_timestamp: '2023-04-28T00:00:00.000Z',
last_modified_by_user_id: '7nsxqpkecdggnk3i1wqj',
};
it('should return the correct data from PUT call', async () => {
await mockProvider.addInteraction({
states: [
{
description: 'a organization exists with id ' + id + '',
parameters: { organization_id: id, is_active: 'true' },
},
],
uponReceiving: 'a request to update an organization',
withRequest: {
method: 'PUT',
path: '/accounting/organization/' + id,
headers: requestHeaders,
body: like(reqBody),
},
willRespondWith: {
status: 200,
headers: responseHeaders,
body: like(returnData),
},
});
await mockProvider.executeTest(async () => {
const response = await lastValueFrom(
service.updateOrganization(id, requestHeaders, reqBody)
);
expect(response).toStrictEqual(camelCaseObjectKeys(returnData));
});
});
});
});
Kevin Grady
10/02/2023, 6:30 PMimport { HttpService } from '@nestjs/axios';
import { HttpException, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { camelCaseObjectKeys, snakeCaseObjectKeys } from '@ui-coe/shared/bff/util';
import { IGenericStringObject, IListWrapperAPI } from '@ui-coe/shared/bff/types';
import { HttpConfigService } from '../../../services/http-config.service';
import { busHierErrorMapper } from '../../shared';
import {
BusHierError,
CreateOrganizationDto,
IOrganizationAPI,
ListWrapper,
Organization,
OrganizationAddress,
OrganizationAddressAPI,
OrganizationList,
OrganizationMapped,
UpdateAddressDto,
UpdateOrganizationDto,
} from '../models';
@Injectable()
export class OrganizationService {
constructor(private http: HttpService, private httpConfigService: HttpConfigService) {}
/**
* @method createOrganization
* @description Create a new Organization
* @param headers IGenericStringObject
* @param body CreateOrganizationDto
* @returns `Observable<Organization | BusHierError>`
*/
createOrganization(
headers: IGenericStringObject,
body: CreateOrganizationDto
): Observable<Organization | BusHierError> {
return this.http
.post(this.httpConfigService.createOrganization(), snakeCaseObjectKeys(body), { headers })
.pipe(
map(response => camelCaseObjectKeys<IOrganizationAPI, Organization>(response.data)),
catchError(err => {
throw new HttpException(busHierErrorMapper(err), err.response?.status);
})
);
}
/**
* @method createOrganizationAddress
* @description Get a list of Organizations
* @param headers IGenericStringObject
* @param query IGenericStringObject
* @returns `Observable<OrganizationList>`
*/
getOrganizations(
headers: IGenericStringObject,
query: IGenericStringObject
): Observable<OrganizationList> {
return this.http
.get(this.httpConfigService.getOrganizations(), {
headers,
params: snakeCaseObjectKeys(query),
})
.pipe(
map(response => {
const camelCaseResponse: ListWrapper<Organization> = camelCaseObjectKeys<
IListWrapperAPI<IOrganizationAPI>,
ListWrapper<Organization>
>(response.data);
const items: OrganizationMapped[] = camelCaseResponse.items.map((item: Organization) => ({
organizationId: item.organizationId,
organizationName: item.organizationName,
organizationCode: item.organizationCode,
isActive: item.isActive,
}));
return {
...camelCaseResponse,
items,
};
}),
catchError(err => {
throw new HttpException(busHierErrorMapper(err), err.response?.status);
})
);
}
/**
* @method getOrganizationById
* @description Get Organization by Id
* @param id string representing the organizationId
* @param headers IGenericStringObject
* @returns `Observable<Organization>`
*/
getOrganizationById(id: string, headers: IGenericStringObject): Observable<Organization> {
return this.http.get(this.httpConfigService.getOrganizationById(id), { headers }).pipe(
map(response => camelCaseObjectKeys<IOrganizationAPI, Organization>(response.data)),
catchError(err => {
throw new HttpException(busHierErrorMapper(err), err.response?.status);
})
);
}
/**
* @method getOrganizationAddresses
* @description Update an Organization
* @param id string representing the organizationId
* @param headers IGenericStringObject
* @param body UpdateOrganizationDto
* @returns `Observable<Organization | BusHierError>`
*/
updateOrganization(
id: string,
headers: IGenericStringObject,
body: UpdateOrganizationDto
): Observable<Organization | BusHierError> {
return this.http
.put(this.httpConfigService.updateOrganization(id), snakeCaseObjectKeys(body), { headers })
.pipe(
map(response => camelCaseObjectKeys<IOrganizationAPI, Organization>(response.data)),
catchError(err => {
throw new HttpException(busHierErrorMapper(err), err.response?.status);
})
);
}
}
The httpConfigService simply determines whether the method will use a local path or not for example:
public createOrganization(): string {
const config: httpConfig = {
path: this.organizationUrl,
localPath: '/create-organization.json',
};
return this.getPath(config);
}
Kevin Grady
10/02/2023, 6:33 PMMatt (pactflow.io / pact-js / pact-go)
Matt (pactflow.io / pact-js / pact-go)
Kevin Grady
10/11/2023, 4:54 PMKevin Grady
11/02/2023, 8:57 PM