You've already forked node-redis
mirror of
https://github.com/redis/node-redis.git
synced 2025-08-06 02:15:48 +03:00
Add example test with grunt and mocha.
Add test for reconnect. Run each test for both parsers and both IP versions. Don't save a reference to this nodified assertion function. Add DEBUG env var which enables extra debug logging in node_redis. Remove Grunt, run Redis on 6378 for non-interference. Remove the tests already ported to Mocha. Port reconnect after pubsub test; add subscribed after reconnect test. Reconnet after pubsub test confused me. I don't think it tested anything, and it didn't pass for me after I ported it. I've disabled it and added a note. In its place, I've added a test to make sure we are still subscribed and can receive pubsub messages after a reconnect. Move test suite config-like stuff into a library. Move test suite createClient args generation into the config library. WIP. Some select tests, most disabled and still WIP.
This commit is contained in:
committed by
Benjamin Coe
parent
6cae0b880f
commit
2b44245056
@@ -14,10 +14,12 @@
|
|||||||
"test": "nyc ./test/run.sh"
|
"test": "nyc ./test/run.sh"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"async": "^1.3.0",
|
||||||
"colors": "~0.6.0-1",
|
"colors": "~0.6.0-1",
|
||||||
"coveralls": "^2.11.2",
|
"coveralls": "^2.11.2",
|
||||||
"hiredis": "^0.4.0",
|
"hiredis": "^0.4.0",
|
||||||
"metrics": ">=0.1.5",
|
"metrics": ">=0.1.5",
|
||||||
|
"mocha": "^2.2.5",
|
||||||
"nyc": "^3.0.0",
|
"nyc": "^3.0.0",
|
||||||
"underscore": "~1.4.4"
|
"underscore": "~1.4.4"
|
||||||
},
|
},
|
||||||
|
28
run-bootstrapped-mocha.js
Normal file
28
run-bootstrapped-mocha.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
var pm = require('./test/lib/redis-process');
|
||||||
|
var cp = require('child_process');
|
||||||
|
var testSets = 'test/mocha/**/*.spec.js';
|
||||||
|
var async = require('async');
|
||||||
|
var redis;
|
||||||
|
|
||||||
|
process.on("exit", function () {
|
||||||
|
if (redis) {
|
||||||
|
redis.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async.series([function startRedis(next) {
|
||||||
|
redis = pm.start(function (err) {
|
||||||
|
next(err);
|
||||||
|
});
|
||||||
|
}, function runMocha(next) {
|
||||||
|
var mocha = cp.spawn('mocha', [testSets], {
|
||||||
|
stdio: "inherit"
|
||||||
|
});
|
||||||
|
mocha.on("exit", function (code) {
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
}, function stopRedis(next) {
|
||||||
|
redis.stop(next);
|
||||||
|
}], function (err) {
|
||||||
|
// done;
|
||||||
|
});
|
5
test/conf/redis.conf
Normal file
5
test/conf/redis.conf
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
daemonize yes
|
||||||
|
port 6378
|
||||||
|
bind ::1
|
||||||
|
unixsocket /tmp/redis.sock
|
||||||
|
unixsocketperm 755
|
30
test/lib/config.js
Normal file
30
test/lib/config.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
module.exports = (function () {
|
||||||
|
var redis = require('../../index');
|
||||||
|
redis.debug_mode = process.env.DEBUG ? JSON.parse(process.env.DEBUG) : false;
|
||||||
|
|
||||||
|
var config = {
|
||||||
|
redis: redis,
|
||||||
|
PORT: 6378,
|
||||||
|
HOST: {
|
||||||
|
IPv4: "127.0.0.1",
|
||||||
|
IPv6: "::1"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
config.configureClient = function (parser, ip, isSocket) {
|
||||||
|
var args = [];
|
||||||
|
|
||||||
|
if (!isSocket) {
|
||||||
|
args.push(config.PORT);
|
||||||
|
args.push(config.HOST[ip]);
|
||||||
|
args.push({ family: ip, parser: parser });
|
||||||
|
} else {
|
||||||
|
args.push(ip);
|
||||||
|
args.push({ parser: parser });
|
||||||
|
}
|
||||||
|
|
||||||
|
return args;
|
||||||
|
};
|
||||||
|
|
||||||
|
return config;
|
||||||
|
})();
|
60
test/lib/nodeify-assertions.js
Normal file
60
test/lib/nodeify-assertions.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
var assert = require('assert');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
isNumber: function (expected) {
|
||||||
|
return function (err, results) {
|
||||||
|
assert.strictEqual(null, err, "expected " + expected + ", got error: " + err);
|
||||||
|
assert.strictEqual(expected, results, expected + " !== " + results);
|
||||||
|
assert.strictEqual(typeof results, "number", "expected a number, got " + typeof results);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
isString: function (str) {
|
||||||
|
return function (err, results) {
|
||||||
|
assert.strictEqual(null, err, "expected string '" + str + "', got error: " + err);
|
||||||
|
assert.equal(str, results, str + " does not match " + results);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
isNull: function () {
|
||||||
|
return function (err, results) {
|
||||||
|
assert.strictEqual(null, err, "expected null, got error: " + err);
|
||||||
|
assert.strictEqual(null, results, results + " is not null");
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
isError: function () {
|
||||||
|
return function (err, results) {
|
||||||
|
assert.notEqual(err, null, "err is null, but an error is expected here.");
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
isNotError: function () {
|
||||||
|
return function (err, results) {
|
||||||
|
assert.strictEqual(err, null, "expected success, got an error: " + err);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
isType: {
|
||||||
|
number: function () {
|
||||||
|
return function (err, results) {
|
||||||
|
assert.strictEqual(null, err, "expected any number, got error: " + err);
|
||||||
|
assert.strictEqual(typeof results, "number", results + " is not a number");
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
positiveNumber: function () {
|
||||||
|
return function (err, results) {
|
||||||
|
assert.strictEqual(null, err, "expected positive number, got error: " + err);
|
||||||
|
assert.strictEqual(true, (results > 0), results + " is not a positive number");
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
23
test/lib/redis-process.js
Normal file
23
test/lib/redis-process.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
var cp = require('child_process');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
start: function (done, isSocket) {
|
||||||
|
var confFile = isSocket ? "test/conf/redis-socket.conf" : "test/conf/redis.conf";
|
||||||
|
var redis = cp.spawn("redis-server", [confFile]);
|
||||||
|
|
||||||
|
redis.once('err', done);
|
||||||
|
setTimeout(function (data) {
|
||||||
|
redis.removeListener('err', done);
|
||||||
|
done();
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return {
|
||||||
|
stop: function (done) {
|
||||||
|
redis.once("exit", function () {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
redis.kill("SIGINT");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
165
test/mocha/connecting.spec.js
Normal file
165
test/mocha/connecting.spec.js
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
var nodeAssert = require("../lib/nodeify-assertions");
|
||||||
|
var config = require("../lib/config");
|
||||||
|
var redis = config.redis;
|
||||||
|
var async = require("async");
|
||||||
|
|
||||||
|
describe("A node_redis client", function () {
|
||||||
|
function allTests(parser, ip, isSocket) {
|
||||||
|
var args = config.configureClient(parser, ip, isSocket);
|
||||||
|
|
||||||
|
describe("using " + parser + " and " + ip, function () {
|
||||||
|
var client;
|
||||||
|
|
||||||
|
after(function () {
|
||||||
|
client.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("connects correctly", function (done) {
|
||||||
|
client = redis.createClient.apply(redis.createClient, args);
|
||||||
|
client.on("error", done);
|
||||||
|
|
||||||
|
client.once("ready", function () {
|
||||||
|
client.removeListener("error", done);
|
||||||
|
client.get("recon 1", function (err, res) {
|
||||||
|
done(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when connected", function () {
|
||||||
|
var client;
|
||||||
|
|
||||||
|
beforeEach(function (done) {
|
||||||
|
client = redis.createClient.apply(redis.createClient, args);
|
||||||
|
client.once("error", function onError(err) {
|
||||||
|
done(err);
|
||||||
|
});
|
||||||
|
client.once("ready", function onReady() {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
client.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when redis closes unexpectedly", function () {
|
||||||
|
it("reconnects and can retrieve the pre-existing data", function (done) {
|
||||||
|
client.on("reconnecting", function on_recon(params) {
|
||||||
|
client.on("connect", function on_connect() {
|
||||||
|
async.parallel([function (cb) {
|
||||||
|
client.get("recon 1", function (err, res) {
|
||||||
|
nodeAssert.isString("one")(err, res);
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
}, function (cb) {
|
||||||
|
client.get("recon 1", function (err, res) {
|
||||||
|
nodeAssert.isString("one")(err, res);
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
}, function (cb) {
|
||||||
|
client.get("recon 2", function (err, res) {
|
||||||
|
nodeAssert.isString("two")(err, res);
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
}, function (cb) {
|
||||||
|
client.get("recon 2", function (err, res) {
|
||||||
|
nodeAssert.isString("two")(err, res);
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
}], function (err, results) {
|
||||||
|
client.removeListener("connect", on_connect);
|
||||||
|
client.removeListener("reconnecting", on_recon);
|
||||||
|
done(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
client.set("recon 1", "one");
|
||||||
|
client.set("recon 2", "two", function (err, res) {
|
||||||
|
// Do not do this in normal programs. This is to simulate the server closing on us.
|
||||||
|
// For orderly shutdown in normal programs, do client.quit()
|
||||||
|
client.stream.destroy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("and it's subscribed to a channel", function () {
|
||||||
|
// reconnect_select_db_after_pubsub
|
||||||
|
// Does not pass.
|
||||||
|
// "Connection in subscriber mode, only subscriber commands may be used"
|
||||||
|
xit("reconnects, unsubscribes, and can retrieve the pre-existing data", function (done) {
|
||||||
|
client.on("reconnecting", function on_recon(params) {
|
||||||
|
client.on("ready", function on_connect() {
|
||||||
|
async.parallel([function (cb) {
|
||||||
|
client.unsubscribe("recon channel", function (err, res) {
|
||||||
|
nodeAssert.isNotError()(err, res);
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
}, function (cb) {
|
||||||
|
client.get("recon 1", function (err, res) {
|
||||||
|
nodeAssert.isString("one")(err, res);
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
}], function (err, results) {
|
||||||
|
client.removeListener("connect", on_connect);
|
||||||
|
client.removeListener("reconnecting", on_recon);
|
||||||
|
done(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
client.set("recon 1", "one");
|
||||||
|
client.subscribe("recon channel", function (err, res) {
|
||||||
|
// Do not do this in normal programs. This is to simulate the server closing on us.
|
||||||
|
// For orderly shutdown in normal programs, do client.quit()
|
||||||
|
client.stream.destroy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("remains subscribed", function () {
|
||||||
|
var client2 = redis.createClient.apply(redis.createClient, args);
|
||||||
|
|
||||||
|
client.on("reconnecting", function on_recon(params) {
|
||||||
|
client.on("ready", function on_connect() {
|
||||||
|
async.parallel([function (cb) {
|
||||||
|
client.on("message", function (channel, message) {
|
||||||
|
try {
|
||||||
|
nodeAssert.isString("recon channel")(null, channel);
|
||||||
|
nodeAssert.isString("a test message")(null, message);
|
||||||
|
} catch (err) {
|
||||||
|
cb(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
client2.subscribe("recon channel", function (err, res) {
|
||||||
|
if (err) {
|
||||||
|
cb(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
client2.publish("recon channel", "a test message");
|
||||||
|
});
|
||||||
|
}], function (err, results) {
|
||||||
|
done(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
client.subscribe("recon channel", function (err, res) {
|
||||||
|
// Do not do this in normal programs. This is to simulate the server closing on us.
|
||||||
|
// For orderly shutdown in normal programs, do client.quit()
|
||||||
|
client.stream.destroy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
['javascript', 'hiredis'].forEach(function (parser) {
|
||||||
|
allTests(parser, "/tmp/redis.sock", true);
|
||||||
|
['IPv4', 'IPv6'].forEach(function (ip) {
|
||||||
|
allTests(parser, ip);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
130
test/mocha/select.spec.js
Normal file
130
test/mocha/select.spec.js
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
var nodeAssert = require("../lib/nodeify-assertions");
|
||||||
|
var config = require("../lib/config");
|
||||||
|
var redis = config.redis;
|
||||||
|
var async = require("async");
|
||||||
|
var assert = require("assert");
|
||||||
|
|
||||||
|
describe("The 'select' method", function () {
|
||||||
|
function allTests(parser, ip, isSocket) {
|
||||||
|
var args = config.configureClient(parser, ip, isSocket);
|
||||||
|
|
||||||
|
describe("using " + parser + " and " + ip, function () {
|
||||||
|
describe("when not connected", function () {
|
||||||
|
var client;
|
||||||
|
|
||||||
|
beforeEach(function (done) {
|
||||||
|
client = redis.createClient.apply(redis.createClient, args);
|
||||||
|
client.once("error", done);
|
||||||
|
|
||||||
|
client.once("connect", function () {
|
||||||
|
client.set("doot", "good calsum", function (err, res) {
|
||||||
|
client.end();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("doesn't even throw an error or call the callback at all WTF", function (done) {
|
||||||
|
this.timeout(50);
|
||||||
|
|
||||||
|
client.select(1, function (err, res) {
|
||||||
|
nodeAssert.isNotError()(err, res);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
done();
|
||||||
|
}, 45);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when connected", function () {
|
||||||
|
var client;
|
||||||
|
|
||||||
|
beforeEach(function (done) {
|
||||||
|
client = redis.createClient.apply(redis.createClient, args);
|
||||||
|
client.once("error", done);
|
||||||
|
|
||||||
|
client.once("connect", function () {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
client.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("changes the database and calls the callback", function (done) {
|
||||||
|
// default value of null means database 0 will be used.
|
||||||
|
assert.strictEqual(client.selected_db, null, "default db should be null");
|
||||||
|
client.select(1, function (err, res) {
|
||||||
|
nodeAssert.isNotError()(err, res);
|
||||||
|
assert.strictEqual(client.selected_db, 1, "db should be 1 after select");
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("and no callback is specified", function () {
|
||||||
|
// select_error_emits_if_no_callback
|
||||||
|
// this is another test that was testing the wrong thing. The old test did indeed emit an error,
|
||||||
|
// but not because of the lacking callback, but because 9999 was an invalid db index.
|
||||||
|
describe("with a valid db index", function () {
|
||||||
|
it("works just fine and does not actually emit an error like the old tests assert WTF", function (done) {
|
||||||
|
assert.strictEqual(client.selected_db, null, "default db should be null");
|
||||||
|
this.timeout(50);
|
||||||
|
client.on("error", function (err) {
|
||||||
|
nodeAssert.isNotError()(err);
|
||||||
|
assert.strictEqual(client.selected_db, 1, "db should be 1 after select");
|
||||||
|
done(new Error("the old tests were crap"));
|
||||||
|
});
|
||||||
|
client.select(1);
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
done();
|
||||||
|
}, 45);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Can't seem to catch the errors thrown here.
|
||||||
|
xdescribe("with an invalid db index", function () {
|
||||||
|
it("emits an error", function (done) {
|
||||||
|
this.timeout(50);
|
||||||
|
|
||||||
|
assert.strictEqual(client.selected_db, null, "default db should be null");
|
||||||
|
client.on("error", function (err) {
|
||||||
|
console.log('got an error', err);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
client.select(9999);
|
||||||
|
} catch (err) {}
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
done(new Error("It was supposed to emit an error."));
|
||||||
|
}, 45);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws an error bc a callback is not", function (done) {
|
||||||
|
assert.strictEqual(client.selected_db, null, "default db should be null");
|
||||||
|
try {
|
||||||
|
client.select(9999);
|
||||||
|
done(new Error("Was supposed to throw an invalid db index error."));
|
||||||
|
} catch (err) {
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
['javascript', 'hiredis'].forEach(function (parser) {
|
||||||
|
//allTests(parser, "/tmp/redis.sock", true);
|
||||||
|
//['IPv4', 'IPv6'].forEach(function (ip) {
|
||||||
|
['IPv4'].forEach(function (ip) {
|
||||||
|
allTests(parser, ip);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
46
test/test.js
46
test/test.js
@@ -117,6 +117,7 @@ next = function next(name) {
|
|||||||
|
|
||||||
// Tests are run in the order they are defined, so FLUSHDB should always be first.
|
// Tests are run in the order they are defined, so FLUSHDB should always be first.
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
tests.IPV4 = function () {
|
tests.IPV4 = function () {
|
||||||
var ipv4addr = process.env.REDIS_PORT_6379_TCP_ADDR || "127.0.0.1";
|
var ipv4addr = process.env.REDIS_PORT_6379_TCP_ADDR || "127.0.0.1";
|
||||||
var ipv4Client = redis.createClient( PORT, ipv4addr, { family : "IPv4", parser: parser } );
|
var ipv4Client = redis.createClient( PORT, ipv4addr, { family : "IPv4", parser: parser } );
|
||||||
@@ -937,51 +938,6 @@ tests.socket_nodelay = function () {
|
|||||||
c3.on("ready", ready_check);
|
c3.on("ready", ready_check);
|
||||||
};
|
};
|
||||||
|
|
||||||
tests.reconnect = function () {
|
|
||||||
var name = "reconnect";
|
|
||||||
|
|
||||||
client.set("recon 1", "one");
|
|
||||||
client.set("recon 2", "two", function (err, res) {
|
|
||||||
// Do not do this in normal programs. This is to simulate the server closing on us.
|
|
||||||
// For orderly shutdown in normal programs, do client.quit()
|
|
||||||
client.stream.destroy();
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on("reconnecting", function on_recon(params) {
|
|
||||||
client.on("connect", function on_connect() {
|
|
||||||
client.select(test_db_num, require_string("OK", name));
|
|
||||||
client.get("recon 1", require_string("one", name));
|
|
||||||
client.get("recon 1", require_string("one", name));
|
|
||||||
client.get("recon 2", require_string("two", name));
|
|
||||||
client.get("recon 2", require_string("two", name));
|
|
||||||
client.removeListener("connect", on_connect);
|
|
||||||
client.removeListener("reconnecting", on_recon);
|
|
||||||
next(name);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
tests.reconnect_select_db_after_pubsub = function() {
|
|
||||||
var name = "reconnect_select_db_after_pubsub";
|
|
||||||
|
|
||||||
client.select(test_db_num);
|
|
||||||
client.set(name, "one");
|
|
||||||
client.subscribe('ChannelV', function (err, res) {
|
|
||||||
client.stream.destroy();
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on("reconnecting", function on_recon(params) {
|
|
||||||
client.on("ready", function on_connect() {
|
|
||||||
client.unsubscribe('ChannelV', function (err, res) {
|
|
||||||
client.get(name, require_string("one", name));
|
|
||||||
client.removeListener("connect", on_connect);
|
|
||||||
client.removeListener("reconnecting", on_recon);
|
|
||||||
next(name);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
tests.select_error_emits_if_no_callback = function () {
|
tests.select_error_emits_if_no_callback = function () {
|
||||||
var prev = client.listeners("error")[0];
|
var prev = client.listeners("error")[0];
|
||||||
client.removeListener("error", prev);
|
client.removeListener("error", prev);
|
||||||
|
Reference in New Issue
Block a user