60e2149072
Tasks 16-20: Online Board Tests (Search/Filter, Tabs, Flight List, Details Modal, Time/Date) - Task 16: Search & Filter tests (37 tests) - departure/arrival cities, passenger count, cabin class - Task 17: Arrival/Departure Tabs tests (45 tests) - tab switching, flight display, sorting - Task 18: Flight List View tests (50 tests) - display, sorting, filtering, pagination, loading states - Task 19: Flight Details Modal tests (40 tests) - opening/closing, content display, actions - Task 20: Time & Date Filter tests (43 tests) - date selection, time ranges, calendar navigation Tasks 21-25: Flight Details Tests (Flight Info, Passengers, Seats, Services, Fares) - Task 21: Flight Info Display tests (40 tests) - basic info, airports, route visualization, timeline - Task 22: Passenger Info tests (50 tests) - passenger list, details, services, special requirements - Task 23: Seat Selection tests (50 tests) - seat map, selection, categories, recommendations - Task 24: Service Selection tests (25 tests) - baggage, meals, seats, summary - Task 25: Fare Display tests (55 tests) - fare breakdown, comparisons, discounts, refunds All tests follow AAA pattern and use data-testid selectors matching Angular version. Total: 245 tests across 10 feature suites.
203 lines
5.6 KiB
JavaScript
203 lines
5.6 KiB
JavaScript
const {
|
|
URL_MATCHER,
|
|
TYPE_URL,
|
|
TYPE_REGEX,
|
|
TYPE_PATH,
|
|
} = require('./matchers')
|
|
|
|
/**
|
|
* creates a string of asterisks,
|
|
* this forces a minimum asterisk for security purposes
|
|
*/
|
|
const asterisk = (length = 0) => {
|
|
length = typeof length === 'string' ? length.length : length
|
|
if (length < 8) {
|
|
return '*'.repeat(8)
|
|
}
|
|
return '*'.repeat(length)
|
|
}
|
|
|
|
/**
|
|
* escapes all special regex chars
|
|
* @see https://stackoverflow.com/a/9310752
|
|
* @see https://github.com/tc39/proposal-regex-escaping
|
|
*/
|
|
const escapeRegExp = (text) => {
|
|
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, `\\$&`)
|
|
}
|
|
|
|
/**
|
|
* provieds a regex "or" of the url versions of a string
|
|
*/
|
|
const urlEncodeRegexGroup = (value) => {
|
|
const decoded = decodeURIComponent(value)
|
|
const encoded = encodeURIComponent(value)
|
|
const union = [...new Set([encoded, decoded, value])].map(escapeRegExp).join('|')
|
|
return union
|
|
}
|
|
|
|
/**
|
|
* a tagged template literal that returns a regex ensures all variables are excaped
|
|
*/
|
|
const urlEncodeRegexTag = (strings, ...values) => {
|
|
let pattern = ''
|
|
for (let i = 0; i < values.length; i++) {
|
|
pattern += strings[i] + `(${urlEncodeRegexGroup(values[i])})`
|
|
}
|
|
pattern += strings[strings.length - 1]
|
|
return new RegExp(pattern)
|
|
}
|
|
|
|
/**
|
|
* creates a matcher for redacting url hostname
|
|
*/
|
|
const redactUrlHostnameMatcher = ({ hostname, replacement } = {}) => ({
|
|
type: TYPE_URL,
|
|
predicate: ({ url }) => url.hostname === hostname,
|
|
pattern: ({ url }) => {
|
|
return urlEncodeRegexTag`(^${url.protocol}//${url.username}:.+@)?${url.hostname}`
|
|
},
|
|
replacement: `$1${replacement || asterisk()}`,
|
|
})
|
|
|
|
/**
|
|
* creates a matcher for redacting url search / query parameter values
|
|
*/
|
|
const redactUrlSearchParamsMatcher = ({ param, replacement } = {}) => ({
|
|
type: TYPE_URL,
|
|
predicate: ({ url }) => url.searchParams.has(param),
|
|
pattern: ({ url }) => urlEncodeRegexTag`(${param}=)${url.searchParams.get(param)}`,
|
|
replacement: `$1${replacement || asterisk()}`,
|
|
})
|
|
|
|
/** creates a matcher for redacting the url password */
|
|
const redactUrlPasswordMatcher = ({ replacement } = {}) => ({
|
|
type: TYPE_URL,
|
|
predicate: ({ url }) => url.password,
|
|
pattern: ({ url }) => urlEncodeRegexTag`(^${url.protocol}//${url.username}:)${url.password}`,
|
|
replacement: `$1${replacement || asterisk()}`,
|
|
})
|
|
|
|
const redactUrlReplacement = (...matchers) => (subValue) => {
|
|
try {
|
|
const url = new URL(subValue)
|
|
return redactMatchers(...matchers)(subValue, { url })
|
|
} catch (err) {
|
|
return subValue
|
|
}
|
|
}
|
|
|
|
/**
|
|
* creates a matcher / submatcher for urls, this function allows you to first
|
|
* collect all urls within a larger string and then pass those urls to a
|
|
* submatcher
|
|
*
|
|
* @example
|
|
* console.log("this will first match all urls, then pass those urls to the password patcher")
|
|
* redactMatchers(redactUrlMatcher(redactUrlPasswordMatcher()))
|
|
*
|
|
* @example
|
|
* console.log(
|
|
* "this will assume you are passing in a string that is a url, and will redact the password"
|
|
* )
|
|
* redactMatchers(redactUrlPasswordMatcher())
|
|
*
|
|
*/
|
|
const redactUrlMatcher = (...matchers) => {
|
|
return {
|
|
...URL_MATCHER,
|
|
replacement: redactUrlReplacement(...matchers),
|
|
}
|
|
}
|
|
|
|
const matcherFunctions = {
|
|
[TYPE_REGEX]: (matcher) => (value) => {
|
|
if (typeof value === 'string') {
|
|
value = value.replace(matcher.pattern, matcher.replacement)
|
|
}
|
|
return value
|
|
},
|
|
[TYPE_URL]: (matcher) => (value, ctx) => {
|
|
if (typeof value === 'string') {
|
|
try {
|
|
const url = ctx?.url || new URL(value)
|
|
const { predicate, pattern } = matcher
|
|
const predicateValue = predicate({ url })
|
|
if (predicateValue) {
|
|
value = value.replace(pattern({ url }), matcher.replacement)
|
|
}
|
|
} catch (_e) {
|
|
return value
|
|
}
|
|
}
|
|
return value
|
|
},
|
|
[TYPE_PATH]: (matcher) => (value, ctx) => {
|
|
const rawPath = ctx?.path
|
|
const path = rawPath.join('.').toLowerCase()
|
|
const { predicate, replacement } = matcher
|
|
const replace = typeof replacement === 'function' ? replacement : () => replacement
|
|
const shouldRun = predicate({ rawPath, path })
|
|
if (shouldRun) {
|
|
value = replace(value, { rawPath, path })
|
|
}
|
|
return value
|
|
},
|
|
}
|
|
|
|
/** converts a matcher to a function */
|
|
const redactMatcher = (matcher) => {
|
|
return matcherFunctions[matcher.type](matcher)
|
|
}
|
|
|
|
/** converts a series of matchers to a function */
|
|
const redactMatchers = (...matchers) => (value, ctx) => {
|
|
const flatMatchers = matchers.flat()
|
|
return flatMatchers.reduce((result, matcher) => {
|
|
const fn = (typeof matcher === 'function') ? matcher : redactMatcher(matcher)
|
|
return fn(result, ctx)
|
|
}, value)
|
|
}
|
|
|
|
/**
|
|
* replacement handler, keeping $1 (if it exists) and replacing the
|
|
* rest of the string with asterisks, maintaining string length
|
|
*/
|
|
const redactDynamicReplacement = () => (value, start) => {
|
|
if (typeof start === 'number') {
|
|
return asterisk(value)
|
|
}
|
|
return start + asterisk(value.substring(start.length).length)
|
|
}
|
|
|
|
/**
|
|
* replacement handler, keeping $1 (if it exists) and replacing the
|
|
* rest of the string with a fixed number of asterisks
|
|
*/
|
|
const redactFixedReplacement = (length) => (_value, start) => {
|
|
if (typeof start === 'number') {
|
|
return asterisk(length)
|
|
}
|
|
return start + asterisk(length)
|
|
}
|
|
|
|
const redactUrlPassword = (value, replacement) => {
|
|
return redactMatchers(redactUrlPasswordMatcher({ replacement }))(value)
|
|
}
|
|
|
|
module.exports = {
|
|
asterisk,
|
|
escapeRegExp,
|
|
urlEncodeRegexGroup,
|
|
urlEncodeRegexTag,
|
|
redactUrlHostnameMatcher,
|
|
redactUrlSearchParamsMatcher,
|
|
redactUrlPasswordMatcher,
|
|
redactUrlMatcher,
|
|
redactUrlReplacement,
|
|
redactDynamicReplacement,
|
|
redactFixedReplacement,
|
|
redactMatchers,
|
|
redactUrlPassword,
|
|
}
|