Skip to content

Commit 630f470

Browse files
committed
feat(davinci-client): support Solution 3 root-level passwordPolicy fallback
Component-level policy takes precedence; falls back to root-level passwordPolicy for backward compatibility during the migration period. - Add passwordPolicy? to DaVinciNextResponse - Thread root-level policy through node.slice → reducer → factory - returnPasswordVerifyCollector prefers field.passwordPolicy ?? rootPasswordPolicy - Add tests for fallback, precedence, and no-policy cases - Restore root-level passwordPolicy in mock data (Solution 3 shape)
1 parent 244dbf4 commit 630f470

9 files changed

Lines changed: 3891 additions & 4801 deletions

File tree

packages/davinci-client/api-report/davinci-client.api.md

Lines changed: 1877 additions & 2400 deletions
Large diffs are not rendered by default.

packages/davinci-client/api-report/davinci-client.types.api.md

Lines changed: 1874 additions & 2397 deletions
Large diffs are not rendered by default.

packages/davinci-client/src/lib/collector.utils.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1212,6 +1212,50 @@ describe('returnPasswordVerifyCollector', () => {
12121212
expect(result.output).not.toHaveProperty('passwordPolicy');
12131213
});
12141214

1215+
it('should fall back to root-level passwordPolicy when field has none', () => {
1216+
const field: PasswordVerifyField = {
1217+
type: 'PASSWORD_VERIFY',
1218+
key: 'user.password',
1219+
label: 'Password',
1220+
};
1221+
const rootPolicy = { id: 'root-policy', name: 'Root', length: { min: 6, max: 128 } };
1222+
1223+
const result = returnPasswordVerifyCollector(field, 0, rootPolicy);
1224+
1225+
expect(result.output.passwordPolicy).toEqual(rootPolicy);
1226+
});
1227+
1228+
it('should prefer component-level policy over root-level', () => {
1229+
const componentPolicy = {
1230+
id: 'component-policy',
1231+
name: 'Component',
1232+
length: { min: 8, max: 255 },
1233+
};
1234+
const rootPolicy = { id: 'root-policy', name: 'Root', length: { min: 6, max: 128 } };
1235+
const field: PasswordVerifyField = {
1236+
type: 'PASSWORD_VERIFY',
1237+
key: 'user.password',
1238+
label: 'Password',
1239+
passwordPolicy: componentPolicy,
1240+
};
1241+
1242+
const result = returnPasswordVerifyCollector(field, 0, rootPolicy);
1243+
1244+
expect(result.output.passwordPolicy).toEqual(componentPolicy);
1245+
});
1246+
1247+
it('should have no passwordPolicy when neither field nor root provides one', () => {
1248+
const field: PasswordVerifyField = {
1249+
type: 'PASSWORD_VERIFY',
1250+
key: 'user.password',
1251+
label: 'Password',
1252+
};
1253+
1254+
const result = returnPasswordVerifyCollector(field, 0, undefined);
1255+
1256+
expect(result.output).not.toHaveProperty('passwordPolicy');
1257+
});
1258+
12151259
it('should record errors when field is missing properties', () => {
12161260
const invalidField = {} as PasswordVerifyField;
12171261
const result = returnPasswordVerifyCollector(invalidField, 0);

packages/davinci-client/src/lib/collector.utils.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import type {
3838
FidoAuthenticationField,
3939
FidoRegistrationField,
4040
MultiSelectField,
41+
PasswordPolicy,
4142
PasswordVerifyField,
4243
PhoneNumberField,
4344
ProtectField,
@@ -443,13 +444,16 @@ export function returnPasswordCollector(field: StandardField, idx: number) {
443444

444445
/**
445446
* @function returnPasswordVerifyCollector - Creates a PasswordVerifyCollector with optional password policy.
447+
* Component-level policy takes precedence over root-level (Solution 3: temporary duplication).
446448
* @param {PasswordVerifyField} field - The PASSWORD_VERIFY field, possibly containing passwordPolicy.
447449
* @param {number} idx - The index of the field in the form.
450+
* @param {PasswordPolicy} [rootPasswordPolicy] - Optional root-level password policy for backward compatibility.
448451
* @returns {PasswordVerifyCollector} The constructed PasswordVerifyCollector object.
449452
*/
450453
export function returnPasswordVerifyCollector(
451454
field: PasswordVerifyField,
452455
idx: number,
456+
rootPasswordPolicy?: PasswordPolicy,
453457
): PasswordVerifyCollector {
454458
let error = '';
455459
if (!('key' in field)) {
@@ -462,6 +466,9 @@ export function returnPasswordVerifyCollector(
462466
error = `${error}Type is not found in the field object. `;
463467
}
464468

469+
// Component-level policy takes precedence over root-level
470+
const passwordPolicy = field.passwordPolicy ?? rootPasswordPolicy;
471+
465472
return {
466473
category: 'SingleValueCollector',
467474
error: error || null,
@@ -477,7 +484,7 @@ export function returnPasswordVerifyCollector(
477484
key: field.key,
478485
label: field.label,
479486
type: field.type,
480-
...(field.passwordPolicy && { passwordPolicy: field.passwordPolicy }),
487+
...(passwordPolicy && { passwordPolicy }),
481488
},
482489
};
483490
}

packages/davinci-client/src/lib/davinci.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,7 @@ export interface DaVinciNextResponse extends DaVinciBaseResponse {
296296
fields?: DaVinciField[];
297297
};
298298
};
299+
passwordPolicy?: PasswordPolicy;
299300
}
300301

301302
/**

packages/davinci-client/src/lib/mock-data/mock-form-fields.data.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,43 @@ export const obj = {
206206
region: 'CA',
207207
themeId: 'activeTheme',
208208
formId: 'f0cf83ab-f8f4-4f4a-9260-8f7d27061fa7',
209+
passwordPolicy: {
210+
id: '39cad7af-3c2f-4672-9c3f-c47e5169e582',
211+
environment: {
212+
id: '02fb4743-189a-4bc7-9d6c-a919edfe6447',
213+
},
214+
name: 'Standard',
215+
description: 'A standard policy that incorporates industry best practices',
216+
excludesProfileData: true,
217+
notSimilarToCurrent: true,
218+
excludesCommonlyUsed: true,
219+
maxAgeDays: 182,
220+
minAgeDays: 1,
221+
maxRepeatedCharacters: 2,
222+
minUniqueCharacters: 5,
223+
history: {
224+
count: 6,
225+
retentionDays: 365,
226+
},
227+
lockout: {
228+
failureCount: 5,
229+
durationSeconds: 900,
230+
},
231+
length: {
232+
min: 8,
233+
max: 255,
234+
},
235+
minCharacters: {
236+
'~!@#$%^&*()-_=+[]{}|;:,.<>/?': 1,
237+
'0123456789': 1,
238+
ABCDEFGHIJKLMNOPQRSTUVWXYZ: 1,
239+
abcdefghijklmnopqrstuvwxyz: 1,
240+
},
241+
populationCount: 1,
242+
createdAt: '2024-01-03T19:50:39.586Z',
243+
updatedAt: '2024-01-03T19:50:39.586Z',
244+
default: true,
245+
},
209246
isResponseCompatibleWithMobileAndWebSdks: true,
210247
fieldTypes: [
211248
'LABEL',

packages/davinci-client/src/lib/node.reducer.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1263,6 +1263,51 @@ describe('PASSWORD_VERIFY with password policy', () => {
12631263
expect(result[0].output).not.toHaveProperty('passwordPolicy');
12641264
});
12651265

1266+
it('should fall back to root-level passwordPolicy when field has no policy', () => {
1267+
const action = {
1268+
type: 'node/next',
1269+
payload: {
1270+
fields: [
1271+
{
1272+
type: 'PASSWORD_VERIFY',
1273+
key: 'user.password',
1274+
label: 'Password',
1275+
},
1276+
],
1277+
formData: {},
1278+
passwordPolicy: mockPasswordPolicy,
1279+
},
1280+
};
1281+
const result = nodeCollectorReducer(undefined, action);
1282+
expect(result[0].type).toBe('PasswordVerifyCollector');
1283+
expect((result[0] as PasswordVerifyCollector).output.passwordPolicy).toEqual(
1284+
mockPasswordPolicy,
1285+
);
1286+
});
1287+
1288+
it('should prefer component-level policy over root-level', () => {
1289+
const rootPolicy = { id: 'root-policy', name: 'Root', length: { min: 6, max: 128 } };
1290+
const action = {
1291+
type: 'node/next',
1292+
payload: {
1293+
fields: [
1294+
{
1295+
type: 'PASSWORD_VERIFY',
1296+
key: 'user.password',
1297+
label: 'Password',
1298+
passwordPolicy: mockPasswordPolicy,
1299+
},
1300+
],
1301+
formData: {},
1302+
passwordPolicy: rootPolicy,
1303+
},
1304+
};
1305+
const result = nodeCollectorReducer(undefined, action);
1306+
expect((result[0] as PasswordVerifyCollector).output.passwordPolicy).toEqual(
1307+
mockPasswordPolicy,
1308+
);
1309+
});
1310+
12661311
it('should still produce PasswordCollector for PASSWORD type (no regression)', () => {
12671312
const action = {
12681313
type: 'node/next',

packages/davinci-client/src/lib/node.reducer.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {
3232
returnFidoAuthenticationCollector,
3333
returnQrCodeCollector,
3434
} from './collector.utils.js';
35-
import type { DaVinciField, UnknownField } from './davinci.types.js';
35+
import type { DaVinciField, PasswordPolicy, UnknownField } from './davinci.types.js';
3636
import type {
3737
ActionCollector,
3838
MultiSelectCollector,
@@ -70,6 +70,7 @@ import type {
7070
export const nextCollectorValues = createAction<{
7171
fields: DaVinciField[];
7272
formData: { value: Record<string, unknown> };
73+
passwordPolicy?: PasswordPolicy;
7374
}>('node/next');
7475
export const updateCollectorValues = createAction<{
7576
id: string;
@@ -174,8 +175,8 @@ export const nodeCollectorReducer = createReducer(initialCollectorValues, (build
174175
return returnPasswordCollector(field, idx);
175176
}
176177
case 'PASSWORD_VERIFY': {
177-
// No data to send; policy may be embedded in field
178-
return returnPasswordVerifyCollector(field, idx);
178+
// No data to send; policy may be embedded in field or at root level
179+
return returnPasswordVerifyCollector(field, idx, action.payload.passwordPolicy);
179180
}
180181
case 'PHONE_NUMBER': {
181182
const prefillData = data as PhoneNumberOutputValue;

packages/davinci-client/src/lib/node.slice.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ export const nodeSlice = createSlice({
185185
payload: {
186186
fields: action.payload.data?.form?.components?.fields,
187187
formData: action.payload.data?.formData,
188+
passwordPolicy: action.payload.data?.passwordPolicy,
188189
},
189190
});
190191

0 commit comments

Comments
 (0)