@planning-inspectorate/data-model 2.40.0 → 2.41.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.
package/.env.example
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Appeals Load Test Script
|
|
2
|
+
|
|
3
|
+
This script is designed to allow load testing of service bus message consumers without the capacity of the sender being a limiting factor. The service bus client/strategy is hopefully resuable for other services in future
|
|
4
|
+
|
|
5
|
+
`load-test-appeals` sends a specified quantity of appeal case messages with service users and documents to service bus
|
|
6
|
+
|
|
7
|
+
This was written as a quick solution to provide ODW with the ability to run a load test without having to generate actual appeals within the Appeals Service, as generating them Appeals side proved too slow
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
- node
|
|
12
|
+
- Azure VPN
|
|
13
|
+
- Azure CLI interface
|
|
14
|
+
- Your user has the ability to send messages manually on the given service bus topic
|
|
15
|
+
- Unsure if this can be run in WSL currently as az login seems to fail
|
|
16
|
+
|
|
17
|
+
## Setup
|
|
18
|
+
|
|
19
|
+
`npm ci`
|
|
20
|
+
|
|
21
|
+
Create an .env file in the route of the project based on the example
|
|
22
|
+
|
|
23
|
+
- LOAD_TEST_SIMULATE_SENDING: (true|false) logs to console if true rather than sending, usful for checking data before sending
|
|
24
|
+
- LOAD_TEST_SERVICE_BUS_NAMESPACE: the fully qualified namespace of the service bus instance
|
|
25
|
+
- APPEALS_LOAD_TEST_CASE_TYPE: (HAS|S78) the case type topic to send on
|
|
26
|
+
- APPEALS_LOAD_TEST_CASE_COUNT: how many appeals to generate
|
|
27
|
+
- APPEALS_LOAD_TEST_DOCUMENTS_PER_CASE: how many documents to generate for each case
|
|
28
|
+
- APPEALS_LOAD_TEST_BATCH_SIZE: how many appeals are sent to service bus in one go, used to improve the speed in which messages can be sent to service bus
|
|
29
|
+
|
|
30
|
+
## Run
|
|
31
|
+
|
|
32
|
+
- set the env vars as needed, retrieve the correct Service Bus namespace from Azure
|
|
33
|
+
- connect to Azure VPN
|
|
34
|
+
- `az login` - login as yourself and connect to test env subscription
|
|
35
|
+
- `npm run load-test-appeals`
|
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import Ajv from 'ajv';
|
|
3
|
+
import addFormats from 'ajv-formats';
|
|
4
|
+
import { loadAllSchemasSync } from '../../src/index.js';
|
|
5
|
+
|
|
6
|
+
const HAS_SCHEMA = 'appeal-has.schema.json';
|
|
7
|
+
const S78_SCHEMA = 'appeal-s78.schema.json';
|
|
8
|
+
const DOCUMENT_SCHEMA = 'appeal-document.schema.json';
|
|
9
|
+
const SERVICE_USER_SCHEMA = 'service-user.schema.json';
|
|
10
|
+
|
|
11
|
+
const now = new Date().toISOString();
|
|
12
|
+
|
|
13
|
+
const BASE_HAS_CASE = {
|
|
14
|
+
caseId: null,
|
|
15
|
+
caseReference: 'mollit dolore dolor non culpa',
|
|
16
|
+
submissionId: null,
|
|
17
|
+
caseStatus: 'ready_to_start',
|
|
18
|
+
caseType: 'ZP',
|
|
19
|
+
caseProcedure: 'inquiry',
|
|
20
|
+
lpaCode: 'nulla',
|
|
21
|
+
caseOfficerId: null,
|
|
22
|
+
inspectorId: 'mollit in ullamco Excepteur commodo',
|
|
23
|
+
allocationLevel: 'D',
|
|
24
|
+
allocationBand: 1,
|
|
25
|
+
caseSpecialisms: ['et culpa elit labore', 'minim Lorem'],
|
|
26
|
+
caseSubmittedDate: now,
|
|
27
|
+
caseCreatedDate: now,
|
|
28
|
+
caseUpdatedDate: now,
|
|
29
|
+
caseValidDate: now,
|
|
30
|
+
caseValidationDate: null,
|
|
31
|
+
caseValidationOutcome: 'invalid',
|
|
32
|
+
caseValidationInvalidDetails: null,
|
|
33
|
+
caseValidationIncompleteDetails: [
|
|
34
|
+
'occaecat dolor labore ut ipsum',
|
|
35
|
+
'eiusmod ullamco pariatur aliqua esse',
|
|
36
|
+
'eiusmod Ut officia',
|
|
37
|
+
'aute',
|
|
38
|
+
'do minim sint labore aliquip'
|
|
39
|
+
],
|
|
40
|
+
caseExtensionDate: null,
|
|
41
|
+
caseStartedDate: null,
|
|
42
|
+
casePublishedDate: null,
|
|
43
|
+
linkedCaseStatus: null,
|
|
44
|
+
leadCaseReference: null,
|
|
45
|
+
lpaQuestionnaireDueDate: null,
|
|
46
|
+
lpaQuestionnaireSubmittedDate: null,
|
|
47
|
+
lpaQuestionnaireCreatedDate: now,
|
|
48
|
+
lpaQuestionnairePublishedDate: now,
|
|
49
|
+
lpaQuestionnaireValidationOutcome: 'incomplete',
|
|
50
|
+
lpaQuestionnaireValidationOutcomeDate: now,
|
|
51
|
+
lpaQuestionnaireValidationDetails: ['dolore cillum in anim adipisicing'],
|
|
52
|
+
lpaStatement: 'Ut ipsum et',
|
|
53
|
+
caseWithdrawnDate: null,
|
|
54
|
+
caseTransferredDate: null,
|
|
55
|
+
transferredCaseClosedDate: null,
|
|
56
|
+
caseDecisionOutcomeDate: null,
|
|
57
|
+
caseDecisionPublishedDate: null,
|
|
58
|
+
caseDecisionOutcome: null,
|
|
59
|
+
caseCompletedDate: null,
|
|
60
|
+
enforcementNotice: null,
|
|
61
|
+
applicationReference: 'eu',
|
|
62
|
+
applicationDate: now,
|
|
63
|
+
applicationDecision: 'refused',
|
|
64
|
+
applicationDecisionDate: now,
|
|
65
|
+
applicationDecisionDueDate: null,
|
|
66
|
+
caseSubmissionDueDate: now,
|
|
67
|
+
siteAddressLine1: 'ullamco',
|
|
68
|
+
siteAddressLine2: null,
|
|
69
|
+
siteAddressTown: 'dolor reprehenderit ut',
|
|
70
|
+
siteAddressCounty: 'dolore eu exercitation enim',
|
|
71
|
+
siteAddressPostcode: 'est',
|
|
72
|
+
siteAccessDetails: [
|
|
73
|
+
'Ut',
|
|
74
|
+
'eiusmod magna aliqua ipsum',
|
|
75
|
+
'sit non ut amet aliqua',
|
|
76
|
+
'id consectetur cupidatat',
|
|
77
|
+
'incididunt ut Lorem eu'
|
|
78
|
+
],
|
|
79
|
+
siteSafetyDetails: null,
|
|
80
|
+
siteAreaSquareMetres: 123,
|
|
81
|
+
floorSpaceSquareMetres: null,
|
|
82
|
+
isCorrectAppealType: null,
|
|
83
|
+
isGreenBelt: true,
|
|
84
|
+
inConservationArea: null,
|
|
85
|
+
affectsScheduledMonument: true,
|
|
86
|
+
hasProtectedSpecies: true,
|
|
87
|
+
isAonbNationalLandscape: null,
|
|
88
|
+
designatedSitesNames: null,
|
|
89
|
+
hasInfrastructureLevy: false,
|
|
90
|
+
isInfrastructureLevyFormallyAdopted: false,
|
|
91
|
+
infrastructureLevyAdoptedDate: null,
|
|
92
|
+
infrastructureLevyExpectedDate: null,
|
|
93
|
+
lpaProcedurePreference: null,
|
|
94
|
+
lpaProcedurePreferenceDetails: null,
|
|
95
|
+
lpaProcedurePreferenceDuration: 1,
|
|
96
|
+
caseworkReason: 'elit reprehenderit',
|
|
97
|
+
importantInformation: null,
|
|
98
|
+
jurisdiction: null,
|
|
99
|
+
redeterminedIndicator: null,
|
|
100
|
+
dateCostsReportDespatched: null,
|
|
101
|
+
dateNotRecoveredOrDerecovered: null,
|
|
102
|
+
dateRecovered: null,
|
|
103
|
+
originalCaseDecisionDate: null,
|
|
104
|
+
targetDate: null,
|
|
105
|
+
ownsAllLand: true,
|
|
106
|
+
ownsSomeLand: null,
|
|
107
|
+
knowsOtherOwners: 'Some',
|
|
108
|
+
knowsAllOwners: null,
|
|
109
|
+
advertisedAppeal: null,
|
|
110
|
+
notificationMethod: null,
|
|
111
|
+
ownersInformed: null,
|
|
112
|
+
originalDevelopmentDescription: null,
|
|
113
|
+
changedDevelopmentDescription: null,
|
|
114
|
+
newConditionDetails: 'tempor anim adipisicing',
|
|
115
|
+
nearbyCaseReferences: null,
|
|
116
|
+
neighbouringSiteAddresses: [
|
|
117
|
+
{
|
|
118
|
+
neighbouringSiteAddressLine1: 'eu veniam',
|
|
119
|
+
neighbouringSiteAddressLine2: null,
|
|
120
|
+
neighbouringSiteAddressTown: 'pariatur dolore',
|
|
121
|
+
neighbouringSiteAddressCounty: 'fugiat minim anim aliqua aliquip',
|
|
122
|
+
neighbouringSiteAddressPostcode: 'ex anim',
|
|
123
|
+
neighbouringSiteAccessDetails: null,
|
|
124
|
+
neighbouringSiteSafetyDetails: 'veniam in sunt'
|
|
125
|
+
}
|
|
126
|
+
],
|
|
127
|
+
reasonForNeighbourVisits: null,
|
|
128
|
+
affectedListedBuildingNumbers: ['Excepteur', 'in consequat sit', 'esse sunt velit culpa', 'voluptate esse'],
|
|
129
|
+
appellantCostsAppliedFor: null,
|
|
130
|
+
lpaCostsAppliedFor: null,
|
|
131
|
+
typeOfPlanningApplication: 'householder-planning',
|
|
132
|
+
siteGridReferenceEasting: '357144',
|
|
133
|
+
siteGridReferenceNorthing: '400534',
|
|
134
|
+
hasLandownersPermission: null,
|
|
135
|
+
isSiteInAreaOfSpecialControlAdverts: false,
|
|
136
|
+
wasApplicationRefusedDueToHighwayOrTraffic: null,
|
|
137
|
+
didAppellantSubmitCompletePhotosAndPlans: null,
|
|
138
|
+
advertDetails: [
|
|
139
|
+
{
|
|
140
|
+
isAdvertInPosition: false,
|
|
141
|
+
isSiteOnHighwayLand: false,
|
|
142
|
+
advertType: 'Other'
|
|
143
|
+
}
|
|
144
|
+
],
|
|
145
|
+
padsSapId: null
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const BASE_S78_CASE = {
|
|
149
|
+
caseId: 96989394,
|
|
150
|
+
caseReference: 'exercitation dolore',
|
|
151
|
+
submissionId: null,
|
|
152
|
+
caseStatus: 'transferred',
|
|
153
|
+
caseType: 'D',
|
|
154
|
+
caseProcedure: 'inquiry',
|
|
155
|
+
lpaCode: 'pariatur cillum non',
|
|
156
|
+
caseOfficerId: null,
|
|
157
|
+
inspectorId: 'amet magna dolor in labore',
|
|
158
|
+
allocationLevel: null,
|
|
159
|
+
allocationBand: null,
|
|
160
|
+
caseSpecialisms: ['laboris est proident minim', 'qui sunt ut', 'consectetur Lorem do sunt aliquip'],
|
|
161
|
+
caseSubmittedDate: now,
|
|
162
|
+
caseCreatedDate: now,
|
|
163
|
+
caseUpdatedDate: now,
|
|
164
|
+
caseValidDate: null,
|
|
165
|
+
caseValidationDate: null,
|
|
166
|
+
caseValidationOutcome: null,
|
|
167
|
+
caseValidationInvalidDetails: null,
|
|
168
|
+
caseValidationIncompleteDetails: null,
|
|
169
|
+
caseExtensionDate: null,
|
|
170
|
+
caseStartedDate: now,
|
|
171
|
+
casePublishedDate: null,
|
|
172
|
+
linkedCaseStatus: 'lead',
|
|
173
|
+
leadCaseReference: null,
|
|
174
|
+
lpaQuestionnaireDueDate: null,
|
|
175
|
+
lpaQuestionnaireSubmittedDate: null,
|
|
176
|
+
lpaQuestionnaireCreatedDate: null,
|
|
177
|
+
lpaQuestionnairePublishedDate: null,
|
|
178
|
+
lpaQuestionnaireValidationOutcome: null,
|
|
179
|
+
lpaQuestionnaireValidationOutcomeDate: null,
|
|
180
|
+
lpaQuestionnaireValidationDetails: ['cupidatat dolore in', 'officia in nostrud non', 'eu amet velit labore aute'],
|
|
181
|
+
lpaStatement: 'ipsum consectetur labore pariatur',
|
|
182
|
+
listOfDocumentsBeforeDecision: null,
|
|
183
|
+
caseWithdrawnDate: null,
|
|
184
|
+
caseTransferredDate: null,
|
|
185
|
+
transferredCaseClosedDate: null,
|
|
186
|
+
caseDecisionOutcomeDate: null,
|
|
187
|
+
caseDecisionPublishedDate: null,
|
|
188
|
+
caseDecisionOutcome: null,
|
|
189
|
+
caseCompletedDate: null,
|
|
190
|
+
enforcementNotice: null,
|
|
191
|
+
applicationReference: 'consequat ullamco in aute amet',
|
|
192
|
+
applicationDate: now,
|
|
193
|
+
applicationDecision: 'not_received',
|
|
194
|
+
applicationDecisionDate: null,
|
|
195
|
+
applicationDecisionDueDate: now,
|
|
196
|
+
caseSubmissionDueDate: null,
|
|
197
|
+
siteAddressLine1: 'dolore mollit',
|
|
198
|
+
siteAddressLine2: null,
|
|
199
|
+
siteAddressTown: 'in',
|
|
200
|
+
siteAddressCounty: 'ut incididunt velit in',
|
|
201
|
+
siteAddressPostcode: 'ut',
|
|
202
|
+
siteAccessDetails: ['ullamco', 'elit in', 'velit officia pariatur mollit', 'nostrud ex culpa'],
|
|
203
|
+
siteSafetyDetails: null,
|
|
204
|
+
siteAreaSquareMetres: 69381079.34849802,
|
|
205
|
+
floorSpaceSquareMetres: null,
|
|
206
|
+
isCorrectAppealType: null,
|
|
207
|
+
isGreenBelt: false,
|
|
208
|
+
inConservationArea: null,
|
|
209
|
+
ownsAllLand: null,
|
|
210
|
+
ownsSomeLand: null,
|
|
211
|
+
knowsOtherOwners: null,
|
|
212
|
+
knowsAllOwners: null,
|
|
213
|
+
advertisedAppeal: true,
|
|
214
|
+
notificationMethod: ['notice'],
|
|
215
|
+
ownersInformed: null,
|
|
216
|
+
originalDevelopmentDescription: 'est',
|
|
217
|
+
changedDevelopmentDescription: null,
|
|
218
|
+
newConditionDetails: null,
|
|
219
|
+
nearbyCaseReferences: ['veniam voluptate incididunt Excepteur'],
|
|
220
|
+
neighbouringSiteAddresses: [
|
|
221
|
+
{
|
|
222
|
+
neighbouringSiteAddressLine1: 'laborum',
|
|
223
|
+
neighbouringSiteAddressLine2: null,
|
|
224
|
+
neighbouringSiteAddressTown: 'aute ipsum dolore',
|
|
225
|
+
neighbouringSiteAddressCounty: null,
|
|
226
|
+
neighbouringSiteAddressPostcode: 'dolor ipsum anim fugiat',
|
|
227
|
+
neighbouringSiteAccessDetails: 'reprehenderit mollit Duis exercitation',
|
|
228
|
+
neighbouringSiteSafetyDetails: null
|
|
229
|
+
}
|
|
230
|
+
],
|
|
231
|
+
reasonForNeighbourVisits: null,
|
|
232
|
+
affectedListedBuildingNumbers: ['tempor adipisicing in'],
|
|
233
|
+
changedListedBuildingNumbers: null,
|
|
234
|
+
preserveGrantLoan: true,
|
|
235
|
+
consultHistoricEngland: false,
|
|
236
|
+
appellantCostsAppliedFor: null,
|
|
237
|
+
lpaCostsAppliedFor: null,
|
|
238
|
+
agriculturalHolding: null,
|
|
239
|
+
tenantAgriculturalHolding: null,
|
|
240
|
+
otherTenantsAgriculturalHolding: null,
|
|
241
|
+
informedTenantsAgriculturalHolding: true,
|
|
242
|
+
appellantProcedurePreference: 'hearing',
|
|
243
|
+
appellantProcedurePreferenceDetails: null,
|
|
244
|
+
appellantProcedurePreferenceDuration: null,
|
|
245
|
+
appellantProcedurePreferenceWitnessCount: null,
|
|
246
|
+
statusPlanningObligation: 'cillum proident fugiat ex quis',
|
|
247
|
+
affectsScheduledMonument: true,
|
|
248
|
+
hasProtectedSpecies: true,
|
|
249
|
+
isAonbNationalLandscape: null,
|
|
250
|
+
designatedSitesNames: null,
|
|
251
|
+
isGypsyOrTravellerSite: false,
|
|
252
|
+
isPublicRightOfWay: true,
|
|
253
|
+
eiaEnvironmentalImpactSchedule: 'schedule-1',
|
|
254
|
+
eiaDevelopmentDescription: 'agriculture-aquaculture',
|
|
255
|
+
eiaSensitiveAreaDetails: 'dolore non anim ut deserunt',
|
|
256
|
+
eiaColumnTwoThreshold: null,
|
|
257
|
+
eiaScreeningOpinion: true,
|
|
258
|
+
eiaRequiresEnvironmentalStatement: null,
|
|
259
|
+
eiaCompletedEnvironmentalStatement: null,
|
|
260
|
+
hasStatutoryConsultees: true,
|
|
261
|
+
consultedBodiesDetails: null,
|
|
262
|
+
hasInfrastructureLevy: null,
|
|
263
|
+
isInfrastructureLevyFormallyAdopted: null,
|
|
264
|
+
infrastructureLevyAdoptedDate: null,
|
|
265
|
+
infrastructureLevyExpectedDate: null,
|
|
266
|
+
lpaProcedurePreference: null,
|
|
267
|
+
lpaProcedurePreferenceDetails: null,
|
|
268
|
+
lpaProcedurePreferenceDuration: 12,
|
|
269
|
+
caseworkReason: 'sit ipsum',
|
|
270
|
+
importantInformation: null,
|
|
271
|
+
jurisdiction: null,
|
|
272
|
+
redeterminedIndicator: null,
|
|
273
|
+
dateCostsReportDespatched: null,
|
|
274
|
+
dateNotRecoveredOrDerecovered: null,
|
|
275
|
+
dateRecovered: null,
|
|
276
|
+
originalCaseDecisionDate: null,
|
|
277
|
+
targetDate: null,
|
|
278
|
+
appellantCommentsSubmittedDate: null,
|
|
279
|
+
appellantStatementSubmittedDate: null,
|
|
280
|
+
appellantProofsSubmittedDate: null,
|
|
281
|
+
finalCommentsDueDate: null,
|
|
282
|
+
interestedPartyRepsDueDate: null,
|
|
283
|
+
lpaCommentsSubmittedDate: null,
|
|
284
|
+
lpaProofsSubmittedDate: null,
|
|
285
|
+
lpaStatementSubmittedDate: null,
|
|
286
|
+
proofsOfEvidenceDueDate: null,
|
|
287
|
+
siteNoticesSentDate: null,
|
|
288
|
+
statementDueDate: null,
|
|
289
|
+
caseManagementConferenceDate: null,
|
|
290
|
+
numberOfResidencesNetChange: 59800248.893144846,
|
|
291
|
+
siteGridReferenceEasting: '357144',
|
|
292
|
+
siteGridReferenceNorthing: '400534',
|
|
293
|
+
siteViewableFromRoad: null,
|
|
294
|
+
siteWithinSSSI: null,
|
|
295
|
+
typeOfPlanningApplication: 'full-appeal',
|
|
296
|
+
developmentType: 'other-minor',
|
|
297
|
+
statementOfCommonGroundDueDate: null,
|
|
298
|
+
planningObligationDueDate: null,
|
|
299
|
+
advertDetails: [
|
|
300
|
+
{
|
|
301
|
+
isAdvertInPosition: false,
|
|
302
|
+
isSiteOnHighwayLand: false,
|
|
303
|
+
advertType: 'Other'
|
|
304
|
+
}
|
|
305
|
+
],
|
|
306
|
+
padsSapId: null,
|
|
307
|
+
applicationMadeUnderActSection: 'existing-development',
|
|
308
|
+
siteUseAtTimeOfApplication: 'Site use at time of application',
|
|
309
|
+
appealUnderActSection: 'proposed-use-of-a-development',
|
|
310
|
+
lpaConsiderAppealInvalid: true,
|
|
311
|
+
lpaAppealInvalidReasons: 'Reason(s) why LPA consider the appeal invalid'
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
function randomCaseReference(prefix, index) {
|
|
315
|
+
const suffix = String(index + 1).padStart(7, '0');
|
|
316
|
+
return `${prefix}${suffix}`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function randomHex(length) {
|
|
320
|
+
return crypto
|
|
321
|
+
.randomBytes(Math.ceil(length / 2))
|
|
322
|
+
.toString('hex')
|
|
323
|
+
.slice(0, length);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function createCase(caseType, index) {
|
|
327
|
+
const template = caseType === 'HAS' ? BASE_HAS_CASE : BASE_S78_CASE;
|
|
328
|
+
const entity = structuredClone(template);
|
|
329
|
+
const referencePrefix = caseType === 'HAS' ? 'HAS' : 'S78';
|
|
330
|
+
|
|
331
|
+
entity.caseId = 500000 + index;
|
|
332
|
+
entity.caseReference = randomCaseReference(referencePrefix, index);
|
|
333
|
+
entity.caseCreatedDate = now;
|
|
334
|
+
entity.caseUpdatedDate = now;
|
|
335
|
+
entity.caseSubmittedDate = now;
|
|
336
|
+
entity.applicationReference = `LOAD-${referencePrefix}-${index + 1}`;
|
|
337
|
+
entity.lpaCode = 'LOAD';
|
|
338
|
+
|
|
339
|
+
return entity;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function createServiceUser(casePayload, serviceUserType, index) {
|
|
343
|
+
const typeName = serviceUserType === 'Agent' ? 'Agent' : 'Appellant';
|
|
344
|
+
return {
|
|
345
|
+
id: crypto.randomUUID(),
|
|
346
|
+
salutation: 'Ms',
|
|
347
|
+
firstName: `Load-Test`,
|
|
348
|
+
lastName: `${typeName}${index + 1}`,
|
|
349
|
+
addressLine1: '1 Test Street',
|
|
350
|
+
addressLine2: null,
|
|
351
|
+
addressTown: 'Testville',
|
|
352
|
+
addressCounty: 'Testshire',
|
|
353
|
+
postcode: 'TE1 1ST',
|
|
354
|
+
addressCountry: 'United Kingdom',
|
|
355
|
+
organisation: 'Load Test Org',
|
|
356
|
+
organisationType: 'Private Company',
|
|
357
|
+
role: typeName,
|
|
358
|
+
telephoneNumber: '+447123456789',
|
|
359
|
+
otherPhoneNumber: null,
|
|
360
|
+
faxNumber: null,
|
|
361
|
+
emailAddress: `${typeName.toLowerCase()}${index + 1}@example.com`,
|
|
362
|
+
webAddress: 'https://example.com',
|
|
363
|
+
serviceUserType: typeName,
|
|
364
|
+
caseReference: casePayload.caseReference,
|
|
365
|
+
sourceSystem: 'back-office-appeals',
|
|
366
|
+
sourceSuid: crypto.randomUUID()
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function createDocument(casePayload, caseIndex, documentIndex) {
|
|
371
|
+
const documentId = crypto.randomUUID();
|
|
372
|
+
const filename = `LOAD-${casePayload.caseReference}-doc-${documentIndex + 1}.pdf`;
|
|
373
|
+
const caseType = typeof casePayload.caseType === 'string' ? casePayload.caseType : null;
|
|
374
|
+
const documentUri = `https://example.local/appeal/${casePayload.caseReference}/${documentId}/v1/${filename}`;
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
documentId,
|
|
378
|
+
caseId: casePayload.caseId,
|
|
379
|
+
caseReference: casePayload.caseReference,
|
|
380
|
+
version: 1,
|
|
381
|
+
filename,
|
|
382
|
+
originalFilename: filename,
|
|
383
|
+
size: 1000 + documentIndex,
|
|
384
|
+
mime: 'application/pdf',
|
|
385
|
+
documentURI: documentUri,
|
|
386
|
+
publishedDocumentURI: documentUri,
|
|
387
|
+
virusCheckStatus: 'scanned',
|
|
388
|
+
fileMD5: randomHex(32),
|
|
389
|
+
dateCreated: now,
|
|
390
|
+
dateReceived: now,
|
|
391
|
+
datePublished: now,
|
|
392
|
+
lastModified: now,
|
|
393
|
+
caseType,
|
|
394
|
+
redactedStatus: 'no_redaction_required',
|
|
395
|
+
documentType: 'plansDrawings',
|
|
396
|
+
sourceSystem: 'back-office-appeals',
|
|
397
|
+
origin: 'pins',
|
|
398
|
+
owner: 'Load Test Team',
|
|
399
|
+
author: 'Load Test Team',
|
|
400
|
+
description: `Load test document ${documentIndex + 1}`,
|
|
401
|
+
caseStage: 'appellant-case',
|
|
402
|
+
horizonFolderId: null
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function buildAjvValidator() {
|
|
407
|
+
const schemas = loadAllSchemasSync();
|
|
408
|
+
const flatSchemas = Object.values(schemas).reduce((acc, current) => ({ ...acc, ...current }), {});
|
|
409
|
+
const ajv = new Ajv({ schemas: flatSchemas, allErrors: true });
|
|
410
|
+
addFormats(ajv);
|
|
411
|
+
return ajv;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function validateOrThrow(ajv, schemaName, payload, label) {
|
|
415
|
+
const valid = ajv.validate(schemaName, payload);
|
|
416
|
+
if (!valid) {
|
|
417
|
+
const errors = ajv.errorsText(ajv.errors, { separator: '\n' });
|
|
418
|
+
throw new Error(`${label} failed schema ${schemaName}:\n${errors}`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* @param {'HAS'|'S78'} [caseType]
|
|
424
|
+
* @param {number} [documentsPerCase]
|
|
425
|
+
* @param {number} [caseIndex]
|
|
426
|
+
* @returns {{case: any, serviceUsers: any[], documents: any[]}}
|
|
427
|
+
*/
|
|
428
|
+
export const generateAppealsLoadTestMessages = (caseType = 'HAS', documentsPerCase = 2, caseIndex = 1) => {
|
|
429
|
+
const caseSchema = caseType === 'HAS' ? HAS_SCHEMA : S78_SCHEMA;
|
|
430
|
+
const ajv = buildAjvValidator();
|
|
431
|
+
|
|
432
|
+
const casePayload = createCase(caseType, caseIndex);
|
|
433
|
+
validateOrThrow(ajv, caseSchema, casePayload, `case[${caseIndex}]`);
|
|
434
|
+
|
|
435
|
+
const serviceUsers = [
|
|
436
|
+
createServiceUser(casePayload, 'Agent', caseIndex),
|
|
437
|
+
createServiceUser(casePayload, 'Appellant', caseIndex)
|
|
438
|
+
];
|
|
439
|
+
|
|
440
|
+
for (const serviceUser of serviceUsers) {
|
|
441
|
+
validateOrThrow(ajv, SERVICE_USER_SCHEMA, serviceUser, `${caseIndex} serviceUser[${serviceUser.role}]`);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const documents = [];
|
|
445
|
+
for (let docIndex = 0; docIndex < documentsPerCase; docIndex += 1) {
|
|
446
|
+
const document = createDocument(casePayload, caseIndex, docIndex);
|
|
447
|
+
validateOrThrow(ajv, DOCUMENT_SCHEMA, document, `document[${caseIndex}][${docIndex}]`);
|
|
448
|
+
documents.push(document);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return {
|
|
452
|
+
case: casePayload,
|
|
453
|
+
serviceUsers,
|
|
454
|
+
documents
|
|
455
|
+
};
|
|
456
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { generateAppealsLoadTestMessages } from './generate-appeals-messages.js';
|
|
2
|
+
import { ServiceBusEventClient } from '../service-bus-client.js';
|
|
3
|
+
import { MESSAGE_EVENT_TYPE } from '../../src/enums.js';
|
|
4
|
+
|
|
5
|
+
/** @type {'HAS'|'S78'} */ //@ts-ignore
|
|
6
|
+
const caseType = process.env.APPEALS_LOAD_TEST_CASE_TYPE || 'HAS'; // 'HAS' or 'S78'
|
|
7
|
+
const caseCount = parseInt(process.env.APPEALS_LOAD_TEST_CASE_COUNT || '1', 10);
|
|
8
|
+
const documentsPerCase = parseInt(process.env.APPEALS_LOAD_TEST_DOCUMENTS_PER_CASE || '2', 10);
|
|
9
|
+
const batchSize = parseInt(process.env.APPEALS_LOAD_TEST_BATCH_SIZE || '50', 10);
|
|
10
|
+
|
|
11
|
+
if (batchSize < 1) {
|
|
12
|
+
throw new Error('Batch size must be at least 1');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const caseTypeTopicMap = {
|
|
16
|
+
HAS: 'appeal-has',
|
|
17
|
+
S78: 'appeal-s78'
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
if (!caseTypeTopicMap[caseType]) {
|
|
21
|
+
throw new Error(`Unsupported case type: ${caseType}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const client = new ServiceBusEventClient();
|
|
25
|
+
|
|
26
|
+
let casesCreated = 0;
|
|
27
|
+
let documentsCreated = 0;
|
|
28
|
+
let serviceUsersCreated = 0;
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
for (let batchIndex = 0; batchIndex < caseCount; batchIndex += batchSize) {
|
|
32
|
+
const batch = [];
|
|
33
|
+
const batchEnd = Math.min(batchIndex + batchSize, caseCount);
|
|
34
|
+
|
|
35
|
+
for (let index = batchIndex; index < batchEnd; index += 1) {
|
|
36
|
+
batch.push(generateAppealsLoadTestMessages(caseType, documentsPerCase, index));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const batchCases = batch.map((m) => m.case);
|
|
40
|
+
const batchServiceUsers = batch.flatMap((m) => m.serviceUsers);
|
|
41
|
+
const batchDocuments = batch.flatMap((m) => m.documents);
|
|
42
|
+
|
|
43
|
+
await Promise.all([
|
|
44
|
+
client.sendEvents(caseTypeTopicMap[caseType], batchCases, MESSAGE_EVENT_TYPE.CREATE),
|
|
45
|
+
client.sendEvents('appeal-service-user', batchServiceUsers, MESSAGE_EVENT_TYPE.CREATE),
|
|
46
|
+
client.sendEvents('appeal-document', batchDocuments, MESSAGE_EVENT_TYPE.CREATE)
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
casesCreated += batch.length;
|
|
50
|
+
documentsCreated += batchDocuments.length;
|
|
51
|
+
serviceUsersCreated += batchServiceUsers.length;
|
|
52
|
+
}
|
|
53
|
+
} finally {
|
|
54
|
+
await client.close();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log('Finished');
|
|
58
|
+
console.log({ casesCreated, documentsCreated, serviceUsersCreated });
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { ServiceBusClient } from '@azure/service-bus';
|
|
2
|
+
import { DefaultAzureCredential } from '@azure/identity';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {object} ServiceBusEventClientOptions
|
|
6
|
+
* @property {string | undefined} [connectionString]
|
|
7
|
+
* @property {import('@azure/core-auth').TokenCredential | undefined} [credential]
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Event client used to broadcast messages to service bus topics.
|
|
12
|
+
* @class
|
|
13
|
+
*/
|
|
14
|
+
export class ServiceBusEventClient {
|
|
15
|
+
constructor() {
|
|
16
|
+
this.client = new ServiceBusClient(process.env.LOAD_TEST_SERVICE_BUS_NAMESPACE, new DefaultAzureCredential());
|
|
17
|
+
this.simulateSending = process.env.LOAD_TEST_SIMULATE_SENDING === 'true'; // true logs to console, false sends to service bus
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} topic
|
|
22
|
+
* @returns Instance of the service bus sender from the service bus client
|
|
23
|
+
*/
|
|
24
|
+
#createSender = (topic) => {
|
|
25
|
+
return this.client.createSender(topic);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Returns unix timestamp representing the trace id
|
|
30
|
+
* @returns {number}
|
|
31
|
+
*/
|
|
32
|
+
#createTraceId = () => {
|
|
33
|
+
return Date.now();
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {object[]} events
|
|
38
|
+
* @param {number} traceId
|
|
39
|
+
* @param {import('../utils/event-type').EventType} type
|
|
40
|
+
* @returns {import('@azure/service-bus').ServiceBusMessage []}
|
|
41
|
+
*/
|
|
42
|
+
#transformMessagesToSend = (events, traceId, type) => {
|
|
43
|
+
return events.map((body) => ({
|
|
44
|
+
body,
|
|
45
|
+
contentType: 'application/json',
|
|
46
|
+
applicationProperties: {
|
|
47
|
+
version: '0.1',
|
|
48
|
+
traceId,
|
|
49
|
+
type
|
|
50
|
+
}
|
|
51
|
+
}));
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {string} topic
|
|
56
|
+
* @param {any[]} events
|
|
57
|
+
* @param {import('../utils/event-type').EventType} eventType
|
|
58
|
+
*/
|
|
59
|
+
sendEvents = async (topic, events, eventType) => {
|
|
60
|
+
const sender = this.#createSender(topic);
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const traceId = this.#createTraceId();
|
|
64
|
+
|
|
65
|
+
console.log(`Publishing ${events.length} events to topic ${topic} with trace id ${traceId}`);
|
|
66
|
+
|
|
67
|
+
const messages = this.#transformMessagesToSend(events, traceId, eventType);
|
|
68
|
+
|
|
69
|
+
if (this.simulateSending) {
|
|
70
|
+
console.log(`Simulating sending messages: ${JSON.stringify(messages)}`);
|
|
71
|
+
} else {
|
|
72
|
+
await sender.sendMessages(messages);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return events;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error(`Error sending messages to topic ${topic}:`, error);
|
|
78
|
+
throw error;
|
|
79
|
+
} finally {
|
|
80
|
+
if (!sender.isClosed) {
|
|
81
|
+
await sender.close();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
close = async () => {
|
|
87
|
+
await this.client.close();
|
|
88
|
+
};
|
|
89
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planning-inspectorate/data-model",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.41.0",
|
|
4
4
|
"description": "JSON Schemas for the Planning Inspectorate's Data Model",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"gen-types": "node src/lib/gen-types.js",
|
|
17
17
|
"gen-cjs": "npx rollup ./src/index.js --file ./src/index.cjs --format cjs && npx rollup ./src/enums.js --file ./src/enums.cjs --format cjs",
|
|
18
18
|
"lint": "npx eslint .",
|
|
19
|
+
"load-test-appeals": "node --env-file=.env load-test/appeals/run-load-script.js",
|
|
19
20
|
"format": "prettier --write .",
|
|
20
21
|
"format-check": "prettier --check .",
|
|
21
22
|
"prepare": "node .husky/install.mjs"
|
|
@@ -30,6 +31,8 @@
|
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
32
33
|
"@adobe/jsonschema2md": "^8.0.3",
|
|
34
|
+
"@azure/identity": "^4.13.0",
|
|
35
|
+
"@azure/service-bus": "^7.9.1",
|
|
33
36
|
"@commitlint/cli": "^21.0.0",
|
|
34
37
|
"@commitlint/config-conventional": "^21.0.0",
|
|
35
38
|
"@eslint/js": "^10.0.1",
|