@jahia/cypress 8.0.0 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +65 -0
  3. package/dist/injections/bash-data.d.ts +1 -0
  4. package/dist/injections/bash-data.js +57 -0
  5. package/dist/injections/chars-data.d.ts +1 -0
  6. package/dist/injections/chars-data.js +25 -0
  7. package/dist/injections/htmlentities-data.d.ts +1 -0
  8. package/dist/injections/htmlentities-data.js +22 -0
  9. package/dist/injections/numbers-data.d.ts +1 -0
  10. package/dist/injections/numbers-data.js +66 -0
  11. package/dist/injections/sql-data.d.ts +1 -0
  12. package/dist/injections/sql-data.js +82 -0
  13. package/dist/injections/xss-data.d.ts +1 -0
  14. package/dist/injections/xss-data.js +740 -0
  15. package/dist/support/apollo/apollo.d.ts +2 -0
  16. package/dist/support/apollo/apollo.js +77 -15
  17. package/dist/support/browserHelper.d.ts +10 -0
  18. package/dist/support/browserHelper.js +167 -0
  19. package/dist/support/index.d.ts +3 -0
  20. package/dist/support/index.js +3 -0
  21. package/dist/support/jfaker.d.ts +60 -0
  22. package/dist/support/jfaker.js +241 -0
  23. package/dist/support/modSince.d.ts +52 -0
  24. package/dist/support/modSince.js +180 -0
  25. package/dist/support/provisioning/executeGroovy.js +41 -2
  26. package/dist/support/provisioning/runProvisioningScript.d.ts +1 -1
  27. package/dist/support/provisioning/runProvisioningScript.js +84 -7
  28. package/dist/support/registerSupport.js +34 -0
  29. package/docs/browser-helper.md +158 -0
  30. package/docs/jfaker.md +450 -0
  31. package/package.json +3 -1
  32. package/src/injections/bash-data.ts +54 -0
  33. package/src/injections/chars-data.ts +22 -0
  34. package/src/injections/htmlentities-data.ts +19 -0
  35. package/src/injections/numbers-data.ts +63 -0
  36. package/src/injections/sql-data.ts +79 -0
  37. package/src/injections/xss-data.ts +737 -0
  38. package/src/support/apollo/apollo.ts +74 -11
  39. package/src/support/browserHelper.ts +186 -0
  40. package/src/support/index.ts +3 -0
  41. package/src/support/jfaker.ts +245 -0
  42. package/src/support/modSince.ts +222 -0
  43. package/src/support/provisioning/executeGroovy.md +7 -1
  44. package/src/support/provisioning/executeGroovy.ts +46 -2
  45. package/src/support/provisioning/runProvisioningScript.ts +89 -12
  46. package/src/support/registerSupport.ts +29 -0
  47. package/tests/cypress/e2e/jfaker.spec.ts +411 -0
  48. package/tests/cypress/e2e/modSince.spec.ts +306 -0
  49. package/tests/cypress.config.ts +23 -0
  50. package/tests/package.json +41 -0
  51. package/tests/reporter-config.json +13 -0
  52. package/tests/yarn.lock +8578 -0
  53. package/tsconfig.json +3 -0
@@ -4,7 +4,9 @@
4
4
  /// <reference types="cypress" />
5
5
 
6
6
  import {ApolloClient, ApolloQueryResult, FetchResult, MutationOptions, QueryOptions} from '@apollo/client/core';
7
+ import {DocumentNode} from '@apollo/client/core';
7
8
  import gql from 'graphql-tag';
9
+ import {FieldNode, getOperationAST, print} from 'graphql';
8
10
 
9
11
  declare global {
10
12
  namespace Cypress {
@@ -15,8 +17,8 @@ declare global {
15
17
  }
16
18
  }
17
19
 
18
- export type FileQueryOptions = Partial<QueryOptions> & { queryFile?: string }
19
- export type FileMutationOptions = Partial<MutationOptions> & { mutationFile?: string }
20
+ export type FileQueryOptions = Partial<QueryOptions> & { queryFile?: string; sourcePackage?: string }
21
+ export type FileMutationOptions = Partial<MutationOptions> & { mutationFile?: string; sourcePackage?: string }
20
22
  export type ApolloOptions = (QueryOptions | MutationOptions | FileQueryOptions | FileMutationOptions) & Partial<Cypress.Loggable>;
21
23
 
22
24
  function isQuery(options: ApolloOptions): options is QueryOptions {
@@ -31,51 +33,112 @@ function isMutationFile(options: ApolloOptions): options is FileMutationOptions
31
33
  return (<FileMutationOptions>options).mutationFile !== undefined;
32
34
  }
33
35
 
36
+ function getOperationLabel(doc: DocumentNode, opType: 'Query' | 'Mutation'): string {
37
+ const opDef = getOperationAST(doc);
38
+ if (opDef?.name?.value) {
39
+ return `[${opType}] ${opDef.name.value}`;
40
+ }
41
+
42
+ // Anonymous operation: traverse up to 2 selection levels for a meaningful label
43
+ const firstSel = opDef?.selectionSet?.selections?.[0];
44
+ if (firstSel?.kind === 'Field') {
45
+ const firstName = (firstSel as FieldNode).name.value;
46
+ const secondSel = (firstSel as FieldNode).selectionSet?.selections?.[0];
47
+ if (secondSel?.kind === 'Field') {
48
+ return `[${opType}] ${firstName} › ${(secondSel as FieldNode).name.value}`;
49
+ }
50
+
51
+ return `[${opType}] ${firstName}`;
52
+ }
53
+
54
+ return `[${opType}]`;
55
+ }
56
+
57
+ function getQueryBody(doc: DocumentNode): string {
58
+ return doc?.loc?.source?.body ?? print(doc);
59
+ }
60
+
34
61
  // eslint-disable-next-line default-param-last, @typescript-eslint/no-shadow
35
62
  export const apollo = function (apollo: ApolloClient<any> = this.currentApolloClient, options: ApolloOptions): void {
36
63
  let result : ApolloQueryResult<any> | FetchResult;
37
64
  let logger : Cypress.Log;
65
+ let duration: number;
38
66
  const optionsWithDefaultCache: ApolloOptions = {fetchPolicy: 'no-cache', ...options};
39
67
 
40
68
  if (!apollo) {
41
69
  cy.apolloClient().apollo(optionsWithDefaultCache);
42
70
  } else if (isQueryFile(optionsWithDefaultCache)) {
43
- const {queryFile, ...apolloOptions} = optionsWithDefaultCache;
71
+ const {queryFile, sourcePackage, ...apolloOptions} = optionsWithDefaultCache as FileQueryOptions & Partial<Cypress.Loggable>;
44
72
  cy.fixture(queryFile).then(content => {
45
- cy.apollo({query: gql(content), ...apolloOptions});
73
+ const fileLabel = sourcePackage ? `${queryFile} @ ${sourcePackage}` : queryFile;
74
+ cy.apollo({query: gql(content), ...apolloOptions, _sourceFile: fileLabel} as ApolloOptions);
46
75
  });
47
76
  } else if (isMutationFile(optionsWithDefaultCache)) {
48
- const {mutationFile, ...apolloOptions} = optionsWithDefaultCache;
77
+ const {mutationFile, sourcePackage, ...apolloOptions} = optionsWithDefaultCache as FileMutationOptions & Partial<Cypress.Loggable>;
49
78
  cy.fixture(mutationFile).then(content => {
50
- cy.apollo({mutation: gql(content), ...apolloOptions});
79
+ const fileLabel = sourcePackage ? `${mutationFile} @ ${sourcePackage}` : mutationFile;
80
+ cy.apollo({mutation: gql(content), ...apolloOptions, _sourceFile: fileLabel} as ApolloOptions);
51
81
  });
52
82
  } else {
53
83
  const {log = true, ...apolloOptions} = optionsWithDefaultCache;
84
+
85
+ const doc = isQuery(apolloOptions) ?
86
+ (apolloOptions as QueryOptions).query :
87
+ (apolloOptions as MutationOptions).mutation;
88
+ const opType = isQuery(apolloOptions) ? 'Query' : 'Mutation';
89
+ const operationLabel = getOperationLabel(doc, opType);
90
+ const queryBody = getQueryBody(doc);
91
+ const variables = (apolloOptions as any).variables;
92
+ const sourceLabel = (optionsWithDefaultCache as any)._sourceFile ? ` (${(optionsWithDefaultCache as any)._sourceFile})` : '';
93
+ const variablesLabel = variables && Object.keys(variables).length > 0 ?
94
+ ` — ${JSON.stringify(variables)}` :
95
+ '';
96
+
54
97
  if (log) {
55
98
  logger = Cypress.log({
56
99
  autoEnd: false,
57
100
  name: 'apollo',
58
101
  displayName: 'apollo',
59
- message: isQuery(apolloOptions) ? `Execute Graphql Query: ${apolloOptions.query.loc.source.body}` : `Execute Graphql Mutation: ${apolloOptions.mutation.loc.source.body}`,
102
+ message: `${operationLabel}${sourceLabel}${variablesLabel}`,
60
103
  consoleProps: () => {
104
+ const errors = (result as any)?.errors ?? (result as any)?.graphQLErrors ?? null;
105
+ const isCaughtError = result instanceof Error;
106
+ const hasErrors = (errors?.length > 0) || isCaughtError;
61
107
  return {
62
- Options: apolloOptions,
108
+ Operation: operationLabel,
109
+ Variables: variables ?? {},
110
+ [`${opType} Body`]: queryBody,
111
+ Duration: duration === undefined ? 'pending' : `${duration}ms`,
112
+ Status: hasErrors ?
113
+ `error${isCaughtError ? `: ${(result as unknown as Error).message}` : ''}` :
114
+ 'success',
115
+ Data: (result as any)?.data ?? null,
116
+ Errors: errors,
63
117
  Yielded: result
64
118
  };
65
119
  }
66
120
  });
67
121
  }
68
122
 
69
- cy.wrap({}, {log: true})
123
+ const startTime = Date.now();
124
+ cy.wrap({}, {log: false})
70
125
  .then(() => (isQuery(optionsWithDefaultCache) ? apollo.query(optionsWithDefaultCache).catch(error => {
71
- cy.log(`Caught Graphql Query Error: ${JSON.stringify(error)}`);
126
+ cy.log(`Caught GraphQL query error: ${(error as any)?.message ?? JSON.stringify(error)}`);
72
127
  return error;
73
128
  }) : apollo.mutate(optionsWithDefaultCache).catch(error => {
74
- cy.log(`Caught Graphql Mutation Error: ${JSON.stringify(error)}`);
129
+ cy.log(`Caught GraphQL mutation error: ${(error as any)?.message ?? JSON.stringify(error)}`);
75
130
  return error;
76
131
  }))
77
132
  .then(r => {
78
133
  result = r;
134
+ duration = Date.now() - startTime;
135
+ if (logger) {
136
+ const errors = (r as any)?.errors ?? (r as any)?.graphQLErrors;
137
+ const hasErrors = (r instanceof Error) || (errors?.length > 0);
138
+ const prefix = hasErrors ? '❌ ' : '✅ ';
139
+ logger.set('message', `${prefix}${operationLabel}${sourceLabel}${variablesLabel}`);
140
+ }
141
+
79
142
  logger?.end();
80
143
  return r;
81
144
  })
@@ -0,0 +1,186 @@
1
+ /*
2
+ * Contains helpers for printing or clearing browser's storage and cookies.
3
+ * These are intended for interactive debugging and log full values by design.
4
+ * Use with caution in automated tests to avoid exposing sensitive data in logs.
5
+ */
6
+
7
+ /**
8
+ * Prints cookie details in a structured format for debugging.
9
+ *
10
+ * Note: This helper logs the full cookie value by design.
11
+ * @param {Cypress.Cookie} cookie Cookie object returned by Cypress.
12
+ * @returns {void}
13
+ * @private
14
+ */
15
+ const printCookieValues = (cookie: Cypress.Cookie): void => {
16
+ const cookieType = cookie.expiry ? 'Persistent' : 'Session';
17
+ const expiryDate = cookie.expiry ? new Date(cookie.expiry * 1000).toISOString() : 'Session only';
18
+ const daysUntilExpiry = cookie.expiry ? Math.round(((cookie.expiry * 1000) - Date.now()) / 1000 / 60 / 60 / 24) : null;
19
+
20
+ cy.log('-'.repeat(60));
21
+ cy.log(`Cookie: ${cookie.name}`);
22
+ cy.log('-'.repeat(60));
23
+ cy.log(`Type: ${cookieType}`);
24
+ cy.log(`Value: ${cookie.value}`);
25
+ cy.log(`Domain: ${cookie.domain}`);
26
+ cy.log(`Path: ${cookie.path}`);
27
+ cy.log(`Secure: ${cookie.secure ? '✔ Yes' : '✘ No'}`);
28
+ cy.log(`HttpOnly: ${cookie.httpOnly ? '✔ Yes' : '✘ No'}`);
29
+ cy.log(`SameSite: ${cookie.sameSite || '(not set)'}`);
30
+
31
+ if (cookie.expiry) {
32
+ cy.log(`Expires: ${expiryDate}`);
33
+ cy.log(`Days left: ${daysUntilExpiry} days`);
34
+ cy.log(`Unix time: ${cookie.expiry}`);
35
+ } else {
36
+ cy.log('Expires: When browser closes (session cookie)');
37
+ }
38
+ };
39
+
40
+ /**
41
+ * Clears cookies based on their persistence type.
42
+ * @param {'session'|'persistent'} [type='session'] Cookie category to clear.
43
+ * @returns {Cypress.Chainable<void>} Cypress chainable resolved when clearing is complete.
44
+ */
45
+ const clearCookiesByType = (type: 'session' | 'persistent' = 'session'): Cypress.Chainable<void> => {
46
+ return cy.getCookies().then(cookies => {
47
+ const cookiesToClear = cookies.filter(cookie => (type === 'session' ? !cookie.expiry : Boolean(cookie.expiry)));
48
+
49
+ cy.step(`🗑️ CLEAR ${cookiesToClear.length} ${type} cookie(s):`, () => {
50
+ cookiesToClear.forEach(cookie => {
51
+ const info = cookie.expiry ? `expires ${new Date(cookie.expiry * 1000).toISOString()}` : 'session only';
52
+ cy.log(` ... clearing ${cookie.name} (${info})`);
53
+ cy.clearCookie(cookie.name);
54
+ });
55
+ });
56
+ }).then(() => undefined);
57
+ };
58
+
59
+ /**
60
+ * Logs all available cookies with metadata and values.
61
+ *
62
+ * Intended for interactive debugging when full cookie visibility is needed.
63
+ * @returns {Cypress.Chainable<void>} Cypress chainable resolved when logging is complete.
64
+ */
65
+ const logCookies = (): Cypress.Chainable<void> => {
66
+ return cy.getCookies().then(cookies => {
67
+ if (cookies.length === 0) {
68
+ cy.log('No cookies found');
69
+ return;
70
+ }
71
+
72
+ cy.step(`COOKIES REPORT - Total: ${cookies.length}`, () => {
73
+ const sessionCookies = cookies.filter(c => !c.expiry);
74
+ const persistentCookies = cookies.filter(c => Boolean(c.expiry));
75
+
76
+ cy.log(`Session Cookies: ${sessionCookies.length}`);
77
+ cy.log(`Persistent Cookies: ${persistentCookies.length}`);
78
+
79
+ cookies.forEach(cookie => {
80
+ printCookieValues(cookie);
81
+ });
82
+ });
83
+ }).then(() => undefined);
84
+ };
85
+
86
+ /**
87
+ * Logs a specific cookie by name in a detailed format.
88
+ *
89
+ * Intended for interactive debugging when full cookie visibility is needed.
90
+ * @param {string} cookieName Name of the cookie to read and print.
91
+ * @returns {Cypress.Chainable<void>} Cypress chainable resolved when logging is complete.
92
+ */
93
+ const logCookie = (cookieName: string): Cypress.Chainable<void> => {
94
+ return cy.getCookie(cookieName).then(cookie => {
95
+ if (!cookie) {
96
+ cy.log(`Cookie "${cookieName}" not found`);
97
+ return;
98
+ }
99
+
100
+ printCookieValues(cookie);
101
+ }).then(() => undefined);
102
+ };
103
+
104
+ /**
105
+ * Clears Session cookies
106
+ * @returns {Cypress.Chainable<void>} Cypress chainable resolved when logging is complete.
107
+ */
108
+ const clearSessionCookies = (): Cypress.Chainable<void> => {
109
+ return clearCookiesByType('session').then(() => undefined);
110
+ };
111
+
112
+ /**
113
+ * Clears Persistent cookies
114
+ * @returns {Cypress.Chainable<void>} Cypress chainable resolved when logging is complete.
115
+ */
116
+ const clearPersistentCookies = (): Cypress.Chainable<void> => {
117
+ return clearCookiesByType('persistent').then(() => undefined);
118
+ };
119
+
120
+ /**
121
+ * Simulates a browser close by clearing session storage and cookies.
122
+ * Persistent cookies are kept intentionally.
123
+ * @returns {void}
124
+ */
125
+ const simulateClose = (): void => {
126
+ cy.log('Simulating browser close...');
127
+
128
+ // Clear session storage
129
+ cy.clearAllSessionStorage();
130
+
131
+ // Clear session cookies only
132
+ clearSessionCookies();
133
+
134
+ cy.log('Browser close simulated (session storage and cookies are cleared)');
135
+ };
136
+
137
+ /**
138
+ * Resets browser state by clearing all storages and all cookies.
139
+ * Use this when a test needs a fully clean client-side state.
140
+ * @returns {void}
141
+ */
142
+ const resetState = (): void => {
143
+ cy.log('Reset browser state...');
144
+
145
+ // Clear all storage
146
+ cy.clearAllLocalStorage();
147
+ cy.clearAllSessionStorage();
148
+
149
+ // Clear all cookies
150
+ cy.clearAllCookies();
151
+
152
+ cy.log('Browser reset is done (all storages and cookies cleared)');
153
+ };
154
+
155
+ /**
156
+ * Logs all sessionStorage entries grouped by origin.
157
+ * Intended for interactive debugging and logs full values.
158
+ * @returns {Cypress.Chainable<void>} Cypress chainable resolved when logging is complete.
159
+ */
160
+ const logSessionStorage = (): Cypress.Chainable<void> => {
161
+ return cy.getAllSessionStorage().then(session => {
162
+ cy.log(`sessionStorage: ${JSON.stringify(session)}`);
163
+ }).then(() => undefined);
164
+ };
165
+
166
+ /**
167
+ * Logs all localStorage entries grouped by origin.
168
+ * Intended for interactive debugging and logs full values.
169
+ * @returns {Cypress.Chainable<void>} Cypress chainable resolved when logging is complete.
170
+ */
171
+ const logLocalStorage = (): Cypress.Chainable<void> => {
172
+ return cy.getAllLocalStorage().then(local => {
173
+ cy.log(`localStorage: ${JSON.stringify(local)}`);
174
+ }).then(() => undefined);
175
+ };
176
+
177
+ export const BrowserHelper = {
178
+ logCookies,
179
+ logCookie,
180
+ logSessionStorage,
181
+ logLocalStorage,
182
+ clearSessionCookies,
183
+ clearPersistentCookies,
184
+ simulateClose,
185
+ resetState
186
+ };
@@ -5,3 +5,6 @@ export * from './logout';
5
5
  export * from './repeatUntil';
6
6
  export * from './testStep';
7
7
  export * from './jsErrorsLogger';
8
+ export * from './jfaker';
9
+ export * from './browserHelper';
10
+ export * from './modSince';
@@ -0,0 +1,245 @@
1
+ /* eslint-disable no-else-return */
2
+ /* eslint-disable @typescript-eslint/no-explicit-any */
3
+ /**
4
+ * jfaker - A flexible data generator for Cypress tests, supporting both faker.js generated data and custom injection payloads.
5
+ *
6
+ * This module provides a unified interface to generate either faker data or custom injection payloads based on the method called and global settings.
7
+ * It uses a dynamic Proxy to handle method calls and determine whether to generate faker data or injection data.
8
+ *
9
+ * IMPORTANT:
10
+ * When using the generated strings from jfaker in Cypress commands like `.type()`, make sure to:
11
+ * use `parseSpecialCharSequences: false`, e.g.: `<input>.type(text, {parseSpecialCharSequences: false})`
12
+ * to prevent Cypress from interpreting special characters in the generated strings (e.g., {, }, [, ], etc.) as commands,
13
+ * which is especially important for injection payloads that may contain such characters.
14
+ */
15
+
16
+ import {faker} from '@faker-js/faker';
17
+
18
+ // Import injection data from corresponding files in injections-ts directory
19
+ import {xssData} from '../injections/xss-data';
20
+ import {sqlData} from '../injections/sql-data';
21
+ import {bashData} from '../injections/bash-data';
22
+ import {charsData} from '../injections/chars-data';
23
+ import {htmlentitiesData} from '../injections/htmlentities-data';
24
+ import {numbersData} from '../injections/numbers-data';
25
+
26
+ const injectionData: Record<string, string[]> = {
27
+ xss: xssData,
28
+ sql: sqlData,
29
+ bash: bashData,
30
+ chars: charsData,
31
+ htmlentities: htmlentitiesData,
32
+ numbers: numbersData
33
+ };
34
+
35
+ // Environment variable key for storing injection type in Cypress env
36
+ // Can be set either using corresponding FakeData method or as an env var from CI/CD pipeline
37
+ const ENV_INJECTIONS_TYPE = 'JAHIA_CYPRESS_INJECTION_TYPE';
38
+
39
+ // Default range for random length of injection payloads; used when length is undefined
40
+ // Random items within the range will be picked and joined into a single string
41
+ const injectionsDefaultLength = {min: 2, max: 5};
42
+
43
+ /**
44
+ * Store FakeData type in Cypress env for persistence across specs
45
+ * @param {string} type FakeData type: 'faker' | 'xss' | 'sql' | 'bash' | 'chars' | 'htmlentities' | 'numbers'
46
+ * @returns void
47
+ */
48
+ function setDataType(type: string): void {
49
+ Cypress.env(ENV_INJECTIONS_TYPE, type);
50
+ }
51
+
52
+ /**
53
+ * Retrieve FakeData type from Cypress env (defaults to 'faker' if not set)
54
+ * @returns {string} Type of FakeData to use
55
+ */
56
+ function getDataType(): string | undefined {
57
+ return Cypress.env(ENV_INJECTIONS_TYPE) || 'faker';
58
+ }
59
+
60
+ /**
61
+ * Generate injection data based on the specified type and length
62
+ * @param {string} type Injection type to generate (xss, sql, bash, chars, htmlentities, numbers)
63
+ * @param {number} length Length of the generated injection (optional)
64
+ * @returns {string} Generated injection string
65
+ */
66
+ function generateInjection(type: string, length?: number): string {
67
+ let result: string[] = [];
68
+
69
+ // Type is specified and exists in injectionData, use it to generate data
70
+ const data = injectionData[type];
71
+ if (!data || data.length === 0) {
72
+ throw new Error(`[jFaker EXCEPTION] No injection data found for type: ${type}.`);
73
+ }
74
+
75
+ if (length === -1) {
76
+ // If length is -1, use all available items from the data array
77
+ result = data;
78
+ } else if (length && length > 0) {
79
+ // If length is specified and greater than 0, pick random items until the combined length meets the requirement
80
+ while (result.join('').length < length) {
81
+ const randomIndex = Math.floor(Math.random() * data.length);
82
+ result.push(data[randomIndex]);
83
+ }
84
+
85
+ // Trim the combined string to the specified length
86
+ const combined = result.join('');
87
+ result = [combined.substring(0, length)];
88
+ } else {
89
+ // If length is not specified, pick a random number of items within the default range
90
+ const itemsCount = Math.floor(Math.random() * injectionsDefaultLength.max) + injectionsDefaultLength.min;
91
+ for (let i = 0; i < itemsCount; i++) {
92
+ const randomIndex = Math.floor(Math.random() * data.length);
93
+ result.push(data[randomIndex]);
94
+ }
95
+ }
96
+
97
+ return result.join('');
98
+ }
99
+
100
+ /**
101
+ * Generate faker data based on the specified entity and options
102
+ * @param {string} entity Faker entity to generate (e.g., "person.firstName")
103
+ * @param {Record<string, unknown> | string | number | boolean | undefined} options Options to pass to the faker method (optional)
104
+ * @returns {string} Generated faker string
105
+ */
106
+ function generateFake(entity: string, options?: Record<string, unknown> | string | number | boolean | undefined): string {
107
+ let generator: () => string;
108
+ const result: string[] = [];
109
+ const parts: string[] = entity.split('.');
110
+ let fakerMethod: any = faker;
111
+
112
+ for (const part of parts) {
113
+ if (fakerMethod && typeof fakerMethod[part] !== 'undefined') {
114
+ fakerMethod = fakerMethod[part];
115
+ } else {
116
+ throw new Error(`[jFaker EXCEPTION] Invalid faker method: ${entity}`);
117
+ }
118
+ }
119
+
120
+ if (typeof fakerMethod === 'function') {
121
+ generator = (options && Object.keys(options).length > 0) ? () => fakerMethod(options) : () => fakerMethod();
122
+ } else {
123
+ throw new Error(`[jFaker EXCEPTION] ${entity} is not a function`);
124
+ }
125
+
126
+ result.push(generator());
127
+
128
+ return result.join('');
129
+ }
130
+
131
+ /**
132
+ * Escape string to prevent issues when used in contexts like HTML or JavaScript
133
+ * @param str
134
+ */
135
+ function escape(str: string): string {
136
+ return JSON.stringify(str).slice(1, -1);
137
+ }
138
+
139
+ /**
140
+ * Simple DeepApi class to create a dynamic nested Proxy for generating fake data using faker or injection data based on method calls
141
+ */
142
+ class DeepApi {
143
+ constructor(private handler: (path: string, args: any[]) => any) {
144
+ return this.createProxy([]);
145
+ }
146
+
147
+ private createProxy(path: string[]): any {
148
+ return new Proxy(
149
+ // The target is a function, so the proxy itself is callable
150
+ function () {},
151
+ {
152
+ // Handle property access - go deeper
153
+ get: (target, prop) => {
154
+ return this.createProxy([...path, String(prop)]);
155
+ },
156
+
157
+ // Handle function call - execute handler
158
+ apply: (target, thisArg, args) => {
159
+ return this.handler(path.join('.'), args);
160
+ }
161
+ }
162
+ );
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Interface to Fake Data generator (using DeepApi proxy to handle dynamic method calls)
168
+ * @param {Record<string, unknown>} options Options for data generation (length for injections, faker options, safe flag), \
169
+ * e.g.: `{length: 100}` for injections to specify desired length of the generated string, \
170
+ * or `{provider: 'example.com'}` for faker to pass options to the faker method. \
171
+ * For faker data generation, an additional option `safe` can be set to `true` \
172
+ * to force faker generation regardless of global type settings \
173
+ * (useful for specific cases where faker data is needed even when global type is set to injection).
174
+ * @remarks
175
+ * Available injection methods:
176
+ * - `.xss()` - Generate XSS injection payloads
177
+ * - `.sql()` - Generate SQL injection payloads
178
+ * - `.bash()` - Generate Bash injection payloads
179
+ * - `.chars()` - Generate random special characters
180
+ * - `.htmlentities()` - Generate HTML entities
181
+ * - `.numbers()` - Generate random numbers entities and edge cases
182
+ * - or any faker.js method can also be called (e.g., `person.firstName()`, `internet.email()`)
183
+ *
184
+ * @returns {string} Generated data string based on the method called and options provided
185
+ *
186
+ * @see https://fakerjs.dev/api/ for available faker methods and options
187
+ * @example
188
+ * ```typescript
189
+ *
190
+ * // Generate faker data with entity.
191
+ * const name = jfaker.person.firstName();
192
+ *
193
+ * // Entity will always be generated using faker (safe: true)
194
+ * const name = jfaker.person.firstName({safe: true});
195
+ *
196
+ * // Generate faker data with options.
197
+ * const email = jfaker.internet.email({provider: 'example.com'});
198
+ *
199
+ * // Generate injection payloads (random between min and max items joined into a single string)..
200
+ * // Entity will always be generated using 'xss'.
201
+ * const xssName = jfaker.xss();
202
+ *
203
+ * // Generate injection payloads with specific length.
204
+ * // Entity will always be generated using 'xss'.
205
+ * const xssName = jfaker.xss({length: 100});
206
+ *
207
+ * // Use all SQL injections.
208
+ * // Entity will always be generated using 'sql'.
209
+ * const allSql = jfaker.sql({length: -1});
210
+ * ```
211
+ */
212
+ const jfaker = new DeepApi((path, args) => {
213
+ // Handle non-faker methods first (escape, setFakeDataType, getFakeDataType)
214
+ switch (path) {
215
+ case 'escape':
216
+ return escape(args[0]);
217
+ case 'setDataType':
218
+ return setDataType(args[0]);
219
+ case 'getDataType':
220
+ return getDataType();
221
+ case Object.prototype.hasOwnProperty.call(injectionData, path) ? path : 'default':
222
+ // If the path corresponds to a valid injection type, generate data using that type,
223
+ // or fallback to default case which treats the path as a faker entity.
224
+ // Keep (safe === true) logic in "default" case for a readability.
225
+ return generateInjection(path, args[0]?.length);
226
+ default: {
227
+ // For faker data generation, check if the generation should be persistent based on global settings and options passed.
228
+ const safe = args[0]?.safe === true;
229
+ // Delete the 'safe' property from options to avoid passing it to faker methods
230
+ delete args[0]?.safe;
231
+
232
+ // Here path represents a faker entity.
233
+ // Check desired data type and 'safe' option to determine whether to generate faker data or injection data.
234
+ // If global type is set to injection (to override faker data) but safe is explicitly set to true in options -
235
+ // generate faker data for this specific call regardless of global settings.
236
+ if (getDataType() === 'faker' || safe) {
237
+ return generateFake(path, args[0]);
238
+ } else {
239
+ return generateInjection(getDataType(), args[0]?.length);
240
+ }
241
+ }
242
+ }
243
+ }) as any;
244
+
245
+ export {jfaker};