Skip to content

Instantly share code, notes, and snippets.

@venkata-qa
Last active October 13, 2025 06:25
Show Gist options
  • Select an option

  • Save venkata-qa/6cc5a268fa1eed88d07e632f8a14663a to your computer and use it in GitHub Desktop.

Select an option

Save venkata-qa/6cc5a268fa1eed88d07e632f8a14663a to your computer and use it in GitHub Desktop.

Revisions

  1. venkata-qa revised this gist Oct 13, 2025. 1 changed file with 5 additions and 241 deletions.
    246 changes: 5 additions & 241 deletions vk.txt
    Original file line number Diff line number Diff line change
    @@ -1,254 +1,18 @@
    /*
    * 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.setTarget(new HttpsTestTarget(
    "booking-dev.api.bupa.co.uk",
    443,
    "/booking"
    ));
    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.
    }
    }
  2. venkata-qa revised this gist Oct 13, 2025. 1 changed file with 254 additions and 156 deletions.
    410 changes: 254 additions & 156 deletions vk.txt
    Original file line number Diff line number Diff line change
    @@ -1,156 +1,254 @@
    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.
    });









    const dispatch = () => Promise.resolve();
    const getState = () => ({
    booking: {
    patientType: '',
    selectedPhotos: [],
    tellUsABitMoreText: '',
    formStatus: '',
    injectedBookingApiQueryParams: {},
    // ...add as needed
    },
    _api: {},
    });
    /*
    * 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.
    }
    }
  3. venkata-qa revised this gist Oct 12, 2025. 1 changed file with 21 additions and 0 deletions.
    21 changes: 21 additions & 0 deletions vk.txt
    Original 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: {},
    });
  4. venkata-qa revised this gist Oct 12, 2025. 1 changed file with 104 additions and 578 deletions.
    682 changes: 104 additions & 578 deletions vk.txt
    Original file line number Diff line number Diff line change
    @@ -1,609 +1,135 @@
    /**
    * 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 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;
    } 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 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",
    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 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(""),
    },
    }),
    },
    });
    const dispatch = () => Promise.resolve();
    const getState = () => ({});

    await pact.executeTest(async (mockServer) => {
    setBaseURL(mockServer.url);
    await expect(
    getAppointmentLocations({ page: 1, "per-page": 5 })()
    ).rejects.toBeDefined();
    });
    });
    describe('Booking Contract - Real Client Approach (Thunk Style)', () => {
    beforeEach(() => {
    bookingAxiosInstance.defaults.baseURL = undefined;
    });

    // ---------- 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({}),
    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 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",
    "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);
    });
    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();
    });
    });

    // ---------- 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),
    },
    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 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);
    });
    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();
    });
    });

    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({}),
    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 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);
    });
    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();
    });
    });

    // ---------- 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();
    });
    });
    });
    // Add more tests for locations, slots, appointments, etc., using
    // getAppointmentLocations(params)(dispatch, getState) etc.
    });
  5. venkata-qa revised this gist Oct 12, 2025. 1 changed file with 241 additions and 0 deletions.
    241 changes: 241 additions & 0 deletions vk.txt
    Original 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();
    });
    });
    });
    });
  6. venkata-qa revised this gist Oct 12, 2025. 1 changed file with 339 additions and 113 deletions.
    452 changes: 339 additions & 113 deletions vk.txt
    Original file line number Diff line number Diff line change
    @@ -1,142 +1,368 @@
    import path from 'path';
    import { PactV3, MatchersV3, SpecificationVersion } from '@pact-foundation/pact';
    /**
    * 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,
    } 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 = {
    Authorization: 'Bearer token123',
    Accept: 'application/json',
    'Content-Type': 'application/json',
    };
    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 provider = new PactV3({
    consumer: 'BookingFrontend',
    provider: 'BookingService',
    dir: path.resolve(__dirname, 'pacts'),
    logLevel: 'info',
    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,
    host: '127.0.0.1',
    port: 0,
    });

    describe('Booking Contract - Real Client Approach', () => {
    beforeEach(() => {
    // Just to be sure, overwrite baseURL before each test (isolation)
    bookingAxiosInstance.defaults.baseURL = undefined;
    });
    const setBaseURL = (mockBaseUrl: string) => {
    bookingAxiosInstance.defaults.baseURL = mockBaseUrl;
    };

    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' }) }),
    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);
    });
    });
    await provider.executeTest(async (mockServer) => {
    bookingAxiosInstance.defaults.baseURL = mockServer.url;
    const response = await getServiceCategories()();
    expect(response.status).toBe(200);
    expect(response.data).toBeDefined();
    });

    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);
    });
    });
    });

    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' }) }),
    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);
    });
    });
    await provider.executeTest(async (mockServer) => {
    bookingAxiosInstance.defaults.baseURL = mockServer.url;
    const response = await getAppointmentsFormat({})();
    expect(response.status).toBe(200);
    expect(response.data).toBeDefined();
    });

    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);
    });
    });
    });

    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' }) }),
    // ---------- 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);
    });
    });
    await provider.executeTest(async (mockServer) => {
    bookingAxiosInstance.defaults.baseURL = mockServer.url;
    const response = await getPractitionerGender({})();
    expect(response.status).toBe(200);
    expect(response.data).toBeDefined();

    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();
    });
    });
    });

    // Add more tests for locations, slots, appointments, etc., following the same pattern.
    // Example:
    // - getAppointmentLocations(params)()
    // - getSlots(params)()
    // - getAppointments(params)()
    // - getAppointment(appointmentId)()
    // ---------- 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);
    });
    });
    });

    // Adjust the test as you add more endpoints!
    });
    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",
  7. venkata-qa revised this gist Oct 12, 2025. 1 changed file with 56 additions and 18 deletions.
    74 changes: 56 additions & 18 deletions vk.txt
    Original file line number Diff line number Diff line change
    @@ -7,8 +7,19 @@ import {
    getAppointmentLocations,
    getSlots,
    getAppointments,
    getAppointment
    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 Tests (Function API)', () => {
    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: '...', value: '...' }), options: eachLike({ id: like('option-1'), label: like({ key: '...', value: '...' }) }) }) }),
    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) => {
    const fn = getServiceCategories();
    // If your function returns a callback, call it:
    const response = await fn({ baseURL: mockServer.url });
    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: '...', value: '...' }), options: eachLike({ id: like('option-1'), label: like({ key: '...', value: '...' }) }) }) }),
    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) => {
    const params = {}; // Add any required params for the function
    const fn = getAppointmentsFormat(params);
    const response = await fn({ baseURL: mockServer.url });
    bookingAxiosInstance.defaults.baseURL = mockServer.url;
    const response = await getAppointmentsFormat({})();
    expect(response.status).toBe(200);
    expect(response.data).toBeDefined();
    });
    });

    it('should get a single appointment by ID', async () => {
    it('should get practitioner genders', async () => {
    provider.addInteraction({
    states: [{ description: 'appointment exists' }],
    uponReceiving: 'a request for appointment by id',
    states: [{ description: 'practitioner genders exist' }],
    uponReceiving: 'a request for practitioner genders',
    withRequest: {
    method: 'GET',
    path: '/api/v1/appointments/my-id-123',
    path: '/api/v1/appointments/practitioners/genders',
    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({}) }),
    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) => {
    const fn = getAppointment('my-id-123');
    const response = await fn({ baseURL: mockServer.url });
    bookingAxiosInstance.defaults.baseURL = mockServer.url;
    const response = await getPractitionerGender({})();
    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.
    // 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!
    });
  8. venkata-qa created this gist Oct 12, 2025.
    104 changes: 104 additions & 0 deletions vk.txt
    Original 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.

    });