@dashevo/moderation-charters-contract 4.2.0-beta.4

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.
@@ -0,0 +1,465 @@
1
+ const crypto = require('crypto');
2
+
3
+ const {
4
+ DashPlatformProtocol,
5
+ JsonSchemaError,
6
+ } = require('@dashevo/wasm-dpp');
7
+ const generateRandomIdentifier = require('@dashevo/wasm-dpp/lib/test/utils/generateRandomIdentifierAsync');
8
+
9
+ const { expect } = require('chai');
10
+ const moderationChartersContractDocumentsSchema = require('../../schema/v1/moderation-charters-contract-documents.json');
11
+
12
+ const expectJsonSchemaError = (validationResult, errorCount = 1) => {
13
+ const errors = validationResult.getErrors();
14
+ expect(errors)
15
+ .to
16
+ .have
17
+ .length(errorCount);
18
+
19
+ const error = validationResult.getErrors()[0];
20
+ expect(error)
21
+ .to
22
+ .be
23
+ .instanceof(JsonSchemaError);
24
+
25
+ return error;
26
+ };
27
+
28
+ const randomIds = (count) => Array.from({ length: count }, () => crypto.randomBytes(32));
29
+
30
+ describe('Moderation Charters Contract', () => {
31
+ let dpp;
32
+ let dataContract;
33
+ let identityId;
34
+
35
+ beforeEach(async () => {
36
+ dpp = new DashPlatformProtocol(
37
+ { generate: () => crypto.randomBytes(32) },
38
+ );
39
+
40
+ identityId = await generateRandomIdentifier();
41
+
42
+ dataContract = dpp.dataContract.create(
43
+ identityId,
44
+ BigInt(1),
45
+ moderationChartersContractDocumentsSchema,
46
+ );
47
+ });
48
+
49
+ const validate = (type, raw) => {
50
+ const document = dpp.document.create(dataContract, identityId, type, raw);
51
+ return document.validate(dpp.protocolVersion);
52
+ };
53
+
54
+ const expectRequired = (type, rawFactory, properties) => {
55
+ properties.forEach((property) => {
56
+ it(`should require ${property}`, async () => {
57
+ const raw = await rawFactory();
58
+ delete raw[property];
59
+
60
+ const error = expectJsonSchemaError(validate(type, raw));
61
+
62
+ expect(error.keyword).to.equal('required');
63
+ expect(error.params.missingProperty).to.equal(property);
64
+ });
65
+ });
66
+ };
67
+
68
+ const expectNoAdditionalProperties = (type, rawFactory, extra) => {
69
+ it(`should not have additional properties such as ${extra}`, async () => {
70
+ const raw = await rawFactory();
71
+ raw[extra] = 42;
72
+
73
+ const error = expectJsonSchemaError(validate(type, raw));
74
+
75
+ expect(error.keyword).to.equal('additionalProperties');
76
+ expect(error.params.additionalProperties).to.deep.equal([extra]);
77
+ });
78
+ };
79
+
80
+ it('should have a valid contract definition', async () => {
81
+ expect(() => dpp.dataContract.create(
82
+ identityId,
83
+ BigInt(1),
84
+ moderationChartersContractDocumentsSchema,
85
+ ))
86
+ .to
87
+ .not
88
+ .throw();
89
+ });
90
+
91
+ it('should have seven document types', () => {
92
+ expect(Object.keys(moderationChartersContractDocumentsSchema).sort()).to.deep.equal([
93
+ 'addedModerator',
94
+ 'electedCharter',
95
+ 'joinRequest',
96
+ 'reason',
97
+ 'removedModerator',
98
+ 'resignationRequest',
99
+ 'submittedCharter',
100
+ ]);
101
+ });
102
+
103
+ describe('reason', () => {
104
+ const rawReason = async () => ({
105
+ code: 'SPM',
106
+ label: 'Spam',
107
+ description: 'Unsolicited promotion, repeated or automated.',
108
+ });
109
+
110
+ it('should be valid', async () => {
111
+ expect(validate('reason', await rawReason()).isValid()).to.be.true();
112
+ });
113
+
114
+ it('should not need a description', async () => {
115
+ const raw = await rawReason();
116
+ delete raw.description;
117
+
118
+ expect(validate('reason', raw).isValid()).to.be.true();
119
+ });
120
+
121
+ expectRequired('reason', rawReason, ['code', 'label']);
122
+ expectNoAdditionalProperties('reason', rawReason, 'severity');
123
+
124
+ ['spm', 'SP', 'SPAM', 'SP1'].forEach((code) => {
125
+ it(`should refuse the code ${code}`, async () => {
126
+ const raw = await rawReason();
127
+ raw.code = code;
128
+
129
+ expect(validate('reason', raw).isValid()).to.be.false();
130
+ });
131
+ });
132
+
133
+ it('should refuse a label over 64 characters', async () => {
134
+ const raw = await rawReason();
135
+ raw.label = 'a'.repeat(65);
136
+
137
+ const error = expectJsonSchemaError(validate('reason', raw));
138
+
139
+ expect(error.keyword).to.equal('maxLength');
140
+ });
141
+ });
142
+
143
+ describe('submittedCharter', () => {
144
+ const rawProposal = async () => ({
145
+ targetContractId: await generateRandomIdentifier(),
146
+ description: 'We remove spam and doxing within a day and warn before we act.',
147
+ reasons: randomIds(2),
148
+ moderatorsShare: 60,
149
+ rewardSplit: {
150
+ leader: 10,
151
+ equal: 40,
152
+ actions: 50,
153
+ },
154
+ });
155
+
156
+ it('should be valid', async () => {
157
+ expect(validate('submittedCharter', await rawProposal()).isValid()).to.be.true();
158
+ });
159
+
160
+ expectRequired('submittedCharter', rawProposal, ['targetContractId', 'description', 'reasons', 'rewardSplit']);
161
+ expectNoAdditionalProperties('submittedCharter', rawProposal, 'members');
162
+ expectNoAdditionalProperties('submittedCharter', rawProposal, 'abilities');
163
+
164
+ describe('description', () => {
165
+ it('should refuse 4097 characters', async () => {
166
+ const raw = await rawProposal();
167
+ raw.description = 'a'.repeat(4097);
168
+
169
+ const error = expectJsonSchemaError(validate('submittedCharter', raw));
170
+
171
+ expect(error.keyword).to.equal('maxLength');
172
+ });
173
+
174
+ it('should refuse more than 4096 bytes within 4096 characters', async () => {
175
+ // 2049 two-byte characters: within `maxLength`, which counts characters, but
176
+ // 4098 bytes, over `maxBytes` (DocumentPropertyMaxBytesExceededError, 10421)
177
+ const raw = await rawProposal();
178
+ raw.description = 'é'.repeat(2049);
179
+
180
+ const errors = validate('submittedCharter', raw).getErrors();
181
+
182
+ expect(errors).to.have.length(1);
183
+ expect(errors[0].getCode()).to.equal(10421);
184
+ });
185
+
186
+ it('should accept 4096 bytes in two-byte characters', async () => {
187
+ const raw = await rawProposal();
188
+ raw.description = 'é'.repeat(2048);
189
+
190
+ expect(validate('submittedCharter', raw).isValid()).to.be.true();
191
+ });
192
+ });
193
+
194
+ describe('reasons', () => {
195
+ it('should accept none, a team that can take no action', async () => {
196
+ const raw = await rawProposal();
197
+ raw.reasons = [];
198
+
199
+ expect(validate('submittedCharter', raw).isValid()).to.be.true();
200
+ });
201
+
202
+ it('should accept sixty-four', async () => {
203
+ const raw = await rawProposal();
204
+ raw.reasons = randomIds(64);
205
+
206
+ expect(validate('submittedCharter', raw).isValid()).to.be.true();
207
+ });
208
+
209
+ it('should refuse sixty-five', async () => {
210
+ const raw = await rawProposal();
211
+ raw.reasons = randomIds(65);
212
+
213
+ const error = expectJsonSchemaError(validate('submittedCharter', raw));
214
+
215
+ expect(error.keyword).to.equal('maxItems');
216
+ });
217
+
218
+ it('should refuse a repeated reason', async () => {
219
+ const raw = await rawProposal();
220
+ const [reason] = randomIds(1);
221
+ raw.reasons = [reason, Buffer.from(reason)];
222
+
223
+ const error = expectJsonSchemaError(validate('submittedCharter', raw));
224
+
225
+ expect(error.keyword).to.equal('uniqueItems');
226
+ });
227
+ });
228
+
229
+ describe('moderatorsShare', () => {
230
+ it('should be optional, the full declared fee', async () => {
231
+ const raw = await rawProposal();
232
+ delete raw.moderatorsShare;
233
+
234
+ expect(validate('submittedCharter', raw).isValid()).to.be.true();
235
+ });
236
+
237
+ it('should accept 0, a team that takes no rewards', async () => {
238
+ const raw = await rawProposal();
239
+ raw.moderatorsShare = 0;
240
+
241
+ expect(validate('submittedCharter', raw).isValid()).to.be.true();
242
+ });
243
+
244
+ it('should refuse a share over 100', async () => {
245
+ const raw = await rawProposal();
246
+ raw.moderatorsShare = 101;
247
+
248
+ const error = expectJsonSchemaError(validate('submittedCharter', raw));
249
+
250
+ expect(error.keyword).to.equal('maximum');
251
+ });
252
+ });
253
+
254
+ describe('rewardSplit', () => {
255
+ ['leader', 'equal', 'actions'].forEach((share) => {
256
+ it(`should require ${share}`, async () => {
257
+ const raw = await rawProposal();
258
+ delete raw.rewardSplit[share];
259
+
260
+ const error = expectJsonSchemaError(validate('submittedCharter', raw));
261
+
262
+ expect(error.keyword).to.equal('required');
263
+ expect(error.params.missingProperty).to.equal(share);
264
+ });
265
+ });
266
+
267
+ it('should refuse a share over 100', async () => {
268
+ const raw = await rawProposal();
269
+ raw.rewardSplit.leader = 101;
270
+
271
+ const error = expectJsonSchemaError(validate('submittedCharter', raw));
272
+
273
+ expect(error.keyword).to.equal('maximum');
274
+ });
275
+
276
+ it('should refuse an unknown share', async () => {
277
+ const raw = await rawProposal();
278
+ raw.rewardSplit.bonus = 0;
279
+
280
+ const error = expectJsonSchemaError(validate('submittedCharter', raw));
281
+
282
+ expect(error.keyword).to.equal('additionalProperties');
283
+ });
284
+ });
285
+ });
286
+
287
+ describe('joinRequest', () => {
288
+ const rawJoinRequest = async () => ({
289
+ submittedCharterId: await generateRandomIdentifier(),
290
+ recipientId: await generateRandomIdentifier(),
291
+ recipientKeyId: 3,
292
+ senderKeyId: 2,
293
+ // A 16-byte IV and two AES blocks.
294
+ encryptedMessage: crypto.randomBytes(48),
295
+ });
296
+
297
+ it('should be valid', async () => {
298
+ expect(validate('joinRequest', await rawJoinRequest()).isValid()).to.be.true();
299
+ });
300
+
301
+ expectRequired('joinRequest', rawJoinRequest, ['submittedCharterId', 'recipientId', 'recipientKeyId', 'senderKeyId', 'encryptedMessage']);
302
+ expectNoAdditionalProperties('joinRequest', rawJoinRequest, 'message');
303
+
304
+ it('should refuse a message shorter than an IV and a block', async () => {
305
+ const raw = await rawJoinRequest();
306
+ raw.encryptedMessage = crypto.randomBytes(31);
307
+
308
+ const error = expectJsonSchemaError(validate('joinRequest', raw));
309
+
310
+ expect(error.keyword).to.equal('minItems');
311
+ });
312
+
313
+ it('should refuse a message over 1040 bytes', async () => {
314
+ const raw = await rawJoinRequest();
315
+ raw.encryptedMessage = crypto.randomBytes(1041);
316
+
317
+ const error = expectJsonSchemaError(validate('joinRequest', raw));
318
+
319
+ expect(error.keyword).to.equal('maxItems');
320
+ });
321
+
322
+ it('should refuse a key id over u32', async () => {
323
+ const raw = await rawJoinRequest();
324
+ raw.senderKeyId = 4294967296;
325
+
326
+ expect(validate('joinRequest', raw).isValid()).to.be.false();
327
+ });
328
+ });
329
+
330
+ describe('electedCharter', () => {
331
+ const rawElectedCharter = async () => ({
332
+ targetContractId: await generateRandomIdentifier(),
333
+ submittedCharterId: await generateRandomIdentifier(),
334
+ members: randomIds(2),
335
+ });
336
+
337
+ it('should be valid', async () => {
338
+ expect(validate('electedCharter', await rawElectedCharter()).isValid()).to.be.true();
339
+ });
340
+
341
+ expectRequired('electedCharter', rawElectedCharter, ['targetContractId', 'submittedCharterId', 'members']);
342
+ expectNoAdditionalProperties('electedCharter', rawElectedCharter, 'leaderPower');
343
+
344
+ it('should accept a leader running alone', async () => {
345
+ const raw = await rawElectedCharter();
346
+ raw.members = [];
347
+
348
+ expect(validate('electedCharter', raw).isValid()).to.be.true();
349
+ });
350
+
351
+ it('should accept fifteen members', async () => {
352
+ const raw = await rawElectedCharter();
353
+ raw.members = randomIds(15);
354
+
355
+ expect(validate('electedCharter', raw).isValid()).to.be.true();
356
+ });
357
+
358
+ it('should refuse sixteen members', async () => {
359
+ const raw = await rawElectedCharter();
360
+ raw.members = randomIds(16);
361
+
362
+ const error = expectJsonSchemaError(validate('electedCharter', raw));
363
+
364
+ expect(error.keyword).to.equal('maxItems');
365
+ });
366
+
367
+ it('should refuse a repeated member', async () => {
368
+ const raw = await rawElectedCharter();
369
+ const [member] = randomIds(1);
370
+ raw.members = [member, Buffer.from(member)];
371
+
372
+ const error = expectJsonSchemaError(validate('electedCharter', raw));
373
+
374
+ expect(error.keyword).to.equal('uniqueItems');
375
+ });
376
+ });
377
+
378
+ describe('addedModerator', () => {
379
+ const rawAddition = async () => ({
380
+ electedCharterId: await generateRandomIdentifier(),
381
+ submittedCharterId: await generateRandomIdentifier(),
382
+ memberId: await generateRandomIdentifier(),
383
+ });
384
+
385
+ it('should be valid', async () => {
386
+ expect(validate('addedModerator', await rawAddition()).isValid()).to.be.true();
387
+ });
388
+
389
+ it('should be deletable, which takes the member off the team', () => {
390
+ expect(moderationChartersContractDocumentsSchema.addedModerator.canBeDeleted).to.be.true();
391
+ });
392
+
393
+ expectRequired('addedModerator', rawAddition, ['electedCharterId', 'submittedCharterId', 'memberId']);
394
+ expectNoAdditionalProperties('addedModerator', rawAddition, 'power');
395
+ });
396
+
397
+ describe('removedModerator', () => {
398
+ const rawRemoval = async () => ({
399
+ electedCharterId: await generateRandomIdentifier(),
400
+ memberId: await generateRandomIdentifier(),
401
+ });
402
+
403
+ it('should be valid without a resignation', async () => {
404
+ expect(validate('removedModerator', await rawRemoval()).isValid()).to.be.true();
405
+ });
406
+
407
+ it('should be deletable, which puts the member back on the team', () => {
408
+ expect(moderationChartersContractDocumentsSchema.removedModerator.canBeDeleted).to.be.true();
409
+ });
410
+
411
+ it('should remove only an elected member', () => {
412
+ const { refersTo } = moderationChartersContractDocumentsSchema.removedModerator.properties.memberId;
413
+
414
+ expect(refersTo).to.deep.equal({
415
+ type: 'listElement',
416
+ documentType: 'electedCharter',
417
+ propertyAgreement: { electedCharterId: '$id' },
418
+ inList: 'members',
419
+ });
420
+ });
421
+
422
+ expectRequired('removedModerator', rawRemoval, ['electedCharterId', 'memberId']);
423
+ expectNoAdditionalProperties('removedModerator', rawRemoval, 'resignationRequestId');
424
+ });
425
+
426
+ describe('resignationRequest', () => {
427
+ const rawResignation = async () => ({
428
+ electedCharterId: await generateRandomIdentifier(),
429
+ recipientId: await generateRandomIdentifier(),
430
+ recipientKeyId: 3,
431
+ senderKeyId: 2,
432
+ // A 16-byte IV and two AES blocks.
433
+ encryptedMessage: crypto.randomBytes(48),
434
+ });
435
+
436
+ it('should be valid', async () => {
437
+ expect(validate('resignationRequest', await rawResignation()).isValid()).to.be.true();
438
+ });
439
+
440
+ it('should be deletable, which withdraws it', () => {
441
+ expect(moderationChartersContractDocumentsSchema.resignationRequest.canBeDeleted).to.be.true();
442
+ });
443
+
444
+ it('should let only a team member ask to leave', () => {
445
+ const { anyOf } = moderationChartersContractDocumentsSchema.resignationRequest.ownerRefersTo;
446
+
447
+ expect(anyOf.map(({ type, documentType }) => [type, documentType])).to.deep.equal([
448
+ ['listElement', 'electedCharter'],
449
+ ['deletableDocument', 'addedModerator'],
450
+ ]);
451
+ });
452
+
453
+ expectRequired('resignationRequest', rawResignation, ['electedCharterId', 'recipientId', 'recipientKeyId', 'senderKeyId', 'encryptedMessage']);
454
+ expectNoAdditionalProperties('resignationRequest', rawResignation, 'memberId');
455
+
456
+ it('should refuse a message shorter than an IV and a block', async () => {
457
+ const raw = await rawResignation();
458
+ raw.encryptedMessage = crypto.randomBytes(31);
459
+
460
+ const error = expectJsonSchemaError(validate('resignationRequest', raw));
461
+
462
+ expect(error.keyword).to.equal('minItems');
463
+ });
464
+ });
465
+ });