nodebb-plugin-phone-verification 1.2.2
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/library.js +758 -0
- package/package.json +30 -0
- package/plugin.json +32 -0
- package/static/lib/admin.js +187 -0
- package/static/lib/main.js +456 -0
- package/templates/admin/plugins/phone-verification.tpl +163 -0
- package/templates/admin/settings/phone-verification.tpl +163 -0
- package/test/helpers.test.js +158 -0
- package/test/phone-storage.test.js +216 -0
- package/test/verification.test.js +173 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
<div class="acp-page-container">
|
|
2
|
+
<div class="row">
|
|
3
|
+
<div class="col-lg-12">
|
|
4
|
+
|
|
5
|
+
<div class="panel panel-primary">
|
|
6
|
+
<div class="panel-heading">
|
|
7
|
+
<h3 class="panel-title">
|
|
8
|
+
<i class="fa fa-cog"></i> הגדרות Call2All - שיחות קוליות
|
|
9
|
+
</h3>
|
|
10
|
+
</div>
|
|
11
|
+
<div class="panel-body">
|
|
12
|
+
<form id="voice-settings-form">
|
|
13
|
+
<div class="form-group">
|
|
14
|
+
<label for="voiceServerEnabled">
|
|
15
|
+
<input type="checkbox" id="voiceServerEnabled" name="voiceServerEnabled" />
|
|
16
|
+
הפעל שיחות קוליות
|
|
17
|
+
</label>
|
|
18
|
+
<p class="help-block">כאשר מופעל, התוסף ישלח שיחה קולית עם קוד האימות דרך Call2All</p>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div class="form-group">
|
|
22
|
+
<label for="voiceServerApiKey">Token של Call2All</label>
|
|
23
|
+
<input type="password" class="form-control" id="voiceServerApiKey" name="voiceServerApiKey"
|
|
24
|
+
placeholder="WU1BUElL.apik_xxxxx..." dir="ltr" />
|
|
25
|
+
<p class="help-block">ה-Token שקיבלת מ-Call2All (מתחיל ב-WU1BUElL)</p>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div class="form-group">
|
|
29
|
+
<details style="border: 1px solid #ddd; padding: 10px; border-radius: 4px; background-color: #f9f9f9;">
|
|
30
|
+
<summary style="cursor: pointer; font-weight: bold; color: #337ab7; outline: none;">
|
|
31
|
+
<i class="fa fa-cogs"></i> הגדרות מתקדמות (עריכת פרמטרים ותוכן ההודעה)
|
|
32
|
+
</summary>
|
|
33
|
+
<div style="margin-top: 15px; padding-left: 10px; border-left: 3px solid #337ab7;">
|
|
34
|
+
<div class="form-group">
|
|
35
|
+
<label for="voiceServerUrl">כתובת ה-API (Endpoint)</label>
|
|
36
|
+
<input type="text" class="form-control" id="voiceServerUrl" name="voiceServerUrl"
|
|
37
|
+
placeholder="https://www.call2all.co.il/ym/api/RunCampaign" dir="ltr" />
|
|
38
|
+
<p class="help-block">כתובת השרת אליו נשלחת הבקשה.</p>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="form-group">
|
|
41
|
+
<label for="voiceTtsMode">מצב ה-TTS (ttsMode)</label>
|
|
42
|
+
<input type="text" class="form-control" id="voiceTtsMode" name="voiceTtsMode"
|
|
43
|
+
placeholder="1" dir="ltr" />
|
|
44
|
+
<p class="help-block">ערך הפרמטר <code>ttsMode</code> הנשלח ל-API (ברירת מחדל: 1).</p>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="form-group">
|
|
47
|
+
<label for="voiceMessageTemplate">תוכן ההודעה (Template)</label>
|
|
48
|
+
<textarea class="form-control" id="voiceMessageTemplate" name="voiceMessageTemplate" rows="3" dir="rtl"></textarea>
|
|
49
|
+
<p class="help-block">
|
|
50
|
+
הטקסט שיוקרא למשתמש.<br/>
|
|
51
|
+
Placeholders חובה: <code>{code}</code> (הקוד), <code>{siteTitle}</code> (שם האתר)
|
|
52
|
+
</p>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</details>
|
|
56
|
+
</div>
|
|
57
|
+
<hr />
|
|
58
|
+
|
|
59
|
+
<div class="form-group">
|
|
60
|
+
<div class="checkbox">
|
|
61
|
+
<label class="mdl-switch mdl-js-switch mdl-js-ripple-effect">
|
|
62
|
+
<input type="checkbox" class="mdl-switch__input" id="blockUnverifiedUsers" name="blockUnverifiedUsers">
|
|
63
|
+
<span class="mdl-switch__label"><strong>חסום כתיבה למשתמשים לא מאומתים</strong></span>
|
|
64
|
+
</label>
|
|
65
|
+
</div>
|
|
66
|
+
<p class="help-block">
|
|
67
|
+
אם מופעל, משתמשים רשומים שלא אימתו את הטלפון שלהם לא יוכלו לפתוח נושאים חדשים או להגיב.
|
|
68
|
+
</p>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div class="form-group">
|
|
72
|
+
<button type="submit" class="btn btn-primary" id="save-settings-btn">
|
|
73
|
+
<i class="fa fa-save"></i> שמור הגדרות
|
|
74
|
+
</button>
|
|
75
|
+
<span id="settings-status" class="text-success" style="margin-right: 10px; display: none;">
|
|
76
|
+
<i class="fa fa-check"></i> נשמר!
|
|
77
|
+
</span>
|
|
78
|
+
</div>
|
|
79
|
+
</form>
|
|
80
|
+
|
|
81
|
+
<hr />
|
|
82
|
+
|
|
83
|
+
<h4>בדיקת שיחה</h4>
|
|
84
|
+
<div class="form-inline">
|
|
85
|
+
<div class="form-group">
|
|
86
|
+
<input type="text" class="form-control" id="test-phone"
|
|
87
|
+
placeholder="05X-XXXXXXX" dir="ltr" style="width: 150px;" />
|
|
88
|
+
</div>
|
|
89
|
+
<button type="button" class="btn btn-warning" id="test-call-btn">
|
|
90
|
+
<i class="fa fa-phone"></i> שלח שיחת בדיקה
|
|
91
|
+
</button>
|
|
92
|
+
<span id="test-status" style="margin-right: 10px;"></span>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<div class="panel panel-default">
|
|
98
|
+
<div class="panel-heading">
|
|
99
|
+
<h3 class="panel-title"><i class="fa fa-phone"></i> ניהול אימות טלפון</h3>
|
|
100
|
+
</div>
|
|
101
|
+
<div class="panel-body">
|
|
102
|
+
|
|
103
|
+
<div class="well">
|
|
104
|
+
<div class="row">
|
|
105
|
+
<div class="col-md-6">
|
|
106
|
+
<h4>חיפוש משתמש לפי מספר טלפון</h4>
|
|
107
|
+
<div class="form-group">
|
|
108
|
+
<div class="input-group">
|
|
109
|
+
<input type="text" class="form-control" id="phone-search" placeholder="הזן מספר טלפון" dir="ltr">
|
|
110
|
+
<span class="input-group-btn">
|
|
111
|
+
<button class="btn btn-primary" type="button" id="search-btn"><i class="fa fa-search"></i> חפש</button>
|
|
112
|
+
</span>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
<div class="col-md-6 text-left" style="padding-top: 35px;">
|
|
117
|
+
<button class="btn btn-success" id="btn-add-manual-user">
|
|
118
|
+
<i class="fa fa-plus"></i> הוסף משתמש מאומת ידנית
|
|
119
|
+
</button>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
<div id="search-result" style="display:none;"><div class="alert" id="search-alert"></div></div>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<div class="row" style="margin-bottom: 20px;">
|
|
126
|
+
<div class="col-md-4">
|
|
127
|
+
<div class="panel panel-info">
|
|
128
|
+
<div class="panel-heading"><h4 class="panel-title">סה"כ משתמשים עם טלפון</h4></div>
|
|
129
|
+
<div class="panel-body text-center"><h2 id="total-users">0</h2></div>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
<div class="col-md-4">
|
|
133
|
+
<div class="panel panel-success">
|
|
134
|
+
<div class="panel-heading"><h4 class="panel-title">משתמשים מאומתים</h4></div>
|
|
135
|
+
<div class="panel-body text-center"><h2 id="verified-count">0</h2></div>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<h4>רשימת משתמשים</h4>
|
|
141
|
+
<div class="table-responsive">
|
|
142
|
+
<table class="table table-striped table-hover" id="users-table">
|
|
143
|
+
<thead>
|
|
144
|
+
<tr>
|
|
145
|
+
<th>מזהה (UID)</th>
|
|
146
|
+
<th>שם משתמש</th>
|
|
147
|
+
<th>מספר טלפון</th>
|
|
148
|
+
<th>תאריך אימות</th>
|
|
149
|
+
<th>סטטוס</th>
|
|
150
|
+
<th class="text-right">פעולות לניהול</th>
|
|
151
|
+
</tr>
|
|
152
|
+
</thead>
|
|
153
|
+
<tbody id="users-tbody">
|
|
154
|
+
<tr><td colspan="6" class="text-center">טוען...</td></tr>
|
|
155
|
+
</tbody>
|
|
156
|
+
</table>
|
|
157
|
+
</div>
|
|
158
|
+
<nav aria-label="ניווט עמודים" class="text-center"><ul class="pagination" id="users-pagination"></ul></nav>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('assert');
|
|
4
|
+
const fc = require('fast-check');
|
|
5
|
+
const plugin = require('../library');
|
|
6
|
+
|
|
7
|
+
describe('Phone Verification Helper Functions', function () {
|
|
8
|
+
|
|
9
|
+
// **Feature: nodebb-phone-verification, Property 1: ולידציית מספר טלפון ישראלי**
|
|
10
|
+
// **Validates: Requirements 1.2**
|
|
11
|
+
describe('validatePhoneNumber', function () {
|
|
12
|
+
|
|
13
|
+
it('Property 1: should return true for valid Israeli mobile numbers (05X format)', function () {
|
|
14
|
+
// Generator for valid Israeli phone numbers
|
|
15
|
+
const validPhoneArb = fc.tuple(
|
|
16
|
+
fc.constantFrom('050', '051', '052', '053', '054', '055', '056', '057', '058', '059'),
|
|
17
|
+
fc.stringOf(fc.constantFrom('0', '1', '2', '3', '4', '5', '6', '7', '8', '9'), { minLength: 7, maxLength: 7 })
|
|
18
|
+
).map(([prefix, suffix]) => prefix + suffix);
|
|
19
|
+
|
|
20
|
+
fc.assert(
|
|
21
|
+
fc.property(validPhoneArb, (phone) => {
|
|
22
|
+
return plugin.validatePhoneNumber(phone) === true;
|
|
23
|
+
}),
|
|
24
|
+
{ numRuns: 100 }
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('Property 1: should return true for valid phones with hyphen (05X-XXXXXXX)', function () {
|
|
29
|
+
const validPhoneWithHyphenArb = fc.tuple(
|
|
30
|
+
fc.constantFrom('050', '051', '052', '053', '054', '055', '056', '057', '058', '059'),
|
|
31
|
+
fc.stringOf(fc.constantFrom('0', '1', '2', '3', '4', '5', '6', '7', '8', '9'), { minLength: 7, maxLength: 7 })
|
|
32
|
+
).map(([prefix, suffix]) => prefix + '-' + suffix);
|
|
33
|
+
|
|
34
|
+
fc.assert(
|
|
35
|
+
fc.property(validPhoneWithHyphenArb, (phone) => {
|
|
36
|
+
return plugin.validatePhoneNumber(phone) === true;
|
|
37
|
+
}),
|
|
38
|
+
{ numRuns: 100 }
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('Property 1: should return false for invalid phone formats', function () {
|
|
43
|
+
// Generator for invalid phones (wrong prefix, wrong length, etc.)
|
|
44
|
+
const invalidPhoneArb = fc.oneof(
|
|
45
|
+
// Wrong prefix (not starting with 05)
|
|
46
|
+
fc.tuple(
|
|
47
|
+
fc.constantFrom('04', '06', '07', '08', '09', '00', '01', '02', '03'),
|
|
48
|
+
fc.stringOf(fc.constantFrom('0', '1', '2', '3', '4', '5', '6', '7', '8', '9'), { minLength: 8, maxLength: 8 })
|
|
49
|
+
).map(([prefix, suffix]) => prefix + suffix),
|
|
50
|
+
// Too short
|
|
51
|
+
fc.stringOf(fc.constantFrom('0', '1', '2', '3', '4', '5', '6', '7', '8', '9'), { minLength: 1, maxLength: 9 }),
|
|
52
|
+
// Too long
|
|
53
|
+
fc.stringOf(fc.constantFrom('0', '1', '2', '3', '4', '5', '6', '7', '8', '9'), { minLength: 11, maxLength: 15 }),
|
|
54
|
+
// Contains letters
|
|
55
|
+
fc.tuple(
|
|
56
|
+
fc.stringOf(fc.constantFrom('0', '1', '2', '3', '4', '5', '6', '7', '8', '9'), { minLength: 5, maxLength: 8 }),
|
|
57
|
+
fc.stringOf(fc.constantFrom('a', 'b', 'c', 'd', 'e', 'A', 'B', 'C'), { minLength: 1, maxLength: 3 })
|
|
58
|
+
).map(([digits, letters]) => digits + letters)
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
fc.assert(
|
|
62
|
+
fc.property(invalidPhoneArb, (phone) => {
|
|
63
|
+
return plugin.validatePhoneNumber(phone) === false;
|
|
64
|
+
}),
|
|
65
|
+
{ numRuns: 100 }
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should return false for null, undefined, or non-string inputs', function () {
|
|
70
|
+
assert.strictEqual(plugin.validatePhoneNumber(null), false);
|
|
71
|
+
assert.strictEqual(plugin.validatePhoneNumber(undefined), false);
|
|
72
|
+
assert.strictEqual(plugin.validatePhoneNumber(123), false);
|
|
73
|
+
assert.strictEqual(plugin.validatePhoneNumber({}), false);
|
|
74
|
+
assert.strictEqual(plugin.validatePhoneNumber([]), false);
|
|
75
|
+
assert.strictEqual(plugin.validatePhoneNumber(''), false);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
// **Feature: nodebb-phone-verification, Property 8: נרמול מספר טלפון**
|
|
81
|
+
// **Validates: Requirements 4.3**
|
|
82
|
+
describe('normalizePhone', function () {
|
|
83
|
+
|
|
84
|
+
it('Property 8: normalized phone should have no hyphens and be 10 characters', function () {
|
|
85
|
+
// Generator for valid phones with optional hyphens
|
|
86
|
+
const phoneWithOptionalHyphenArb = fc.tuple(
|
|
87
|
+
fc.constantFrom('050', '051', '052', '053', '054', '055', '056', '057', '058', '059'),
|
|
88
|
+
fc.boolean(),
|
|
89
|
+
fc.stringOf(fc.constantFrom('0', '1', '2', '3', '4', '5', '6', '7', '8', '9'), { minLength: 7, maxLength: 7 })
|
|
90
|
+
).map(([prefix, hasHyphen, suffix]) => hasHyphen ? prefix + '-' + suffix : prefix + suffix);
|
|
91
|
+
|
|
92
|
+
fc.assert(
|
|
93
|
+
fc.property(phoneWithOptionalHyphenArb, (phone) => {
|
|
94
|
+
const normalized = plugin.normalizePhone(phone);
|
|
95
|
+
// Should have no hyphens
|
|
96
|
+
const noHyphens = !normalized.includes('-');
|
|
97
|
+
// Should be 10 characters
|
|
98
|
+
const correctLength = normalized.length === 10;
|
|
99
|
+
// Should only contain digits
|
|
100
|
+
const onlyDigits = /^\d+$/.test(normalized);
|
|
101
|
+
|
|
102
|
+
return noHyphens && correctLength && onlyDigits;
|
|
103
|
+
}),
|
|
104
|
+
{ numRuns: 100 }
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('Property 8: normalizing twice should give same result', function () {
|
|
109
|
+
const phoneArb = fc.tuple(
|
|
110
|
+
fc.constantFrom('050', '051', '052', '053', '054', '055', '056', '057', '058', '059'),
|
|
111
|
+
fc.boolean(),
|
|
112
|
+
fc.stringOf(fc.constantFrom('0', '1', '2', '3', '4', '5', '6', '7', '8', '9'), { minLength: 7, maxLength: 7 })
|
|
113
|
+
).map(([prefix, hasHyphen, suffix]) => hasHyphen ? prefix + '-' + suffix : prefix + suffix);
|
|
114
|
+
|
|
115
|
+
fc.assert(
|
|
116
|
+
fc.property(phoneArb, (phone) => {
|
|
117
|
+
const once = plugin.normalizePhone(phone);
|
|
118
|
+
const twice = plugin.normalizePhone(once);
|
|
119
|
+
return once === twice;
|
|
120
|
+
}),
|
|
121
|
+
{ numRuns: 100 }
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should return empty string for invalid inputs', function () {
|
|
126
|
+
assert.strictEqual(plugin.normalizePhone(null), '');
|
|
127
|
+
assert.strictEqual(plugin.normalizePhone(undefined), '');
|
|
128
|
+
assert.strictEqual(plugin.normalizePhone(123), '');
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// **Feature: nodebb-phone-verification, Property 2: יצירת קוד אימות תקין**
|
|
133
|
+
// **Validates: Requirements 2.1**
|
|
134
|
+
describe('generateVerificationCode', function () {
|
|
135
|
+
|
|
136
|
+
it('Property 2: generated code should always be exactly 6 digits', function () {
|
|
137
|
+
fc.assert(
|
|
138
|
+
fc.property(fc.constant(null), () => {
|
|
139
|
+
const code = plugin.generateVerificationCode();
|
|
140
|
+
// Should be exactly 6 characters
|
|
141
|
+
const correctLength = code.length === 6;
|
|
142
|
+
// Should only contain digits
|
|
143
|
+
const onlyDigits = /^\d{6}$/.test(code);
|
|
144
|
+
|
|
145
|
+
return correctLength && onlyDigits;
|
|
146
|
+
}),
|
|
147
|
+
{ numRuns: 100 }
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('Property 2: generated codes should be strings', function () {
|
|
152
|
+
for (let i = 0; i < 100; i++) {
|
|
153
|
+
const code = plugin.generateVerificationCode();
|
|
154
|
+
assert.strictEqual(typeof code, 'string');
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
});
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('assert');
|
|
4
|
+
const fc = require('fast-check');
|
|
5
|
+
const plugin = require('../library');
|
|
6
|
+
|
|
7
|
+
describe('Phone Storage', function () {
|
|
8
|
+
|
|
9
|
+
beforeEach(function () {
|
|
10
|
+
plugin.clearAllPhones();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// **Feature: nodebb-phone-verification, Property 6: ייחודיות מספר טלפון**
|
|
14
|
+
// **Validates: Requirements 4.1**
|
|
15
|
+
describe('Phone Uniqueness (Property 6)', function () {
|
|
16
|
+
|
|
17
|
+
it('Property 6: duplicate phone should be rejected for different user', function () {
|
|
18
|
+
const validPhoneArb = fc.tuple(
|
|
19
|
+
fc.constantFrom('050', '052', '054'),
|
|
20
|
+
fc.stringOf(fc.constantFrom('0', '1', '2', '3', '4', '5', '6', '7', '8', '9'), { minLength: 7, maxLength: 7 })
|
|
21
|
+
).map(([prefix, suffix]) => prefix + suffix);
|
|
22
|
+
|
|
23
|
+
const uidArb = fc.integer({ min: 1, max: 100000 });
|
|
24
|
+
|
|
25
|
+
fc.assert(
|
|
26
|
+
fc.property(validPhoneArb, uidArb, uidArb, (phone, uid1, uid2) => {
|
|
27
|
+
// Skip if same user
|
|
28
|
+
if (uid1 === uid2) return true;
|
|
29
|
+
|
|
30
|
+
plugin.clearAllPhones();
|
|
31
|
+
|
|
32
|
+
// First user saves phone
|
|
33
|
+
const result1 = plugin.savePhoneToUser(uid1, phone);
|
|
34
|
+
|
|
35
|
+
// Second user tries same phone
|
|
36
|
+
const result2 = plugin.savePhoneToUser(uid2, phone);
|
|
37
|
+
|
|
38
|
+
return result1.success === true &&
|
|
39
|
+
result2.success === false &&
|
|
40
|
+
result2.error === 'PHONE_EXISTS';
|
|
41
|
+
}),
|
|
42
|
+
{ numRuns: 100 }
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('Property 6: isPhoneExists should return true for existing phone', function () {
|
|
47
|
+
const phone = '0501234567';
|
|
48
|
+
const uid = 1;
|
|
49
|
+
|
|
50
|
+
assert.strictEqual(plugin.isPhoneExists(phone), false);
|
|
51
|
+
plugin.savePhoneToUser(uid, phone);
|
|
52
|
+
assert.strictEqual(plugin.isPhoneExists(phone), true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('Property 6: same user can update their own phone', function () {
|
|
56
|
+
const uid = 1;
|
|
57
|
+
const phone1 = '0501234567';
|
|
58
|
+
const phone2 = '0521234567';
|
|
59
|
+
|
|
60
|
+
plugin.savePhoneToUser(uid, phone1);
|
|
61
|
+
const result = plugin.savePhoneToUser(uid, phone1); // Same phone, same user
|
|
62
|
+
assert.strictEqual(result.success, true);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// **Feature: nodebb-phone-verification, Property 7: שמירה ושליפה (Round-trip)**
|
|
67
|
+
// **Validates: Requirements 4.2, 4.3**
|
|
68
|
+
describe('Save and Retrieve Round-trip (Property 7)', function () {
|
|
69
|
+
|
|
70
|
+
it('Property 7: saved phone should be retrievable with same normalized value', function () {
|
|
71
|
+
const validPhoneArb = fc.tuple(
|
|
72
|
+
fc.constantFrom('050', '052', '054'),
|
|
73
|
+
fc.boolean(), // with or without hyphen
|
|
74
|
+
fc.stringOf(fc.constantFrom('0', '1', '2', '3', '4', '5', '6', '7', '8', '9'), { minLength: 7, maxLength: 7 })
|
|
75
|
+
).map(([prefix, hasHyphen, suffix]) => hasHyphen ? prefix + '-' + suffix : prefix + suffix);
|
|
76
|
+
|
|
77
|
+
const uidArb = fc.integer({ min: 1, max: 100000 });
|
|
78
|
+
|
|
79
|
+
fc.assert(
|
|
80
|
+
fc.property(validPhoneArb, uidArb, (phone, uid) => {
|
|
81
|
+
plugin.clearAllPhones();
|
|
82
|
+
|
|
83
|
+
plugin.savePhoneToUser(uid, phone);
|
|
84
|
+
const retrieved = plugin.getUserPhone(uid);
|
|
85
|
+
|
|
86
|
+
// Retrieved phone should be normalized (no hyphens)
|
|
87
|
+
const expectedNormalized = plugin.normalizePhone(phone);
|
|
88
|
+
|
|
89
|
+
return retrieved !== null &&
|
|
90
|
+
retrieved.phone === expectedNormalized &&
|
|
91
|
+
retrieved.phone.length === 10 &&
|
|
92
|
+
!retrieved.phone.includes('-');
|
|
93
|
+
}),
|
|
94
|
+
{ numRuns: 100 }
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('Property 7: findUserByPhone should return correct uid', function () {
|
|
99
|
+
const validPhoneArb = fc.tuple(
|
|
100
|
+
fc.constantFrom('050', '052', '054'),
|
|
101
|
+
fc.stringOf(fc.constantFrom('0', '1', '2', '3', '4', '5', '6', '7', '8', '9'), { minLength: 7, maxLength: 7 })
|
|
102
|
+
).map(([prefix, suffix]) => prefix + suffix);
|
|
103
|
+
|
|
104
|
+
const uidArb = fc.integer({ min: 1, max: 100000 });
|
|
105
|
+
|
|
106
|
+
fc.assert(
|
|
107
|
+
fc.property(validPhoneArb, uidArb, (phone, uid) => {
|
|
108
|
+
plugin.clearAllPhones();
|
|
109
|
+
|
|
110
|
+
plugin.savePhoneToUser(uid, phone);
|
|
111
|
+
const foundUid = plugin.findUserByPhone(phone);
|
|
112
|
+
|
|
113
|
+
return foundUid === uid;
|
|
114
|
+
}),
|
|
115
|
+
{ numRuns: 100 }
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('getAllUsersWithPhones', function () {
|
|
121
|
+
|
|
122
|
+
it('should return all users with phones', function () {
|
|
123
|
+
plugin.savePhoneToUser(1, '0501111111');
|
|
124
|
+
plugin.savePhoneToUser(2, '0502222222');
|
|
125
|
+
plugin.savePhoneToUser(3, '0503333333');
|
|
126
|
+
|
|
127
|
+
const all = plugin.getAllUsersWithPhones();
|
|
128
|
+
assert.strictEqual(all.length, 3);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
// **Feature: nodebb-phone-verification, Property 9: חיפוש לפי מספר טלפון**
|
|
135
|
+
// **Validates: Requirements 5.2**
|
|
136
|
+
describe('Search by Phone (Property 9)', function () {
|
|
137
|
+
|
|
138
|
+
it('Property 9: findUserByPhone should return correct user for saved phone', function () {
|
|
139
|
+
const validPhoneArb = fc.tuple(
|
|
140
|
+
fc.constantFrom('050', '052', '054'),
|
|
141
|
+
fc.stringOf(fc.constantFrom('0', '1', '2', '3', '4', '5', '6', '7', '8', '9'), { minLength: 7, maxLength: 7 })
|
|
142
|
+
).map(([prefix, suffix]) => prefix + suffix);
|
|
143
|
+
|
|
144
|
+
const uidArb = fc.integer({ min: 1, max: 100000 });
|
|
145
|
+
|
|
146
|
+
fc.assert(
|
|
147
|
+
fc.property(validPhoneArb, uidArb, (phone, uid) => {
|
|
148
|
+
plugin.clearAllPhones();
|
|
149
|
+
|
|
150
|
+
plugin.savePhoneToUser(uid, phone);
|
|
151
|
+
const foundUid = plugin.findUserByPhone(phone);
|
|
152
|
+
|
|
153
|
+
return foundUid === uid;
|
|
154
|
+
}),
|
|
155
|
+
{ numRuns: 100 }
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('Property 9: search with hyphen should find same user as without', function () {
|
|
160
|
+
const phone = '0501234567';
|
|
161
|
+
const phoneWithHyphen = '050-1234567';
|
|
162
|
+
const uid = 42;
|
|
163
|
+
|
|
164
|
+
plugin.savePhoneToUser(uid, phone);
|
|
165
|
+
|
|
166
|
+
const found1 = plugin.findUserByPhone(phone);
|
|
167
|
+
const found2 = plugin.findUserByPhone(phoneWithHyphen);
|
|
168
|
+
|
|
169
|
+
assert.strictEqual(found1, uid);
|
|
170
|
+
assert.strictEqual(found2, uid);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// **Feature: nodebb-phone-verification, Property 10: הסתרת טלפון ממשתמשים רגילים**
|
|
175
|
+
// **Validates: Requirements 5.3**
|
|
176
|
+
describe('Phone Privacy (Property 10)', function () {
|
|
177
|
+
|
|
178
|
+
it('Property 10: admin can view any phone', function () {
|
|
179
|
+
const uid = 1;
|
|
180
|
+
const callerUid = 999;
|
|
181
|
+
const isAdmin = true;
|
|
182
|
+
|
|
183
|
+
assert.strictEqual(plugin.canViewPhone(uid, callerUid, isAdmin), true);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('Property 10: regular user cannot view other user phone', function () {
|
|
187
|
+
const uid = 1;
|
|
188
|
+
const callerUid = 2;
|
|
189
|
+
const isAdmin = false;
|
|
190
|
+
|
|
191
|
+
assert.strictEqual(plugin.canViewPhone(uid, callerUid, isAdmin), false);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('Property 10: user can view own phone', function () {
|
|
195
|
+
const uid = 1;
|
|
196
|
+
const callerUid = 1;
|
|
197
|
+
const isAdmin = false;
|
|
198
|
+
|
|
199
|
+
assert.strictEqual(plugin.canViewPhone(uid, callerUid, isAdmin), true);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('Property 10: privacy rule applies to all users', function () {
|
|
203
|
+
const uidArb = fc.integer({ min: 1, max: 100000 });
|
|
204
|
+
|
|
205
|
+
fc.assert(
|
|
206
|
+
fc.property(uidArb, uidArb, (uid, callerUid) => {
|
|
207
|
+
const isAdmin = false;
|
|
208
|
+
const canView = plugin.canViewPhone(uid, callerUid, isAdmin);
|
|
209
|
+
|
|
210
|
+
// Non-admin can only view own phone
|
|
211
|
+
return canView === (uid === callerUid);
|
|
212
|
+
}),
|
|
213
|
+
{ numRuns: 100 }
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
});
|