Skip to content

Commit 5baca32

Browse files
committed
audit checks: add I_I_InvalidAccessCodeFormat for Interview
This audit check validates the format of the access code of an interview. It is only validated if the access code is present. Some surveys may not implement access codes at all. The validateAccessCode function is defined in the survey project.
1 parent 45efd94 commit 5baca32

File tree

4 files changed

+138
-3
lines changed

4 files changed

+138
-3
lines changed

locales/en/audits.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"I_M_StartedAt": "Interview start time is missing",
44
"I_I_StartedAtBeforeSurveyStartDate": "Interview start time is before survey start date",
55
"I_I_StartedAtAfterSurveyEndDate": "Interview start time is after survey end date",
6+
"I_I_InvalidAccessCodeFormat": "Interview access code format is invalid",
67

78
"HH_I_Size": "Household size is out of range (should be between 1 and 20)",
89
"HH_M_Size": "Household size is missing",

locales/fr/audits.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"I_M_StartedAt": "La date de début de l'entrevue est manquante",
44
"I_I_StartedAtBeforeSurveyStartDate": "Le début de l'entrevue est avant la date de début de l'enquête",
55
"I_I_StartedAtAfterSurveyEndDate": "Le début de l'entrevue est après la date de fin de l'enquête",
6+
"I_I_InvalidAccessCodeFormat": "Le format du code d'accès de l'entrevue est invalide",
67

78
"HH_I_Size": "La taille du ménage est en dehors de la plage autorisée (devrait être entre 1 et 20)",
89
"HH_M_Size": "La taille du ménage est manquante",

packages/evolution-backend/src/services/audits/auditChecks/checks/InterviewAuditChecks.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
*/
77

88
import type { AuditForObject } from 'evolution-common/lib/services/audits/types';
9+
import { _isBlank } from 'chaire-lib-common/lib/utils/LodashExtensions';
910
import type { InterviewAuditCheckContext, InterviewAuditCheckFunction } from '../AuditCheckContexts';
1011
import projectConfig from 'evolution-common/lib/config/project.config';
1112
import { secondsToMillisecondsTimestamp, parseISODateToTimestamp } from 'evolution-common/lib/utils/DateTimeUtils';
13+
import { validateAccessCode } from '../../../accessCode';
1214

1315
export const interviewAuditChecks: { [errorCode: string]: InterviewAuditCheckFunction } = {
1416
/**
@@ -36,7 +38,7 @@ export const interviewAuditChecks: { [errorCode: string]: InterviewAuditCheckFun
3638
/**
3739
* Check if interview start date is missing or invalid
3840
* @param context - InterviewAuditCheckContext
39-
* @returns AuditForObject | undefined
41+
* @returns {AuditForObject | undefined}
4042
*/
4143
I_M_StartedAt: (context: InterviewAuditCheckContext): AuditForObject | undefined => {
4244
const { interview } = context;
@@ -62,7 +64,7 @@ export const interviewAuditChecks: { [errorCode: string]: InterviewAuditCheckFun
6264
* Check if interview started at timestamp is before the survey start date
6365
* Will be ignored if survey start date is not set.
6466
* @param context - InterviewAuditCheckContext
65-
* @returns AuditForObject | undefined
67+
* @returns {AuditForObject | undefined}
6668
*/
6769
I_I_StartedAtBeforeSurveyStartDate: (context: InterviewAuditCheckContext): AuditForObject | undefined => {
6870
const { interview } = context;
@@ -96,7 +98,7 @@ export const interviewAuditChecks: { [errorCode: string]: InterviewAuditCheckFun
9698
* Check if interview started at timestamp is after the survey end date
9799
* Will be ignored if survey end date is not set.
98100
* @param context - InterviewAuditCheckContext
99-
* @returns AuditForObject | undefined
101+
* @returns {AuditForObject | undefined}
100102
*/
101103
I_I_StartedAtAfterSurveyEndDate: (context: InterviewAuditCheckContext): AuditForObject | undefined => {
102104
const { interview } = context;
@@ -124,5 +126,37 @@ export const interviewAuditChecks: { [errorCode: string]: InterviewAuditCheckFun
124126
};
125127
}
126128
return undefined;
129+
},
130+
131+
/**
132+
* Check if interview access code format is invalid
133+
* Only validates the format if access code is present.
134+
* It does not verify that the access code is valid
135+
* (for instance it does not check if a letter has been sent with this access code)
136+
* Some surveys may not implement access codes at all.
137+
* The validateAccessCode function is defined in the survey project.
138+
* @param context - InterviewAuditCheckContext
139+
* @returns {AuditForObject | undefined}
140+
*/
141+
I_I_InvalidAccessCodeFormat: (context: InterviewAuditCheckContext): AuditForObject | undefined => {
142+
const { interview } = context;
143+
const accessCode = interview.accessCode;
144+
145+
// Only validate format if access code is present (some surveys don't use access codes)
146+
if (!_isBlank(accessCode)) {
147+
const isValid = validateAccessCode(accessCode as string);
148+
if (!isValid) {
149+
return {
150+
objectType: 'interview',
151+
objectUuid: interview.uuid!,
152+
errorCode: 'I_I_InvalidAccessCodeFormat',
153+
version: 1,
154+
level: 'error',
155+
message: 'Interview access code format is invalid',
156+
ignore: false
157+
};
158+
}
159+
}
160+
return undefined;
127161
}
128162
};
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright 2025, Polytechnique Montreal and contributors
3+
*
4+
* This file is licensed under the MIT License.
5+
* License text available at https://opensource.org/licenses/MIT
6+
*/
7+
8+
import { v4 as uuidV4 } from 'uuid';
9+
import type { InterviewAuditCheckContext } from '../../../AuditCheckContexts';
10+
import { createMockInterview } from './testHelper';
11+
import { interviewAuditChecks } from '../../InterviewAuditChecks';
12+
import { registerAccessCodeValidationFunction } from '../../../../../accessCode';
13+
14+
// Register a simple validation function for testing
15+
// Valid codes are 8 digits, optionally with a hyphen in the middle (e.g., "1234-5678" or "12345678")
16+
registerAccessCodeValidationFunction((accessCode: string) => {
17+
return /^\d{4}-?\d{4}$/.test(accessCode);
18+
});
19+
20+
describe('I_I_InvalidAccessCodeFormat audit check', () => {
21+
const validUuid = uuidV4();
22+
23+
describe('should pass in valid scenarios', () => {
24+
it.each([
25+
{
26+
description: 'interview has valid access code with hyphen',
27+
accessCode: '1234-5678'
28+
},
29+
{
30+
description: 'interview has valid access code without hyphen',
31+
accessCode: '12345678'
32+
},
33+
{
34+
description: 'interview has no access code (undefined)',
35+
accessCode: undefined
36+
},
37+
{
38+
description: 'interview has null access code',
39+
accessCode: null
40+
},
41+
{
42+
description: 'interview has empty string access code',
43+
accessCode: ''
44+
}
45+
])('$description', ({ accessCode }) => {
46+
const interview = createMockInterview({ accessCode: accessCode as string | undefined });
47+
const context: InterviewAuditCheckContext = { interview };
48+
49+
const result = interviewAuditChecks.I_I_InvalidAccessCodeFormat(context);
50+
51+
expect(result).toBeUndefined();
52+
});
53+
});
54+
55+
describe('should fail when access code is invalid', () => {
56+
it.each([
57+
{
58+
description: 'access code with letters',
59+
accessCode: 'invalid-code'
60+
},
61+
{
62+
description: 'access code too short',
63+
accessCode: '123'
64+
},
65+
{
66+
description: 'access code with special characters',
67+
accessCode: '!@#$%^&*()'
68+
},
69+
{
70+
description: 'access code too long',
71+
accessCode: '1234567890123456789012345678901234567890'
72+
},
73+
{
74+
description: 'access code with 7 digits only',
75+
accessCode: '1234567'
76+
},
77+
{
78+
description: 'access code with multiple hyphens',
79+
accessCode: '12-34-56-78'
80+
}
81+
])('$description', ({ accessCode }) => {
82+
const interview = createMockInterview({ accessCode }, validUuid);
83+
const context: InterviewAuditCheckContext = { interview };
84+
85+
const result = interviewAuditChecks.I_I_InvalidAccessCodeFormat(context);
86+
87+
expect(result).toEqual({
88+
objectType: 'interview',
89+
objectUuid: validUuid,
90+
errorCode: 'I_I_InvalidAccessCodeFormat',
91+
version: 1,
92+
level: 'error',
93+
message: 'Interview access code format is invalid',
94+
ignore: false
95+
});
96+
});
97+
});
98+
});
99+

0 commit comments

Comments
 (0)