import { diff } from "deep-diff" ; import queryString from "query-string" ; import { compareTwoStrings } from "string-similarity" ; import { serialiseBuffer } from "./persistence" ; import { Headers , PersistedBuffer , TapeRecord } from "./tape" ; /**
 * Returns a "similarity score" between a request and an existing record.
 *
 * The score is a number between 0 and +Infinity:
 * - 0 means "identical" (perfect match)
 * - +Infinity means "very different" (no match)
 */ export function computeSimilarity ( requestMethod , requestPath , requestHeaders , requestBody , compareTo ) { if ( requestMethod !== compareTo . request . method ) { return + Infinity ; } if ( pathWithoutQueryParameters ( requestPath ) !== pathWithoutQueryParameters ( compareTo . request . path ) ) { return + Infinity ; } const parsedQuery = queryParameters ( requestPath ) ; const parsedCompareToQuery = queryParameters ( compareTo . request . path ) ; const headers = stripExtraneousHeaders ( requestHeaders ) ; const compareToHeaders = stripExtraneousHeaders ( compareTo . request . headers ) ; const serialisedRequestBody = serialiseBuffer ( requestBody , requestHeaders ) ; const serialisedCompareToRequestBody = serialiseBuffer ( compareTo . request . body , compareTo . request . headers ) ; return ( countObjectDifferences ( parsedQuery , parsedCompareToQuery ) + countObjectDifferences ( headers , compareToHeaders ) + countBodyDifferences ( serialisedRequestBody , serialisedCompareToRequestBody ) ) ; } /**
 * Returns the numbers of differences between two persisted body buffers.
 */ function countBodyDifferences ( a , b ) { if ( a . encoding === "utf8" && b . encoding === "utf8" ) { try { const requestBodyJson = JSON . parse ( a . data || "{}" ) ; const recordBodyJson = JSON . parse ( b . data || "{}" ) ; return countObjectDifferences ( requestBodyJson , recordBodyJson ) ; } catch ( e ) { return countStringDifferences ( a . data , b . data ) ; } } if ( a . encoding === "base64" && b . encoding === "base64" ) { return countStringDifferences ( a . data , b . data ) ; } return + Infinity ; } /**
 * Returns the number of fields that differ between two objects.
 */ function countObjectDifferences ( a , b ) { return ( diff ( a , b ) || [ ] ) . length ; } /**
 * Returns the number of characters that differ between two strings.
 */ function countStringDifferences ( a , b ) { const stringSimilarityScore = compareTwoStrings ( a , b ) ; const numberOfDifferentCharacters = Math . round ( ( ( 1 - stringSimilarityScore ) * ( a . length + b . length ) ) / 2 ) ; return numberOfDifferentCharacters ; } function pathWithoutQueryParameters ( path ) { const questionMarkPosition = path . indexOf ( "?" ) ; if ( questionMarkPosition !== - 1 ) { return path . substr ( 0 , questionMarkPosition ) ; } else { return path ; } } function queryParameters ( path ) { const questionMarkPosition = path . indexOf ( "?" ) ; if ( questionMarkPosition !== - 1 ) { return queryString . parse ( path . substr ( questionMarkPosition ) ) ; } else { return { } ; } } /**
 * Strips out headers that are likely to result in false negatives.
 */ function stripExtraneousHeaders ( headers ) { const safeHeaders = { } ; for ( const key of Object . keys ( headers ) ) { switch ( key ) { case "accept" : case "accept-encoding" : case "age" : case "cache-control" : case "clear-site-data" : case "connection" : case "expires" : case "from" : case "host" : case "postman-token" : case "pragma" : case "referer" : case "referer-policy" : case "te" : case "trailer" : case "transfer-encoding" : case "user-agent" : case "warning" : continue ; default : safeHeaders [ key ] = headers [ key ] ; } } return safeHeaders ; }