Last active
October 13, 2025 06:25
-
-
Save venkata-qa/6cc5a268fa1eed88d07e632f8a14663a to your computer and use it in GitHub Desktop.
Revisions
-
venkata-qa revised this gist
Oct 13, 2025 . 1 changed file with 5 additions and 241 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,254 +1,18 @@ import au.com.dius.pact.provider.junit5.*; import au.com.dius.pact.provider.junit5.http.*; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; @Provider("booking-backend") public class BookingServiceProviderPactTest { @TestTemplate @ExtendWith(PactVerificationInvocationContextProvider.class) void pactVerificationTestTemplate(PactVerificationContext context) { context.setTarget(new HttpsTestTarget( "booking-dev.api.bupa.co.uk", 443, "/booking" )); context.verifyInteraction(); } } -
venkata-qa revised this gist
Oct 13, 2025 . 1 changed file with 254 additions and 156 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,156 +1,254 @@ /* * NOTE: Provider state handlers in this contract test do not currently automate test data setup. * Verification results will rely on the presence and accuracy of existing (manual or legacy) data * in the backend/test environment. This is a temporary measure; provider state automation and repeatable * test data setup will be implemented as a separate initiative. See the README for more details. */ package com.booking; import au.com.dius.pact.provider.junit5.*; import au.com.dius.pact.provider.junit5.http.*; import au.com.dius.pact.provider.junitsupport.Provider; import au.com.dius.pact.provider.junitsupport.State; import au.com.dius.pact.provider.junitsupport.loader.PactBroker; import au.com.dius.pact.provider.junitsupport.loader.PactFolder; // import au.com.dius.pact.provider.spring.SpringBootHttpTarget; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; // Removed Spring Boot and Spring test imports import au.com.dius.pact.provider.junit5.http.HttpRequest; import au.com.dius.pact.provider.junit5.http.HttpRequestFilter; import provider.auth.TokenService; /** * Comprehensive Pact Provider Tests for Booking Service * * Verifies that the booking backend service correctly implements all contracts * defined by consumer applications (booking-frontend) based on the * booking-swagger.yaml OpenAPI specification. * * This test class: * - Loads pact contracts from the Pact Broker or local files * - Sets up provider states for each interaction scenario * - Verifies API responses match consumer expectations * - Validates all 9 endpoints with comprehensive test coverage * - Tests authentication, error handling, and edge cases */ @Provider("booking-backend") @PactBroker( host = "${pact.broker.host:localhost}", port = "${pact.broker.port:9292}", scheme = "${pact.broker.scheme:http}" ) @PactFolder("src/test/resources/pacts") public class BookingServiceProviderPactTest { // Static cache for the token during the test session private static String bearerToken = null; /** * Automatically inject a dynamic access token into every provider verification request. * Token is retrieved once (via TokenService) and reused for the whole test suite. */ @HttpRequestFilter public void injectAuthToken(HttpRequest request) { try { if (bearerToken == null) { bearerToken = TokenService.getUserToken(); } request.setHeader("Authorization", "Bearer " + bearerToken); } catch (Exception e) { throw new RuntimeException("Failed to generate auth token for contract test", e); } } @BeforeEach void before(PactVerificationContext context) { // Point to remote hosted provider service context.setTarget(new HttpTestTarget( "booking-dev.api.bupa.co.uk", 443, "/booking", true // use https )); } @TestTemplate @ExtendWith(PactVerificationInvocationContextProvider.class) void pactVerificationTestTemplate(PactVerificationContext context) { context.verifyInteraction(); } /** * Provider State: service categories exist * Sets up test data for service categories endpoint * Ensures the database contains valid service categories for testing * TODO: Automate when test data solution is ready. */ @State("service categories exist") public void serviceCategoriesExist() { // TODO: Automate state setup; currently relies on manual test data. } /** * Provider State: user is not authenticated * Configures the application to simulate unauthenticated requests * TODO: Automate when test data solution is ready. */ @State("user is not authenticated") public void userNotAuthenticated() { // TODO: Automate state setup; currently relies on manual test data. } /** * Provider State: appointment formats exist * Sets up test data for appointment formats endpoint * TODO: Automate when test data solution is ready. */ @State("appointment formats exist") public void appointmentFormatsExist() { // TODO: Automate state setup; currently relies on manual test data. } /** * Provider State: practitioner genders exist * Sets up test data for practitioner genders endpoint * TODO: Automate when test data solution is ready. */ @State("practitioner genders exist") public void practitionerGendersExist() { // TODO: Automate state setup; currently relies on manual test data. } /** * Provider State: share preferences exist * Sets up test data for share preferences endpoint * TODO: Automate when test data solution is ready. */ @State("share preferences exist") public void sharePreferencesExist() { // TODO: Automate state setup; currently relies on manual test data. } /** * Provider State: locations exist * Sets up test data for locations endpoint with required product identifier * TODO: Automate when test data solution is ready. */ @State("locations exist") public void locationsExist() { // TODO: Automate state setup; currently relies on manual test data. } /** * Provider State: missing required parameters * Configures the application to handle requests with missing required parameters * TODO: Automate when test data solution is ready. */ @State("missing required parameters") public void missingRequiredParameters() { // TODO: Automate state setup; currently relies on manual test data. } /** * Provider State: available slots exist * Sets up test data for available slots endpoint * TODO: Automate when test data solution is ready. */ @State("available slots exist") public void availableSlotsExist() { // TODO: Automate state setup; currently relies on manual test data. } /** * Provider State: appointments exist * Sets up test data for appointments list endpoint * TODO: Automate when test data solution is ready. */ @State("appointments exist") public void appointmentsExist() { // TODO: Automate state setup; currently relies on manual test data. } /** * Provider State: appointment can be created * Sets up the system to allow appointment creation * TODO: Automate when test data solution is ready. */ @State("appointment can be created") public void appointmentCanBeCreated() { // TODO: Automate state setup; currently relies on manual test data. } /** * Provider State: appointment exists * Sets up test data for appointment retrieval by ID * TODO: Automate when test data solution is ready. */ @State("appointment exists") public void appointmentExists() { // TODO: Automate state setup; currently relies on manual test data. } /** * Provider State: appointment does not exist * Ensures the specified appointment ID does not exist in the system * TODO: Automate when test data solution is ready. */ @State("appointment does not exist") public void appointmentDoesNotExist() { // TODO: Automate state setup; currently relies on manual test data. } /** * Provider State: appointment can be updated * Sets up the system to allow appointment updates * TODO: Automate when test data solution is ready. */ @State("appointment can be updated") public void appointmentCanBeUpdated() { // TODO: Automate state setup; currently relies on manual test data. } /** * Provider State: appointment can be rescheduled * Sets up the system to allow appointment rescheduling * TODO: Automate when test data solution is ready. */ @State("appointment can be rescheduled") public void appointmentCanBeRescheduled() { // TODO: Automate state setup; currently relies on manual test data. } /** * Provider State: service is healthy * Configures the system to report healthy status * TODO: Automate when test data solution is ready. */ @State("service is healthy") public void serviceIsHealthy() { // TODO: Automate state setup; currently relies on manual test data. } /** * Additional provider states for comprehensive error testing * TODO: Automate when test data solution is ready. */ @State("authentication required") public void authenticationRequired() { // TODO: Automate state setup; currently relies on manual test data. } @State("invalid appointment data") public void invalidAppointmentData() { // TODO: Automate state setup; currently relies on manual test data. } @State("system under maintenance") public void systemUnderMaintenance() { // TODO: Automate state setup; currently relies on manual test data. } @State("rate limit exceeded") public void rateLimitExceeded() { // TODO: Automate state setup; currently relies on manual test data. } } -
venkata-qa revised this gist
Oct 12, 2025 . 1 changed file with 21 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -133,3 +133,24 @@ describe('Booking Contract - Real Client Approach (Thunk Style)', () => { // Add more tests for locations, slots, appointments, etc., using // getAppointmentLocations(params)(dispatch, getState) etc. }); const dispatch = () => Promise.resolve(); const getState = () => ({ booking: { patientType: '', selectedPhotos: [], tellUsABitMoreText: '', formStatus: '', injectedBookingApiQueryParams: {}, // ...add as needed }, _api: {}, }); -
venkata-qa revised this gist
Oct 12, 2025 . 1 changed file with 104 additions and 578 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,609 +1,135 @@ import path from 'path'; import { PactV3, MatchersV3, SpecificationVersion } from '@pact-foundation/pact'; import { getServiceCategories, getAppointmentsFormat, getPractitionerGender, getAppointmentLocations, getSlots, getAppointments, getAppointment, } from '../__tests__/client/booking.endpoint'; import axios from 'axios'; let bookingAxiosInstance; try { bookingAxiosInstance = require('../__tests__/client/api/axiosInstance').bookingAxiosInstance; } catch { bookingAxiosInstance = axios.create(); } const { eachLike, like } = MatchersV3; const BASE_HEADERS = { Authorization: 'Bearer token123', Accept: 'application/json', 'Content-Type': 'application/json', }; const provider = new PactV3({ consumer: 'BookingFrontend', provider: 'BookingService', dir: path.resolve(__dirname, 'pacts'), logLevel: 'info', spec: SpecificationVersion.SPECIFICATION_VERSION_V3, host: '127.0.0.1', port: 0, }); const dispatch = () => Promise.resolve(); const getState = () => ({}); describe('Booking Contract - Real Client Approach (Thunk Style)', () => { beforeEach(() => { bookingAxiosInstance.defaults.baseURL = undefined; }); it('should get service categories', async () => { provider.addInteraction({ states: [{ description: 'service categories exist' }], uponReceiving: 'a request for service categories', withRequest: { method: 'GET', path: '/api/v1/appointments/services/categories', headers: BASE_HEADERS, }, willRespondWith: { status: 200, headers: { 'Content-Type': 'application/json' }, body: like({ data: eachLike({ id: like('cat-123'), label: like({ key: 'key', value: 'category' }), options: eachLike({ id: like('option-1'), label: like({ key: 'o', value: 'option label' }) }), }), }), }, }); await provider.executeTest(async (mockServer) => { bookingAxiosInstance.defaults.baseURL = mockServer.url; const response = await getServiceCategories()(dispatch, getState); expect(response.status).toBe(200); expect(response.data).toBeDefined(); }); }); it('should get appointment formats', async () => { provider.addInteraction({ states: [{ description: 'appointment formats exist' }], uponReceiving: 'a request for appointment formats', withRequest: { method: 'GET', path: '/api/v1/appointments/formats', headers: BASE_HEADERS, }, willRespondWith: { status: 200, headers: { 'Content-Type': 'application/json' }, body: like({ data: eachLike({ id: like('format-123'), label: like({ key: 'k1', value: 'format value' }), options: eachLike({ id: like('fo1'), label: like({ key: 'op', value: 'option format' }) }), }), }), }, }); await provider.executeTest(async (mockServer) => { bookingAxiosInstance.defaults.baseURL = mockServer.url; const response = await getAppointmentsFormat({})(dispatch, getState); expect(response.status).toBe(200); expect(response.data).toBeDefined(); }); }); it('should get practitioner genders', async () => { provider.addInteraction({ states: [{ description: 'practitioner genders exist' }], uponReceiving: 'a request for practitioner genders', withRequest: { method: 'GET', path: '/api/v1/appointments/practitioners/genders', headers: BASE_HEADERS, }, willRespondWith: { status: 200, headers: { 'Content-Type': 'application/json' }, body: like({ data: eachLike({ id: like('gender-1'), label: like({ key: 'gk', value: 'gender label' }), options: eachLike({ id: like('go1'), label: like({ key: 'gop', value: 'option' }) }), }), }), }, }); await provider.executeTest(async (mockServer) => { bookingAxiosInstance.defaults.baseURL = mockServer.url; const response = await getPractitionerGender({})(dispatch, getState); expect(response.status).toBe(200); expect(response.data).toBeDefined(); }); }); // Add more tests for locations, slots, appointments, etc., using // getAppointmentLocations(params)(dispatch, getState) etc. }); -
venkata-qa revised this gist
Oct 12, 2025 . 1 changed file with 241 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -366,3 +366,244 @@ describe("Booking Service – consumer contract", () => { "appointment-format": "video", "practitioner-gender": "male", "practitioner-id": "123", "location-id": "123", "slot-id": "12345", "slot-start": "2025-04-11T08:30", "slot-end": "2025-04-11T09:00", }; pact.addInteraction({ states: [{ description: "slot can be booked" }], uponReceiving: "a request to create an appointment", withRequest: { method: "POST", path: "/api/v1/appointments", body: like(requestBody), headers: { "Content-Type": "application/json" }, }, willRespondWith: { status: 200, headers: { "Content-Type": "application/json; charset=utf-8" }, body: like({ data: { created: boolean(true), providerAppointmentId: regex("^[0-9a-fA-F-]{36}$", "2a0b4dc0-7f2e-4e3e-8e6e-1fbe8b22c111"), providerStatusCode: integer(201), }, metadata: like({}), }), }, }); await pact.executeTest(async (mockServer) => { setBaseURL(mockServer.url); // your reserveSlot() takes a transformed payload; we pass the shape directly here const res = await reserveSlot(requestBody as any)(); expect(res.status).toBe(200); expect(res.data.data.created).toBe(true); }); }); }); // ---------- Appointments (list / detail / lifecycle) ---------- describe("GET /api/v1/appointments (list)", () => { it("returns paginated appointments (200)", async () => { const query = { page: "1", "per-page": "10", status: "BOOKED", }; pact.addInteraction({ states: [{ description: "appointments exist for the patient" }], uponReceiving: "a request to list appointments", withRequest: { method: "GET", path: "/api/v1/appointments", query }, willRespondWith: { status: 200, headers: { "Content-Type": "application/json; charset=utf-8" }, body: like({ data: eachLike({ key: string("606e65d2108fa9eff2673c8b83d3de85"), type: { key: string("AT0005"), name: string("Genomics screening"), }, start: string("2024-09-20T21:15:00"), finish: string("2024-09-20T21:30:00"), location: { key: string("1666") }, clinician: { key: string("1666"), sexType: integer(1), fullName: string("Angela Sweeting") }, }, { min: 1 }), metadata: { "current-page": integer(1), "last-page": integer(1), "per-page": integer(10), total: integer(1), }, }), }, }); await pact.executeTest(async (mockServer) => { setBaseURL(mockServer.url); const res = await getAppointments({ page: 1, "per-page": 10, status: "BOOKED" as any })(); expect(res.status).toBe(200); }); }); }); describe("GET /api/v1/appointments/{id} (detail)", () => { it("returns an appointment by id (200)", async () => { const id = "606e65d2108fa9eff2673c8b83d3de85"; pact.addInteraction({ states: [{ description: `appointment ${id} exists` }], uponReceiving: "a request to read an appointment", withRequest: { method: "GET", path: `/api/v1/appointments/${id}` }, willRespondWith: { status: 200, headers: { "Content-Type": "application/json; charset=utf-8" }, body: like({ data: { key: string(id), type: { key: string("AT0005"), name: string("Genomics screening") }, start: string("2024-09-20T21:15:00"), finish: string("2024-09-20T21:30:00"), location: { key: string("1666") }, clinician: { key: string("1666"), sexType: integer(2), fullName: string("Angela Sweeting") }, }, metadata: like({}), }), }, }); await pact.executeTest(async (mockServer) => { setBaseURL(mockServer.url); const res = await getAppointment(id)(); expect(res.status).toBe(200); expect(res.data.data.key).toBe(id); }); }); }); describe("PATCH /api/v1/appointments/{id} (cancel)", () => { it("cancels an appointment (200)", async () => { const id = "606e65d2108fa9eff2673c8b83d3de85"; pact.addInteraction({ states: [{ description: `appointment ${id} exists and can be cancelled` }], uponReceiving: "a request to cancel an appointment", withRequest: { method: "PATCH", path: `/api/v1/appointments/${id}`, headers: { "Content-Type": "application/json" }, body: like({ status: "CANCELLED" }), }, willRespondWith: { status: 200, headers: { "Content-Type": "application/json; charset=utf-8" }, body: like({ data: { key: string(id), // rest is not strongly constrained in PATCH response in spec, so keep loose: }, metadata: like({}), }), }, }); await pact.executeTest(async (mockServer) => { setBaseURL(mockServer.url); const res = await cancelAppointment(id)(); expect(res.status).toBe(200); }); }); }); describe("PUT /api/v1/appointments/{id} (reschedule)", () => { it("reschedules an appointment (200)", async () => { const id = "606e65d2108fa9eff2673c8b83d3de85"; const body = { "product-category": "GENOMICS", "product-identifier": "GNM01", "appointment-format": "video", "practitioner-gender": "male", "practitioner-id": "123", "location-id": "123", "slot-id": "9999", "slot-start": "2025-04-12T08:30", "slot-end": "2025-04-12T09:00", }; pact.addInteraction({ states: [{ description: `appointment ${id} exists and can be rescheduled` }], uponReceiving: "a request to reschedule an appointment", withRequest: { method: "PUT", path: `/api/v1/appointments/${id}`, headers: { "Content-Type": "application/json" }, body: like(body), }, willRespondWith: { status: 200, headers: { "Content-Type": "application/json; charset=utf-8" }, body: like({ data: { key: string(id), start: string("2025-04-12T08:30"), finish: string("2025-04-12T09:00"), location: { key: string("123") }, clinician: { key: string("123"), sexType: integer(1), fullName: string("John Doe"), }, type: { key: string("AT0005"), name: string("Genomics screening"), }, }, metadata: like({}), }), }, }); await pact.executeTest(async (mockServer) => { setBaseURL(mockServer.url); const res = await rescheduleAppointment({ appointmentId: id, params: body as any })(); expect(res.status).toBe(200); }); }); }); // ---------- Unauthorized example for a metadata endpoint ---------- describe("401 example", () => { it("returns 401 if not authenticated", async () => { pact.addInteraction({ states: [{ description: "request missing/invalid Authorization header" }], uponReceiving: "an unauthorized request for formats", withRequest: { method: "GET", path: "/api/v1/appointments/formats" }, willRespondWith: { status: 401, headers: { "Content-Type": "application/json; charset=utf-8" }, body: like({ status: string("Unauthorized"), status_code: integer(401), code: string("AUTHENTICATION_ERROR"), message: string("Missing or invalid Authorization header"), errors: eachLike({}), metadata: { timestamp: string("2025-02-10T12:45:22.406966"), path: string("/api/v1/appointments/services/categories"), }, }), }, }); await pact.executeTest(async (mockServer) => { setBaseURL(mockServer.url); await expect(getAppointmentsFormat({})()).rejects.toBeDefined(); }); }); }); }); -
venkata-qa revised this gist
Oct 12, 2025 . 1 changed file with 339 additions and 113 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,142 +1,368 @@ /** * Contract tests for booking.api.ts using Pact V3 * * Run with: npm run test:contract */ import path from "path"; import { PactV3, MatchersV3, SpecificationVersion } from "@pact-foundation/pact"; import { getServiceCategories, getAppointmentsFormat, getPractitionerGender, getDetailsNhs, getAppointmentLocations, getSlots, reserveSlot, getAppointments, getAppointment, cancelAppointment, rescheduleAppointment, } from "../src/booking.api"; // <- adjust import to your project layout // We’ll mutate the axios instance baseURL at runtime so the client hits the Pact mock server. import { bookingAxiosInstance } from "../src/api/axiosInstance"; const { eachLike, like, integer, boolean, string, regex } = MatchersV3; const pact = new PactV3({ consumer: "booking-web-client", provider: "booking-service", port: 0, // random free port dir: path.resolve(process.cwd(), "pacts"), logLevel: process.env.PACT_LOG_LEVEL ?? "warn", spec: SpecificationVersion.SPECIFICATION_VERSION_V3, }); const setBaseURL = (mockBaseUrl: string) => { bookingAxiosInstance.defaults.baseURL = mockBaseUrl; }; describe("Booking Service – consumer contract", () => { // ---------- Service & form metadata ---------- describe("GET /api/v1/appointments/services/categories", () => { it("returns service categories (200)", async () => { pact.addInteraction({ states: [{ description: "service categories available" }], uponReceiving: "a request for service categories", withRequest: { method: "GET", path: "/api/v1/appointments/services/categories", }, willRespondWith: { status: 200, headers: { "Content-Type": "application/json; charset=utf-8" }, body: like({ data: { id: regex( // UUID-ish "^[0-9a-fA-F-]{36}$", "3fa85f64-5717-4562-b3fc-2c963f66afa6" ), label: { key: string("serviceCategories.selection"), value: string("What can we help you with?"), }, options: eachLike({ id: like("e089e66c-bc64-49f6-99ef-4af721d9ba3c"), label: { key: like("serviceCategories.muscleBonesJoints"), value: like("Muscles, bones or joints"), description: like("Knee, back, shoulder or any other aches and pains"), }, metadata: { // free-form string map; assert one key we expect to be present by example "service-category": string("muscleBonesJoints"), }, }), metadata: like({}), itemType: string("SINGLE_RESPONSE_ITEM"), required: boolean(true), }, metadata: like({}), }), }, }); await pact.executeTest(async (mockServer) => { setBaseURL(mockServer.url); const res = await getServiceCategories()(); expect(res.status).toBe(200); expect(res.data.data).toBeDefined(); expect(res.data.data.options.length).toBeGreaterThan(0); }); }); }); describe("GET /api/v1/appointments/formats", () => { it("returns appointment formats (200)", async () => { pact.addInteraction({ states: [{ description: "appointment formats available" }], uponReceiving: "a request for appointment formats", withRequest: { method: "GET", path: "/api/v1/appointments/formats" }, willRespondWith: { status: 200, headers: { "Content-Type": "application/json; charset=utf-8" }, body: like({ data: { id: like("3fa85f64-5717-4562-b3fc-2c963f66afa6"), label: { key: string("appointmentFormats.selection"), value: string("Pick the preferred appointment format") }, options: eachLike({ id: like("e089e66c-bc64-49f6-99ef-4af721d9ba3c"), label: { key: like("appointmentFormats.video"), value: like("Video"), description: like("Remote appointment over video.") }, metadata: { "appointment-format": string("video") }, }), metadata: like({}), itemType: string("SINGLE_RESPONSE_ITEM"), required: boolean(true), }, metadata: like({}), }), }, }); await pact.executeTest(async (mockServer) => { setBaseURL(mockServer.url); const res = await getAppointmentsFormat({})(); // your client passes Metadata; server ignores/accepts empty expect(res.status).toBe(200); }); }); }); describe("GET /api/v1/appointments/practitioners/genders", () => { it("returns practitioner genders (200)", async () => { pact.addInteraction({ states: [{ description: "practitioner genders available" }], uponReceiving: "a request for practitioner genders", withRequest: { method: "GET", path: "/api/v1/appointments/practitioners/genders" }, willRespondWith: { status: 200, headers: { "Content-Type": "application/json; charset=utf-8" }, body: like({ data: { id: like("3fa85f64-5717-4562-b3fc-2c963f66afa6"), label: { key: string("practitionerGenders.selection"), value: string("Select the preferred practitioner gender") }, options: eachLike({ id: like("e089e66c-bc64-49f6-99ef-4af721d9ba3c"), label: { key: like("practitionerGenders.other"), value: like("No preference") }, metadata: { "practitioner-gender": string("other") }, }), metadata: like({}), itemType: string("SINGLE_RESPONSE_ITEM"), required: boolean(true), }, metadata: like({}), }), }, }); await pact.executeTest(async (mockServer) => { setBaseURL(mockServer.url); const res = await getPractitionerGender({})(); expect(res.status).toBe(200); }); }); }); describe("GET /api/v1/appointments/share", () => { it("returns NHS share details preference (200)", async () => { pact.addInteraction({ states: [{ description: "share preference available" }], uponReceiving: "a request for share preference (NHS details)", withRequest: { method: "GET", path: "/api/v1/appointments/share" }, willRespondWith: { status: 200, headers: { "Content-Type": "application/json; charset=utf-8" }, body: like({ data: { id: like("3fa85f64-5717-4562-b3fc-2c963f66afa6"), label: { key: string("share_appointment_details_consent"), value: string("Share appointment details?") }, options: eachLike({ id: like("e089e66c-bc64-49f6-99ef-4af721d9ba3c"), label: { key: like("yes"), value: like("Yes") }, metadata: { "share-nhs": like(true) }, }), metadata: like({}), itemType: string("SINGLE_RESPONSE_ITEM"), required: boolean(true), }, metadata: like({}), }), }, }); await pact.executeTest(async (mockServer) => { setBaseURL(mockServer.url); const res = await getDetailsNhs({})(); expect(res.status).toBe(200); }); }); }); // ---------- Locations ---------- describe("GET /api/v1/appointments/locations", () => { it("returns paginated locations (200) with required query", async () => { const query = { "product-identifier": "GNM01", page: "1", "per-page": "5", }; pact.addInteraction({ states: [{ description: "locations available for product GNM01" }], uponReceiving: "a request for appointment locations", withRequest: { method: "GET", path: "/api/v1/appointments/locations", query, }, willRespondWith: { status: 200, headers: { "Content-Type": "application/json; charset=utf-8" }, body: like({ data: { id: like("3fa85f64-5717-4562-b3fc-2c963f66afa6"), label: { key: string("appointmentLocation.selection"), value: string("Select the preferred appointment location") }, options: eachLike({ id: like("e089e66c-bc64-49f6-99ef-4af721d9ba3c"), label: { key: like("123"), value: like("Location name") }, metadata: { "location-id": string("123") }, }), metadata: like({}), itemType: string("SINGLE_RESPONSE_ITEM"), required: boolean(true), }, metadata: { "current-page": integer(1), "last-page": integer(10), "per-page": integer(5), total: integer(100), }, }), }, }); await pact.executeTest(async (mockServer) => { setBaseURL(mockServer.url); const res = await getAppointmentLocations({ "product-identifier": "GNM01", page: 1, "per-page": 5, })(); expect(res.status).toBe(200); }); }); it("returns 400 when product-identifier is missing", async () => { pact.addInteraction({ states: [{ description: "product identifier missing" }], uponReceiving: "a bad request for appointment locations", withRequest: { method: "GET", path: "/api/v1/appointments/locations", query: { page: "1", "per-page": "5" }, // no product-identifier }, willRespondWith: { status: 400, headers: { "Content-Type": "application/json; charset=utf-8" }, body: like({ status: string("Bad request"), status_code: integer(400), code: string("VALIDATION_ERROR"), message: string("Missing or invalid product-identifier property"), errors: eachLike({ field: string("product-identifier"), code: string("MISSING"), message: string("Product identifier can't be empty"), }), metadata: { timestamp: string("2025-02-10T12:45:22.406966"), path: string(""), }, }), }, }); await pact.executeTest(async (mockServer) => { setBaseURL(mockServer.url); await expect( getAppointmentLocations({ page: 1, "per-page": 5 })() ).rejects.toBeDefined(); }); }); }); // ---------- Slots / booking flow ---------- describe("GET /api/v1/appointments/slots", () => { it("returns available slots (200) with query filters", async () => { const query = { "product-identifier": "GNM01", "from-date": "2025-02-10", "to-date": "2025-02-17", "appointment-format": "video", "practitioner-gender": "male", "location-id": "9999", }; pact.addInteraction({ states: [{ description: "slots available for product GNM01" }], uponReceiving: "a request for appointment slots", withRequest: { method: "GET", path: "/api/v1/appointments/slots", query, }, willRespondWith: { status: 200, headers: { "Content-Type": "application/json; charset=utf-8" }, body: like({ data: { id: like("3fa85f64-5717-4562-b3fc-2c963f66afa6"), label: { key: string("appointmentSlots.selection"), value: string("Pick up preferred appointment slot") }, options: eachLike({ date: regex("^\\d{4}-\\d{2}-\\d{2}$", "2025-02-10"), times: eachLike({ start: regex("^\\d{2}:\\d{2}$", "08:00"), end: regex("^\\d{2}:\\d{2}$", "08:30"), metadata: { "slot-id": string("12345"), "practitioner-id": string("123321"), "practitioner-name": string("John Doe"), "location-id": string("9999"), }, }), }, { min: 1 }), metadata: like({}), itemType: string("DATE_TIME_RANGE"), required: boolean(true), }, metadata: like({}), }), }, }); await pact.executeTest(async (mockServer) => { setBaseURL(mockServer.url); const res = await getSlots({ "product-identifier": "GNM01", "from-date": "2025-02-10", "to-date": "2025-02-17", "appointment-format": "video", "practitioner-gender": "male", "location-id": "9999", })(); expect(res.status).toBe(200); }); }); }); describe("POST /api/v1/appointments (reserve/book)", () => { it("books an appointment (200)", async () => { // Minimal payload shaped like AppointmentCreateRequest const requestBody = { "product-category": "GENOMICS", "product-identifier": "GNM01", "appointment-format": "video", "practitioner-gender": "male", "practitioner-id": "123", -
venkata-qa revised this gist
Oct 12, 2025 . 1 changed file with 56 additions and 18 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -7,8 +7,19 @@ import { getAppointmentLocations, getSlots, getAppointments, getAppointment, } from '../__tests__/client/booking.endpoint'; // Update the file path if bookingAxiosInstance is available elsewhere: import axios from 'axios'; // Helper: try to import your actual axios instance, else use a new instance and inject it let bookingAxiosInstance; try { // @ts-ignore bookingAxiosInstance = require('../__tests__/client/api/axiosInstance').bookingAxiosInstance; } catch { bookingAxiosInstance = axios.create(); } const { eachLike, like } = MatchersV3; const BASE_HEADERS = { @@ -27,7 +38,12 @@ const provider = new PactV3({ port: 0, }); describe('Booking Contract - Real Client Approach', () => { beforeEach(() => { // Just to be sure, overwrite baseURL before each test (isolation) bookingAxiosInstance.defaults.baseURL = undefined; }); it('should get service categories', async () => { provider.addInteraction({ states: [{ description: 'service categories exist' }], @@ -40,13 +56,18 @@ describe('Booking Contract Tests (Function API)', () => { willRespondWith: { status: 200, headers: { 'Content-Type': 'application/json' }, body: like({ data: eachLike({ id: like('cat-123'), label: like({ key: 'key', value: 'category' }), options: eachLike({ id: like('option-1'), label: like({ key: 'o', value: 'option label' }) }), }), }), }, }); await provider.executeTest(async (mockServer) => { bookingAxiosInstance.defaults.baseURL = mockServer.url; const response = await getServiceCategories()(); expect(response.status).toBe(200); expect(response.data).toBeDefined(); }); @@ -64,41 +85,58 @@ describe('Booking Contract Tests (Function API)', () => { willRespondWith: { status: 200, headers: { 'Content-Type': 'application/json' }, body: like({ data: eachLike({ id: like('format-123'), label: like({ key: 'k1', value: 'format value' }), options: eachLike({ id: like('fo1'), label: like({ key: 'op', value: 'option format' }) }), }), }), }, }); await provider.executeTest(async (mockServer) => { bookingAxiosInstance.defaults.baseURL = mockServer.url; const response = await getAppointmentsFormat({})(); expect(response.status).toBe(200); expect(response.data).toBeDefined(); }); }); it('should get practitioner genders', async () => { provider.addInteraction({ states: [{ description: 'practitioner genders exist' }], uponReceiving: 'a request for practitioner genders', withRequest: { method: 'GET', path: '/api/v1/appointments/practitioners/genders', headers: BASE_HEADERS, }, willRespondWith: { status: 200, headers: { 'Content-Type': 'application/json' }, body: like({ data: eachLike({ id: like('gender-1'), label: like({ key: 'gk', value: 'gender label' }), options: eachLike({ id: like('go1'), label: like({ key: 'gop', value: 'option' }) }), }), }), }, }); await provider.executeTest(async (mockServer) => { bookingAxiosInstance.defaults.baseURL = mockServer.url; const response = await getPractitionerGender({})(); expect(response.status).toBe(200); expect(response.data).toBeDefined(); }); }); // Add more tests for locations, slots, appointments, etc., following the same pattern. // Example: // - getAppointmentLocations(params)() // - getSlots(params)() // - getAppointments(params)() // - getAppointment(appointmentId)() // Adjust the test as you add more endpoints! }); -
venkata-qa created this gist
Oct 12, 2025 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,104 @@ import path from 'path'; import { PactV3, MatchersV3, SpecificationVersion } from '@pact-foundation/pact'; import { getServiceCategories, getAppointmentsFormat, getPractitionerGender, getAppointmentLocations, getSlots, getAppointments, getAppointment } from '../__tests__/client/booking.endpoint'; const { eachLike, like } = MatchersV3; const BASE_HEADERS = { Authorization: 'Bearer token123', Accept: 'application/json', 'Content-Type': 'application/json', }; const provider = new PactV3({ consumer: 'BookingFrontend', provider: 'BookingService', dir: path.resolve(__dirname, 'pacts'), logLevel: 'info', spec: SpecificationVersion.SPECIFICATION_VERSION_V3, host: '127.0.0.1', port: 0, }); describe('Booking Contract Tests (Function API)', () => { it('should get service categories', async () => { provider.addInteraction({ states: [{ description: 'service categories exist' }], uponReceiving: 'a request for service categories', withRequest: { method: 'GET', path: '/api/v1/appointments/services/categories', headers: BASE_HEADERS, }, willRespondWith: { status: 200, headers: { 'Content-Type': 'application/json' }, body: like({ data: eachLike({ id: like('cat-123'), label: like({ key: '...', value: '...' }), options: eachLike({ id: like('option-1'), label: like({ key: '...', value: '...' }) }) }) }), }, }); await provider.executeTest(async (mockServer) => { const fn = getServiceCategories(); // If your function returns a callback, call it: const response = await fn({ baseURL: mockServer.url }); expect(response.status).toBe(200); expect(response.data).toBeDefined(); }); }); it('should get appointment formats', async () => { provider.addInteraction({ states: [{ description: 'appointment formats exist' }], uponReceiving: 'a request for appointment formats', withRequest: { method: 'GET', path: '/api/v1/appointments/formats', headers: BASE_HEADERS, }, willRespondWith: { status: 200, headers: { 'Content-Type': 'application/json' }, body: like({ data: eachLike({ id: like('format-123'), label: like({ key: '...', value: '...' }), options: eachLike({ id: like('option-1'), label: like({ key: '...', value: '...' }) }) }) }), }, }); await provider.executeTest(async (mockServer) => { const params = {}; // Add any required params for the function const fn = getAppointmentsFormat(params); const response = await fn({ baseURL: mockServer.url }); expect(response.status).toBe(200); expect(response.data).toBeDefined(); }); }); it('should get a single appointment by ID', async () => { provider.addInteraction({ states: [{ description: 'appointment exists' }], uponReceiving: 'a request for appointment by id', withRequest: { method: 'GET', path: '/api/v1/appointments/my-id-123', headers: BASE_HEADERS, }, willRespondWith: { status: 200, headers: { 'Content-Type': 'application/json' }, body: like({ data: { key: 'my-id-123', type: like({ key: '...', name: '...' }), start: like('2024-09-20T21:15:00'), finish: like('2024-09-20T21:30:00'), location: like({ key: '...' }), clinician: like({ key: '...' }) }, metadata: like({}) }), }, }); await provider.executeTest(async (mockServer) => { const fn = getAppointment('my-id-123'); const response = await fn({ baseURL: mockServer.url }); expect(response.status).toBe(200); expect(response.data).toBeDefined(); }); }); // Add more tests using other functions as needed, referencing booking-swagger.yaml for endpoints and expected data. });