mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 932803 - [Contacts API] Add tests for migrations. r=reuben
This commit is contained in:
parent
951004a300
commit
ea02b3a6a4
@ -20,12 +20,13 @@ Cu.import("resource://gre/modules/IndexedDBHelper.jsm");
|
||||
Cu.import("resource://gre/modules/PhoneNumberUtils.jsm");
|
||||
Cu.importGlobalProperties(["indexedDB"]);
|
||||
|
||||
const DB_NAME = "contacts";
|
||||
const DB_VERSION = 19;
|
||||
const STORE_NAME = "contacts";
|
||||
const SAVED_GETALL_STORE_NAME = "getallcache";
|
||||
/* all exported symbols need to be bound to this on B2G - Bug 961777 */
|
||||
this.DB_NAME = "contacts";
|
||||
this.DB_VERSION = 19;
|
||||
this.STORE_NAME = "contacts";
|
||||
this.SAVED_GETALL_STORE_NAME = "getallcache";
|
||||
const CHUNK_SIZE = 20;
|
||||
const REVISION_STORE = "revision";
|
||||
this.REVISION_STORE = "revision";
|
||||
const REVISION_KEY = "revision";
|
||||
|
||||
function exportContact(aRecord) {
|
||||
|
@ -22,7 +22,9 @@ XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
|
||||
"@mozilla.org/parentprocessmessagemanager;1",
|
||||
"nsIMessageListenerManager");
|
||||
|
||||
let ContactService = {
|
||||
|
||||
/* all exported symbols need to be bound to this on B2G - Bug 961777 */
|
||||
let ContactService = this.ContactService = {
|
||||
init: function() {
|
||||
if (DEBUG) debug("Init");
|
||||
this._messages = ["Contacts:Find", "Contacts:GetAll", "Contacts:GetAll:SendNow",
|
||||
|
@ -10,3 +10,8 @@
|
||||
[test_contacts_international.html]
|
||||
[test_contacts_substringmatching.html]
|
||||
[test_contacts_substringmatchingVE.html]
|
||||
[test_migration.html]
|
||||
support-files =
|
||||
test_migration_chrome.js
|
||||
skip-if = os == "android"
|
||||
|
||||
|
@ -389,7 +389,7 @@ function checkCount(count, msg, then) {
|
||||
var index = 0;
|
||||
|
||||
function next() {
|
||||
ok(true, "Begin!");
|
||||
info("Step " + index);
|
||||
if (index >= steps.length) {
|
||||
ok(false, "Shouldn't get here!");
|
||||
return;
|
||||
|
@ -295,7 +295,8 @@ var steps = [
|
||||
},
|
||||
function () {
|
||||
ok(true, "all done!\n");
|
||||
SpecialPowers.setIntPref("dom.phonenumber.substringmatching.BR", -1);
|
||||
SpecialPowers.clearUserPref("dom.phonenumber.substringmatching.BR");
|
||||
SpecialPowers.clearUserPref("ril.lastKnownSimMcc");
|
||||
SimpleTest.finish();
|
||||
}
|
||||
];
|
||||
|
@ -124,7 +124,8 @@ var steps = [
|
||||
},
|
||||
function () {
|
||||
ok(true, "all done!\n");
|
||||
SpecialPowers.setIntPref("dom.phonenumber.substringmatching.VE", -1);
|
||||
SpecialPowers.clearUserPref("dom.phonenumber.substringmatching.BR");
|
||||
SpecialPowers.clearUserPref("ril.lastKnownSimMcc");
|
||||
SimpleTest.finish();
|
||||
}
|
||||
];
|
||||
|
210
dom/contacts/tests/test_migration.html
Normal file
210
dom/contacts/tests/test_migration.html
Normal file
@ -0,0 +1,210 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Migration tests</title>
|
||||
<script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
</head>
|
||||
<body>
|
||||
<h1>migration tests</h1>
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none">
|
||||
|
||||
</div>
|
||||
<pre id="test">
|
||||
<script type="text/javascript;version=1.8" src="shared.js"></script>
|
||||
<script class="testbody" type="text/javascript">
|
||||
"use strict";
|
||||
|
||||
var backend, contactsCount, allContacts;
|
||||
function loadChromeScript() {
|
||||
var url = SimpleTest.getTestFileURL("test_migration_chrome.js");
|
||||
backend = SpecialPowers.loadChromeScript(url);
|
||||
}
|
||||
|
||||
function addBackendEvents() {
|
||||
backend.addMessageListener("createDB.success", function(count) {
|
||||
contactsCount = count;
|
||||
ok(true, "Created the database");
|
||||
next();
|
||||
});
|
||||
backend.addMessageListener("createDB.error", function(err) {
|
||||
ok(false, err);
|
||||
next();
|
||||
});
|
||||
|
||||
backend.addMessageListener("deleteDB.success", function() {
|
||||
ok(true, "Deleted the database");
|
||||
next();
|
||||
});
|
||||
backend.addMessageListener("deleteDB.error", function(err) {
|
||||
ok(false, err);
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
function createDB(version) {
|
||||
info("Will create the DB at version " + version);
|
||||
backend.sendAsyncMessage("createDB", version);
|
||||
}
|
||||
|
||||
function deleteDB() {
|
||||
info("Will delete the DB.");
|
||||
backend.sendAsyncMessage("deleteDB");
|
||||
}
|
||||
|
||||
function setSubstringMatching(value) {
|
||||
info("Setting substring matching to " + value);
|
||||
|
||||
if (value) {
|
||||
SpecialPowers.setIntPref("dom.phonenumber.substringmatching.BR", value);
|
||||
|
||||
// this is the Mcc for Brazil, so that we trigger the previous pref
|
||||
SpecialPowers.setCharPref("ril.lastKnownSimMcc", "724");
|
||||
} else {
|
||||
SpecialPowers.clearUserPref("dom.phonenumber.substringmatching.BR");
|
||||
SpecialPowers.clearUserPref("ril.lastKnownSimMcc");
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
var steps = [
|
||||
function setupChromeScript() {
|
||||
loadChromeScript();
|
||||
addBackendEvents();
|
||||
next();
|
||||
},
|
||||
|
||||
deleteDB, // let's be sure the DB does not exist yet
|
||||
createDB.bind(null, 12),
|
||||
setSubstringMatching.bind(null, 7),
|
||||
|
||||
function testAccessMozContacts() {
|
||||
info("Checking we have the right number of contacts: " + contactsCount);
|
||||
var req = mozContacts.getCount();
|
||||
req.onsuccess = function onsuccess() {
|
||||
ok(true, "Could access the mozContacts API");
|
||||
ise(this.result, contactsCount, "Contacts count is correct");
|
||||
next();
|
||||
};
|
||||
|
||||
req.onerror = function onerror() {
|
||||
ok(false, "Couldn't access the mozContacts API");
|
||||
next();
|
||||
};
|
||||
},
|
||||
|
||||
function testRetrieveAllContacts() {
|
||||
/* if the migration does not work right, either we'll have an error, or the
|
||||
contacts won't be migrated properly and thus will fail WebIDL conversion,
|
||||
which will manifest as a timeout */
|
||||
info("Checking the contacts are corrected to obey WebIDL constraints. (upgrades 14 to 17)");
|
||||
var req = mozContacts.find();
|
||||
req.onsuccess = function onsuccess() {
|
||||
if (this.result) {
|
||||
ise(this.result.length, contactsCount, "Contacts array length is correct");
|
||||
allContacts = this.result;
|
||||
next();
|
||||
} else {
|
||||
ok(false, "Could access the mozContacts API but got no contacts!");
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
req.onerror = function onerror() {
|
||||
ok(false, "Couldn't access the mozContacts API");
|
||||
next();
|
||||
};
|
||||
},
|
||||
|
||||
function checkNameIndex() {
|
||||
info("Checking name index migration (upgrades 17 to 19).");
|
||||
if (!allContacts) {
|
||||
next();
|
||||
}
|
||||
|
||||
var count = allContacts.length;
|
||||
|
||||
function finishRequest() {
|
||||
count--;
|
||||
if (!count) {
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
allContacts.forEach(function(contact) {
|
||||
var name = contact.name && contact.name[0];
|
||||
if (!name) {
|
||||
count--;
|
||||
return;
|
||||
}
|
||||
|
||||
var req = mozContacts.find({
|
||||
filterBy: ["name"],
|
||||
filterValue: name,
|
||||
filterOp: "equals"
|
||||
});
|
||||
|
||||
req.onsuccess = function onsuccess() {
|
||||
if (this.result) {
|
||||
info("Found contact '" + name + "', checking it's the correct one.");
|
||||
checkContacts(this.result[0], contact);
|
||||
} else {
|
||||
ok(false, "Could not find contact with name '" + name + "'");
|
||||
}
|
||||
|
||||
finishRequest();
|
||||
};
|
||||
|
||||
req.onerror = function onerror() {
|
||||
ok(false, "Error while finding contact with name '" + name + "'!");
|
||||
finishRequest();
|
||||
}
|
||||
});
|
||||
|
||||
if (!count) {
|
||||
ok(false, "No contact had a name, this is unexpected.");
|
||||
next();
|
||||
}
|
||||
},
|
||||
|
||||
function checkSubstringMatching() {
|
||||
var subject = "0004567890"; // the last 7 digits are the same that at least one contact
|
||||
info("Looking for a contact matching " + subject);
|
||||
var req = mozContacts.find({
|
||||
filterValue: subject,
|
||||
filterOp: "match",
|
||||
filterBy: ["tel"],
|
||||
filterLimit: 1
|
||||
});
|
||||
|
||||
req.onsuccess = function onsuccess() {
|
||||
if (this.result && this.result[0]) {
|
||||
ok(true, "Found a contact with number " + this.result[0].tel[0].value);
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
req.onerror = function onerror() {
|
||||
ok(false, "Error while finding contact for substring matching check!");
|
||||
next();
|
||||
};
|
||||
},
|
||||
|
||||
deleteDB,
|
||||
setSubstringMatching.bind(null, null),
|
||||
|
||||
function finish() {
|
||||
backend.destroy();
|
||||
info("all done!\n");
|
||||
SimpleTest.finish();
|
||||
}
|
||||
];
|
||||
|
||||
start_tests();
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
331
dom/contacts/tests/test_migration_chrome.js
Normal file
331
dom/contacts/tests/test_migration_chrome.js
Normal file
@ -0,0 +1,331 @@
|
||||
/* global
|
||||
sendAsyncMessage,
|
||||
addMessageListener,
|
||||
indexedDB
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
|
||||
|
||||
let imports = {};
|
||||
|
||||
Cu.import("resource://gre/modules/ContactDB.jsm", imports);
|
||||
Cu.import("resource://gre/modules/ContactService.jsm", imports);
|
||||
Cu.import("resource://gre/modules/Promise.jsm", imports);
|
||||
Cu.importGlobalProperties(["indexedDB"]);
|
||||
|
||||
const {
|
||||
STORE_NAME,
|
||||
SAVED_GETALL_STORE_NAME,
|
||||
REVISION_STORE,
|
||||
DB_NAME,
|
||||
ContactService,
|
||||
Promise
|
||||
} = imports;
|
||||
|
||||
let DEBUG = false;
|
||||
function debug(str) {
|
||||
if (DEBUG){
|
||||
dump("-*- TestMigrationChromeScript: " + str + "\n");
|
||||
}
|
||||
}
|
||||
|
||||
const DATA = {
|
||||
12: {
|
||||
SCHEMA: function createSchema12(db, transaction) {
|
||||
let objectStore = db.createObjectStore(STORE_NAME, {keyPath: "id"});
|
||||
objectStore.createIndex("familyName", "properties.familyName", { multiEntry: true });
|
||||
objectStore.createIndex("givenName", "properties.givenName", { multiEntry: true });
|
||||
objectStore.createIndex("familyNameLowerCase", "search.familyName", { multiEntry: true });
|
||||
objectStore.createIndex("givenNameLowerCase", "search.givenName", { multiEntry: true });
|
||||
objectStore.createIndex("telLowerCase", "search.tel", { multiEntry: true });
|
||||
objectStore.createIndex("emailLowerCase", "search.email", { multiEntry: true });
|
||||
objectStore.createIndex("tel", "search.exactTel", { multiEntry: true });
|
||||
objectStore.createIndex("category", "properties.category", { multiEntry: true });
|
||||
objectStore.createIndex("email", "search.email", { multiEntry: true });
|
||||
objectStore.createIndex("telMatch", "search.parsedTel", {multiEntry: true});
|
||||
db.createObjectStore(SAVED_GETALL_STORE_NAME);
|
||||
db.createObjectStore(REVISION_STORE).put(0, "revision");
|
||||
},
|
||||
BAD: [
|
||||
{
|
||||
// this contact is bad because its "name" property is not an array and
|
||||
// is the empty string (upgrade 16 to 17)
|
||||
"properties": {
|
||||
"name": "",
|
||||
"email": [],
|
||||
"url": [{
|
||||
"type": ["source"],
|
||||
"value": "urn:service:gmail:uid:http://www.google.com/m8/feeds/contacts/XXX/base/4567894654"
|
||||
}],
|
||||
"category": ["gmail"],
|
||||
"adr": [],
|
||||
"tel": [{
|
||||
"type": ["mobile"],
|
||||
"value": "+7 123 456-78-90"
|
||||
}],
|
||||
"sex": "undefined",
|
||||
"genderIdentity": "undefined"
|
||||
},
|
||||
"search": {
|
||||
"givenName": [],
|
||||
"familyName": [],
|
||||
"email": [],
|
||||
"category": ["gmail"],
|
||||
"tel": ["+71234567890","71234567890","1234567890","234567890","34567890",
|
||||
"4567890","567890","67890","7890","890","90","0","81234567890"],
|
||||
"exactTel": ["+71234567890"],
|
||||
"parsedTel": ["+71234567890","1234567890","81234567890","34567890"]
|
||||
},
|
||||
"updated": new Date("2013-07-27T16:47:40.974Z"),
|
||||
"published": new Date("2013-07-27T16:47:40.974Z"),
|
||||
"id": "bad-1"
|
||||
},
|
||||
{
|
||||
// This contact is bad because its "name" property is not an array
|
||||
// (upgrade 14 to 15)
|
||||
"properties": {
|
||||
"name": "name-bad-2",
|
||||
"email": [],
|
||||
"url": [{
|
||||
"type": ["source"],
|
||||
"value": "urn:service:gmail:uid:http://www.google.com/m8/feeds/contacts/XXX/base/4567894654"
|
||||
}],
|
||||
"category": ["gmail"],
|
||||
"adr": [],
|
||||
"tel": [{
|
||||
"type": ["mobile"],
|
||||
"value": "+7 123 456-78-90"
|
||||
}],
|
||||
"sex": "undefined",
|
||||
"genderIdentity": "undefined"
|
||||
},
|
||||
"search": {
|
||||
"givenName": [],
|
||||
"familyName": [],
|
||||
"email": [],
|
||||
"category": ["gmail"],
|
||||
"tel": ["+71234567890","71234567890","1234567890","234567890","34567890",
|
||||
"4567890","567890","67890","7890","890","90","0","81234567890"],
|
||||
"exactTel": ["+71234567890"],
|
||||
"parsedTel": ["+71234567890","1234567890","81234567890","34567890"]
|
||||
},
|
||||
"updated": new Date("2013-07-27T16:47:40.974Z"),
|
||||
"published": new Date("2013-07-27T16:47:40.974Z"),
|
||||
"id": "bad-2"
|
||||
},
|
||||
{
|
||||
// This contact is bad because its bday property is a String (upgrade 15
|
||||
// to 16), and its anniversary property is an empty string (upgrade 16
|
||||
// to 17)
|
||||
"properties": {
|
||||
"name": ["name-bad-3"],
|
||||
"email": [],
|
||||
"url": [{
|
||||
"type": ["source"],
|
||||
"value": "urn:service:gmail:uid:http://www.google.com/m8/feeds/contacts/XXX/base/4567894654"
|
||||
}],
|
||||
"category": ["gmail"],
|
||||
"adr": [],
|
||||
"tel": [{
|
||||
"type": ["mobile"],
|
||||
"value": "+7 123 456-78-90"
|
||||
}],
|
||||
"sex": "undefined",
|
||||
"genderIdentity": "undefined",
|
||||
"bday": "2013-07-27T16:47:40.974Z",
|
||||
"anniversary": ""
|
||||
},
|
||||
"search": {
|
||||
"givenName": [],
|
||||
"familyName": [],
|
||||
"email": [],
|
||||
"category": ["gmail"],
|
||||
"tel": ["+71234567890","71234567890","1234567890","234567890","34567890",
|
||||
"4567890","567890","67890","7890","890","90","0","81234567890"],
|
||||
"exactTel": ["+71234567890"],
|
||||
"parsedTel": ["+71234567890","1234567890","81234567890","34567890"]
|
||||
},
|
||||
"updated": new Date("2013-07-27T16:47:40.974Z"),
|
||||
"published": new Date("2013-07-27T16:47:40.974Z"),
|
||||
"id": "bad-3"
|
||||
},
|
||||
{
|
||||
// This contact is bad because its tel property has a tel.type null
|
||||
// value (upgrade 16 to 17), and email.type not array value (upgrade 14
|
||||
// to 15)
|
||||
"properties": {
|
||||
"name": ["name-bad-4"],
|
||||
"email": [{
|
||||
"value": "toto@toto.com",
|
||||
"type": "home"
|
||||
}],
|
||||
"url": [{
|
||||
"type": ["source"],
|
||||
"value": "urn:service:gmail:uid:http://www.google.com/m8/feeds/contacts/XXX/base/4567894654"
|
||||
}],
|
||||
"category": ["gmail"],
|
||||
"adr": [],
|
||||
"tel": [{
|
||||
"type": null,
|
||||
"value": "+7 123 456-78-90"
|
||||
}],
|
||||
"sex": "undefined",
|
||||
"genderIdentity": "undefined"
|
||||
},
|
||||
"search": {
|
||||
"givenName": [],
|
||||
"familyName": [],
|
||||
"email": [],
|
||||
"category": ["gmail"],
|
||||
"tel": ["+71234567890","71234567890","1234567890","234567890","34567890",
|
||||
"4567890","567890","67890","7890","890","90","0","81234567890"],
|
||||
"exactTel": ["+71234567890"],
|
||||
"parsedTel": ["+71234567890","1234567890","81234567890","34567890"]
|
||||
},
|
||||
"updated": new Date("2013-07-27T16:47:40.974Z"),
|
||||
"published": new Date("2013-07-27T16:47:40.974Z"),
|
||||
"id": "bad-4"
|
||||
}
|
||||
],
|
||||
GOOD: [
|
||||
{
|
||||
"properties": {
|
||||
"name": ["name-good-1"],
|
||||
"email": [],
|
||||
"url": [{
|
||||
"type": ["source"],
|
||||
"value": "urn:service:gmail:uid:http://www.google.com/m8/feeds/contacts/XXX/base/4567894654"
|
||||
}],
|
||||
"category": ["gmail"],
|
||||
"adr": [],
|
||||
"tel": [{
|
||||
"type": ["mobile"],
|
||||
"value": "+7 123 456-78-90"
|
||||
}],
|
||||
"sex": "undefined",
|
||||
"genderIdentity": "undefined"
|
||||
},
|
||||
"search": {
|
||||
"givenName": [],
|
||||
"familyName": [],
|
||||
"email": [],
|
||||
"category": ["gmail"],
|
||||
"tel": ["+71234567890","71234567890","1234567890","234567890","34567890",
|
||||
"4567890","567890","67890","7890","890","90","0","81234567890"],
|
||||
"exactTel": ["+71234567890"],
|
||||
"parsedTel": ["+71234567890","1234567890","81234567890","34567890"]
|
||||
},
|
||||
"updated": new Date("2013-07-27T16:47:40.974Z"),
|
||||
"published": new Date("2013-07-27T16:47:40.974Z"),
|
||||
"id": "good-1"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
function DataManager(version) {
|
||||
if (!(version in DATA)) {
|
||||
throw new Error("Version " + version + " can't be found in our test datas.");
|
||||
}
|
||||
|
||||
this.version = version;
|
||||
this.data = DATA[version];
|
||||
}
|
||||
|
||||
DataManager.prototype = {
|
||||
open: function() {
|
||||
debug("opening for version " + this.version);
|
||||
var deferred = Promise.defer();
|
||||
|
||||
let req = indexedDB.open(DB_NAME, this.version);
|
||||
req.onupgradeneeded = function() {
|
||||
let db = req.result;
|
||||
let transaction = req.transaction;
|
||||
this.createSchema(db, transaction);
|
||||
this.addContacts(db, transaction);
|
||||
}.bind(this);
|
||||
|
||||
req.onsuccess = function() {
|
||||
debug("succeeded opening the db, let's close it now");
|
||||
req.result.close();
|
||||
deferred.resolve(this.contactsCount());
|
||||
}.bind(this);
|
||||
|
||||
req.onerror = function() {
|
||||
deferred.reject(this.error);
|
||||
};
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
createSchema: function(db, transaction) {
|
||||
debug("createSchema for version " + this.version);
|
||||
this.data.SCHEMA(db, transaction);
|
||||
},
|
||||
|
||||
addContacts: function(db, transaction) {
|
||||
debug("adding contacts for version " + this.version);
|
||||
var os = transaction.objectStore(STORE_NAME);
|
||||
|
||||
this.data.GOOD.forEach(function(contact) {
|
||||
os.put(contact);
|
||||
});
|
||||
this.data.BAD.forEach(function(contact) {
|
||||
os.put(contact);
|
||||
});
|
||||
},
|
||||
|
||||
contactsCount: function() {
|
||||
return this.data.BAD.length + this.data.GOOD.length;
|
||||
}
|
||||
};
|
||||
|
||||
DataManager.delete = function() {
|
||||
debug("Deleting the database");
|
||||
var deferred = Promise.defer();
|
||||
|
||||
/* forcibly close the db before deleting it */
|
||||
ContactService._db.close();
|
||||
|
||||
var req = indexedDB.deleteDatabase(DB_NAME);
|
||||
req.onsuccess = function() {
|
||||
debug("Successfully deleted!");
|
||||
deferred.resolve();
|
||||
};
|
||||
|
||||
req.onerror = function() {
|
||||
debug("Not deleted, error is " + this.error.name);
|
||||
deferred.reject(this.error);
|
||||
};
|
||||
|
||||
req.onblocked = function() {
|
||||
debug("Waiting for the current users");
|
||||
};
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
addMessageListener("createDB", function(version) {
|
||||
// Promises help handling gracefully exceptions
|
||||
Promise.resolve().then(function() {
|
||||
return new DataManager(version);
|
||||
}).then(function(manager) {
|
||||
return manager.open();
|
||||
}).then(function onSuccess(count) {
|
||||
sendAsyncMessage("createDB.success", count);
|
||||
}, function onError(err) {
|
||||
sendAsyncMessage("createDB.error", "Failed to create the DB: " +
|
||||
"(" + err.name + ") " + err.message);
|
||||
});
|
||||
});
|
||||
|
||||
addMessageListener("deleteDB", function() {
|
||||
Promise.resolve().then(
|
||||
DataManager.delete
|
||||
).then(function onSuccess() {
|
||||
sendAsyncMessage("deleteDB.success");
|
||||
}, function onError(err) {
|
||||
sendAsyncMessage("deleteDB.error", "Failed to delete the DB:" + err.name);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user