Allow configuring multiple default networks

This commit is contained in:
Reto Brunner 2022-10-30 21:19:09 +01:00
commit 55b3eef60e
10 changed files with 626 additions and 493 deletions

View file

@ -20,6 +20,20 @@
</h1>
<template v-if="!config?.lockNetwork">
<h2>Network settings</h2>
<template v-if="config.defaults.length > 0">
<div class="connect-row">
<label for="connect:presetName">Preset</label>
<select id="connect:presetName" v-model="presetName" name="presetName">
<option
v-for="defaultNetwork in config.defaults"
:key="defaultNetwork.name"
:value="defaultNetwork.name"
>
{{ defaultNetwork.name }}
</option>
</select>
</div>
</template>
<div class="connect-row">
<label for="connect:name">Name</label>
<input
@ -175,35 +189,48 @@
</div>
</template>
</template>
<template v-else-if="config.lockNetwork && !store.state.serverConfiguration?.public">
<h2>Network settings</h2>
<div class="connect-row">
<label for="connect:name">Name</label>
<input
id="connect:name"
v-model.trim="defaults.name"
class="input"
name="name"
maxlength="100"
/>
</div>
<div class="connect-row">
<label for="connect:password">Password</label>
<RevealPassword
v-slot:default="slotProps"
class="input-wrap password-container"
>
<input
id="connect:password"
v-model="defaults.password"
class="input"
:type="slotProps.isVisible ? 'text' : 'password'"
placeholder="Server password (optional)"
name="password"
maxlength="300"
/>
</RevealPassword>
</div>
<template v-else-if="config.lockNetwork">
<template
v-if="
$store.state.serverConfiguration.defaults.length > 1 ||
!$store.state.serverConfiguration.public
"
>
<h2>Network settings</h2>
</template>
<template v-if="$store.state.serverConfiguration.defaults.length > 1">
<div class="connect-row">
<label for="connect:name">Network</label>
<select id="connect:name" v-model="defaults.name" name="name">
<option
v-for="defaultNetwork in config.defaults"
:key="defaultNetwork.name"
:value="defaultNetwork.name"
>
{{ defaultNetwork.name }}
</option>
</select>
</div>
</template>
<template v-if="!$store.state.serverConfiguration.public">
<div class="connect-row">
<label for="connect:password">Password</label>
<RevealPassword
v-slot:default="slotProps"
class="input-wrap password-container"
>
<input
id="connect:password"
v-model="defaults.password"
class="input"
:type="slotProps.isVisible ? 'text' : 'password'"
placeholder="Server password (optional)"
name="password"
maxlength="300"
/>
</RevealPassword>
</div>
</template>
</template>
<h2>User preferences</h2>
@ -470,6 +497,7 @@ export default defineComponent({
const config = ref(store.state.serverConfiguration);
const previousUsername = ref(props.defaults?.username);
const displayPasswordField = ref(false);
const presetName = ref(store.state.serverConfiguration.defaults[0]?.name);
const publicPassword = ref<HTMLInputElement | null>(null);
@ -481,6 +509,18 @@ export default defineComponent({
}
});
watch(presetName, (newValue) => {
const defaults = store.state.serverConfiguration.defaults.find(
(def) => def.name === newValue
);
if (!defaults) {
return;
}
Object.assign(props.defaults, defaults);
});
const commandsInput = ref<HTMLInputElement | null>(null);
const resizeCommandsInput = () => {
@ -557,6 +597,7 @@ export default defineComponent({
store,
config,
displayPasswordField,
presetName,
publicPassword,
commandsInput,
resizeCommandsInput,

View file

@ -49,7 +49,7 @@ export default defineComponent({
if (
!Object.prototype.hasOwnProperty.call(
store.state.serverConfiguration?.defaults,
store.state.serverConfiguration?.defaults[0],
key
)
) {
@ -57,11 +57,15 @@ export default defineComponent({
}
// When the network is locked, URL overrides should not affect disabled fields
if (
store.state.serverConfiguration?.lockNetwork &&
["name", "host", "port", "tls", "rejectUnauthorized"].includes(key)
) {
continue;
if (store.state.serverConfiguration?.lockNetwork) {
if (["host", "port", "tls", "rejectUnauthorized"].includes(key)) {
continue;
}
// Network name is only disabled if there is a single network
if (key === "name" && store.state.serverConfiguration?.defaults.length < 2) {
continue;
}
}
if (key === "join") {
@ -78,7 +82,7 @@ export default defineComponent({
}
// Override server provided defaults with parameters passed in the URL if they match the data type
switch (typeof store.state.serverConfiguration?.defaults[key]) {
switch (typeof store.state.serverConfiguration?.defaults[0][key]) {
case "boolean":
if (value === "0" || value === "false") {
parsedParams[key] = false;
@ -102,7 +106,7 @@ export default defineComponent({
const defaults = ref<Partial<NetworkFormDefaults>>(
Object.assign(
{},
store.state.serverConfiguration?.defaults,
store.state.serverConfiguration?.defaults[0],
parseOverrideParams(props.queryParams)
)
);

View file

@ -218,14 +218,14 @@ module.exports = {
// default.
leaveMessage: "The Lounge - https://thelounge.chat",
// ## Default network
// ## Default networks
// ### `defaults`
//
// Specifies default network information that will be used as placeholder
// values in the *Connect* window.
// Specifies a list of default network information that will be used as
// placeholder values in the *Connect* window.
//
// The available keys for the `defaults` object are:
// The available keys for the individual items are:
//
// - `name`: Name to display in the channel list of The Lounge. This value is
// not forwarded to the IRC network.
@ -249,37 +249,41 @@ module.exports = {
// Libera.Chat by default:
//
// ```js
// defaults: {
// name: "Libera.Chat",
// host: "irc.libera.chat",
// port: 6697,
// password: "",
// tls: true,
// rejectUnauthorized: true,
// nick: "thelounge%%",
// username: "thelounge",
// realname: "The Lounge User",
// join: "#thelounge"
// }
// defaults: [
// {
// name: "Libera.Chat",
// host: "irc.libera.chat",
// port: 6697,
// password: "",
// tls: true,
// rejectUnauthorized: true,
// nick: "thelounge%%",
// username: "thelounge",
// realname: "The Lounge User",
// join: "#thelounge"
// },
// ]
// ```
defaults: {
name: "Libera.Chat",
host: "irc.libera.chat",
port: 6697,
password: "",
tls: true,
rejectUnauthorized: true,
nick: "thelounge%%",
username: "thelounge",
realname: "",
join: "#thelounge",
leaveMessage: "",
},
defaults: [
{
name: "Libera.Chat",
host: "irc.libera.chat",
port: 6697,
password: "",
tls: true,
rejectUnauthorized: true,
nick: "thelounge%%",
username: "thelounge",
realname: "",
join: "#thelounge",
leaveMessage: "",
},
],
// ### `lockNetwork`
//
// When set to `true`, users will not be able to modify host, port and TLS
// settings and will be limited to the configured network.
// settings and will be limited to the configured networks.
// These fields will also be hidden from the UI.
//
// This value is set to `false` by default.

View file

@ -290,12 +290,15 @@ class Client {
});
}
// BUG: this isn't the name of the network, but the username
const defaultNetwork =
Config.values.defaults.find((network) => this.name === network.name) ||
Config.values.defaults[0];
// TODO; better typing for args
const network = new Network({
uuid: args.uuid,
name: String(
args.name || (Config.values.lockNetwork ? Config.values.defaults.name : "") || ""
),
name: String(args.name || (Config.values.lockNetwork ? defaultNetwork.name : "") || ""),
host: String(args.host || ""),
port: parseInt(String(args.port), 10),
tls: !!args.tls,

View file

@ -94,7 +94,7 @@ export type ConfigType = {
fileUpload: FileUpload;
transports: string[];
leaveMessage: string;
defaults: Defaults;
defaults: Defaults[];
lockNetwork: boolean;
messageStorage: string[];
useHexIp: boolean;
@ -152,14 +152,16 @@ class Config {
return path.join(this.getPackagesPath(), "node_modules", packageName);
}
getDefaultNick() {
if (!this.values.defaults.nick) {
getDefaultNickForNetwork(networkName: string) {
const defaultNick = this.values.defaults.find(
(network) => network.name === networkName
)?.nick;
if (!defaultNick) {
return "thelounge";
}
return this.values.defaults.nick.replace(/%/g, () =>
Math.floor(Math.random() * 10).toString()
);
return defaultNick.replace(/%/g, () => Math.floor(Math.random() * 10).toString());
}
merge(newConfig: ConfigType) {
@ -178,17 +180,38 @@ class Config {
return _.mergeWith(oldConfig, newConfig, (objValue, srcValue, key) => {
// Do not override config variables if the type is incorrect (e.g. object changed into a string)
// note that Arrays, Objects, Classes et al all have an 'object' type and can't be differentiated
if (
typeof objValue !== "undefined" &&
objValue !== null &&
typeof objValue !== typeof srcValue
) {
log.warn(`Incorrect type for "${colors.bold(key)}", please verify your config.`);
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return objValue;
}
if (key === "defaults") {
if (srcValue === undefined || srcValue === null) {
return [];
}
log.warn(
`Configuration key "${colors.bold(
key
)}" should be an array of networks. Support for the old object format will be removed in a future version, please update your config. https://thelounge.chat/docs/configuration#default-networks`
);
// we assume that the user provided value is actually valid... this might be a mistake
if (Array.isArray(srcValue)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return srcValue;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return [srcValue];
}
// For arrays, simply override the value with user provided one.
if (_.isArray(objValue)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return

View file

@ -182,7 +182,7 @@ class Network {
// Remove new lines and limit length
const cleanString = (str: string) => str.replace(/[\x00\r\n]/g, "").substring(0, 300);
this.setNick(cleanNick(String(this.nick || Config.getDefaultNick())));
this.setNick(cleanNick(String(this.nick || Config.getDefaultNickForNetwork(this.name))));
if (!this.username) {
// If username is empty, make one from the provided nick
@ -224,27 +224,38 @@ class Network {
}
if (Config.values.lockNetwork) {
// Get the first configured network that matches this one, if any.
let defaultNetwork = Config.values.defaults.find(
(network) => this.name === network.name
);
// BUG: This should probably be an error, not just a silent disregard of the value
// Otherwise, default to the first configured
if (defaultNetwork === undefined) {
defaultNetwork = Config.values.defaults[0];
}
// This check is needed to prevent invalid user configurations
if (
!Config.values.public &&
this.host &&
this.host.length > 0 &&
this.host !== Config.values.defaults.host
defaultNetwork === undefined
) {
error(this, `The hostname you specified (${this.host}) is not allowed.`);
return false;
}
if (Config.values.public) {
this.name = Config.values.defaults.name;
this.name = defaultNetwork.name;
// Sync lobby channel name
this.channels[0].name = Config.values.defaults.name;
this.channels[0].name = defaultNetwork.name;
}
this.host = Config.values.defaults.host;
this.port = Config.values.defaults.port;
this.tls = Config.values.defaults.tls;
this.rejectUnauthorized = Config.values.defaults.rejectUnauthorized;
this.host = defaultNetwork.host;
this.port = defaultNetwork.port;
this.tls = defaultNetwork.tls;
this.rejectUnauthorized = defaultNetwork.rejectUnauthorized;
}
if (this.host.length === 0) {

View file

@ -83,7 +83,7 @@ export default <IrcEventHandler>function (irc, network) {
lobby.pushMessage(client, msg, true);
if (irc.connection.registered === false) {
irc.changeNick(Config.getDefaultNick());
irc.changeNick(Config.getDefaultNickForNetwork(network.name));
}
client.emit("nick", {

View file

@ -58,7 +58,6 @@ export type ClientConfiguration = Pick<
gitCommit: string | null;
defaultTheme: string;
themes: ThemeForClient[];
defaults: Defaults;
fileUploadMaxFileSize?: number;
};
@ -872,14 +871,17 @@ function getClientConfiguration(): ClientConfiguration {
config.defaults = _.clone(Config.values.defaults);
} else {
// Only send defaults that are visible on the client
config.defaults = _.pick(Config.values.defaults, [
"name",
"nick",
"username",
"password",
"realname",
"join",
]) as Defaults;
config.defaults = Config.values.defaults.map(
(network) =>
_.pick(network, [
"name",
"nick",
"username",
"password",
"realname",
"join",
]) as Defaults
);
}
config.isUpdateAvailable = changelog.isUpdateAvailable;
@ -888,10 +890,12 @@ function getClientConfiguration(): ClientConfiguration {
config.gitCommit = Helper.getGitCommit();
config.themes = themes.getAll();
config.defaultTheme = Config.values.theme;
config.defaults.nick = Config.getDefaultNick();
config.defaults.sasl = "";
config.defaults.saslAccount = "";
config.defaults.saslPassword = "";
config.defaults.forEach((network) => {
network.nick = Config.getDefaultNickForNetwork(network.name);
network.sasl = "";
network.saslAccount = "";
network.saslPassword = "";
});
if (Uploader) {
config.fileUploadMaxFileSize = Uploader.getMaxFileSize();

View file

@ -2,8 +2,8 @@
import config from "../../../defaults/config.js";
config.defaults.name = "Example IRC Server";
config.defaults.host = "irc.example.com";
config.defaults[0].name = "Example IRC Server";
config.defaults[0].host = "irc.example.com";
config.public = true;
config.prefetch = true;
// @ts-ignore

View file

@ -134,433 +134,476 @@ describe("Network", function () {
});
});
describe("#validate()", function () {
it("should set correct defaults", function () {
Config.values.defaults.nick = "";
it("should set correct defaults", function () {
Config.values.defaults[0].nick = "";
const network = new Network({
host: "localhost",
});
expect(network.validate({} as any)).to.be.true;
expect(network.nick).to.equal("thelounge");
expect(network.username).to.equal("thelounge");
expect(network.realname).to.equal("thelounge");
expect(network.port).to.equal(6667);
const network2 = new Network({
host: "localhost",
nick: "@Invalid Nick?",
});
expect(network2.validate({} as any)).to.be.true;
expect(network2.username).to.equal("InvalidNick");
const network = new Network({
host: "localhost",
});
it("should enforce lockNetwork", function () {
Config.values.lockNetwork = true;
expect(network.validate({} as any)).to.be.true;
expect(network.nick).to.equal("thelounge");
expect(network.username).to.equal("thelounge");
expect(network.realname).to.equal("thelounge");
expect(network.port).to.equal(6667);
// Make sure we lock in private mode
Config.values.public = false;
const network = new Network({
host: "",
port: 1337,
tls: false,
rejectUnauthorized: false,
});
expect(network.validate({} as any)).to.be.true;
expect(network.host).to.equal("irc.example.com");
expect(network.port).to.equal(6697);
expect(network.tls).to.be.true;
expect(network.rejectUnauthorized).to.be.true;
// Make sure we lock in public mode (also resets public=true for other tests)
Config.values.public = true;
const network2 = new Network({
host: "some.fake.tld",
});
expect(network2.validate({} as any)).to.be.true;
expect(network2.host).to.equal("irc.example.com");
Config.values.lockNetwork = false;
});
it("realname should be set to nick only if realname is empty", function () {
const network = new Network({
host: "localhost",
nick: "dummy",
});
expect(network.validate({} as any)).to.be.true;
expect(network.nick).to.equal("dummy");
expect(network.realname).to.equal("dummy");
const network2 = new Network({
host: "localhost",
nick: "dummy",
realname: "notdummy",
});
expect(network2.validate({} as any)).to.be.true;
expect(network2.nick).to.equal("dummy");
expect(network2.realname).to.equal("notdummy");
});
it("should apply STS policies iff they match", function () {
const client = {idMsg: 1, emit() {}} as any;
STSPolicies.update("irc.example.com", 7000, 3600);
expect(STSPolicies.get("irc.example.com")).to.not.be.null;
let network = new Network({
host: "irc.example.com",
port: 1337,
tls: false,
});
expect(network.validate(client)).to.be.true;
expect(network.port).to.equal(7000);
expect(network.tls).to.be.true;
network = new Network({
host: "irc2.example.com",
port: 1337,
tls: false,
});
expect(network.validate(client)).to.be.true;
expect(network.port).to.equal(1337);
expect(network.tls).to.be.false;
STSPolicies.update("irc.example.com", 7000, 0); // Cleanup
expect(STSPolicies.get("irc.example.com")).to.be.null;
});
it("should not remove client certs if TLS is disabled", function () {
Config.values.public = false;
const client = {idMsg: 1, emit() {}, messageStorage: []};
const network = new Network({host: "irc.example.com", sasl: "external"});
(network as any).createIrcFramework(client);
expect(network.irc).to.not.be.null;
const client_cert = network.irc?.options?.client_certificate;
expect(client_cert).to.not.be.null;
expect(ClientCertificate.get(network.uuid)).to.deep.equal(client_cert);
expect(network.validate(client as any)).to.be.true;
expect(ClientCertificate.get(network.uuid)).to.deep.equal(client_cert); // Should be unchanged
ClientCertificate.remove(network.uuid);
Config.values.public = true;
});
it("should not remove client certs if there is a STS policy", function () {
Config.values.public = false;
const client = {idMsg: 1, emit() {}, messageStorage: []};
STSPolicies.update("irc.example.com", 7000, 3600);
expect(STSPolicies.get("irc.example.com")).to.not.be.null;
const network = new Network({host: "irc.example.com", sasl: "external"});
(network as any).createIrcFramework(client);
expect(network.irc).to.not.be.null;
const client_cert = network.irc?.options?.client_certificate;
expect(client_cert).to.not.be.null;
expect(ClientCertificate.get(network.uuid)).to.deep.equal(client_cert);
expect(network.validate(client as any)).to.be.true;
expect(ClientCertificate.get(network.uuid)).to.deep.equal(client_cert); // Should be unchanged
ClientCertificate.remove(network.uuid);
Config.values.public = true;
STSPolicies.update("irc.example.com", 7000, 0); // Cleanup
expect(STSPolicies.get("irc.example.com")).to.be.null;
const network2 = new Network({
host: "localhost",
nick: "@Invalid Nick?",
});
expect(network2.validate({} as any)).to.be.true;
expect(network2.username).to.equal("InvalidNick");
});
describe("#createIrcFramework(client)", function () {
it("should generate and use a client certificate when using SASL external", function () {
Config.values.public = false;
it("should enforce lockNetwork", function () {
Config.values.lockNetwork = true;
const client = {idMsg: 1, emit() {}};
STSPolicies.update("irc.example.com", 7000, 3600);
expect(STSPolicies.get("irc.example.com")).to.not.be.null;
// Make sure we lock in private mode
Config.values.public = false;
let network: any = new Network({host: "irc.example.com"});
network.createIrcFramework(client);
expect(network.irc).to.not.be.null;
expect(network.irc.options.client_certificate).to.be.null;
network = new Network({host: "irc.example.com", sasl: "external"});
network.createIrcFramework(client);
expect(network.irc).to.not.be.null;
expect(network.irc.options.client_certificate).to.not.be.null;
ClientCertificate.remove(network.uuid);
Config.values.public = true;
STSPolicies.update("irc.example.com", 7000, 0); // Cleanup
expect(STSPolicies.get("irc.example.com")).to.be.null;
const network = new Network({
host: "",
port: 1337,
tls: false,
rejectUnauthorized: false,
});
expect(network.validate({} as any)).to.be.true;
expect(network.host).to.equal("irc.example.com");
expect(network.port).to.equal(6697);
expect(network.tls).to.be.true;
expect(network.rejectUnauthorized).to.be.true;
// Make sure we lock in public mode (also resets public=true for other tests)
Config.values.public = true;
const network2 = new Network({
host: "some.fake.tld",
});
expect(network2.validate({} as any)).to.be.true;
expect(network2.host).to.equal("irc.example.com");
Config.values.lockNetwork = false;
});
describe("#edit(client, args)", function () {
it("should enforce correct types", function () {
let saveCalled = false;
let nameEmitCalled = false;
const network = new Network();
(network as any).edit(
{
emit(name, data) {
if (name === "network:name") {
nameEmitCalled = true;
expect(data.uuid).to.equal(network.uuid);
expect(data.name).to.equal("Lounge Test Network");
}
},
save() {
saveCalled = true;
},
},
{
nick: "newNick",
host: "new.tld",
name: "Lounge Test Network",
port: "1337",
tls: undefined,
rejectUnauthorized: undefined,
username: 1234,
password: 4567,
realname: 8901,
sasl: "something",
saslAccount: 1337,
saslPassword: 1337,
commands: "/command 1 2 3\r\n/ping HELLO\r\r\r\r/whois test\r\n\r\n",
ip: "newIp",
hostname: "newHostname",
uuid: "newuuid",
}
);
expect(saveCalled).to.be.true;
expect(nameEmitCalled).to.be.true;
expect(network.uuid).to.not.equal("newuuid");
// @ts-expect-error Property 'ip' does not exist on type 'Network'.
expect(network.ip).to.be.undefined;
// @ts-expect-error Property 'hostname' does not exist on type 'Network'.
expect(network.hostname).to.be.undefined;
expect(network.name).to.equal("Lounge Test Network");
expect(network.channels[0].name).to.equal("Lounge Test Network");
expect(network.nick).to.equal("newNick");
expect(network.host).to.equal("new.tld");
expect(network.port).to.equal(1337);
expect(network.tls).to.be.false;
expect(network.rejectUnauthorized).to.be.false;
expect(network.username).to.equal("1234");
expect(network.password).to.equal("4567");
expect(network.realname).to.equal("8901");
expect(network.sasl).to.equal("");
expect(network.saslAccount).to.equal("1337");
expect(network.saslPassword).to.equal("1337");
expect(network.commands).to.deep.equal([
"/command 1 2 3",
"/ping HELLO",
"/whois test",
]);
it("realname should be set to nick only if realname is empty", function () {
const network = new Network({
host: "localhost",
nick: "dummy",
});
expect(network.validate({} as any)).to.be.true;
expect(network.nick).to.equal("dummy");
expect(network.realname).to.equal("dummy");
const network2 = new Network({
host: "localhost",
nick: "dummy",
realname: "notdummy",
});
expect(network2.validate({} as any)).to.be.true;
expect(network2.nick).to.equal("dummy");
expect(network2.realname).to.equal("notdummy");
});
describe("#getFilteredClone(lastActiveChannel, lastMessage)", function () {
it("should filter channels", function () {
const chan = new Chan();
chan.setUser(new User({nick: "test"}));
it("should apply STS policies iff they match", function () {
const client = {idMsg: 1, emit() {}} as any;
STSPolicies.update("irc.example.com", 7000, 3600);
expect(STSPolicies.get("irc.example.com")).to.not.be.null;
const network = new Network({
channels: [chan],
});
expect(network.channels[0].users).to.be.empty;
let network = new Network({
host: "irc.example.com",
port: 1337,
tls: false,
});
it("should keep necessary properties", function () {
const network = new Network();
const clone = network.getFilteredClone();
expect(network.validate(client)).to.be.true;
expect(network.port).to.equal(7000);
expect(network.tls).to.be.true;
expect(clone)
.to.be.an("object")
.that.has.all.keys("channels", "status", "nick", "name", "serverOptions", "uuid");
expect(clone.status).to.be.an("object").that.has.all.keys("connected", "secure");
network = new Network({
host: "irc2.example.com",
port: 1337,
tls: false,
});
expect(network.validate(client)).to.be.true;
expect(network.port).to.equal(1337);
expect(network.tls).to.be.false;
STSPolicies.update("irc.example.com", 7000, 0); // Cleanup
expect(STSPolicies.get("irc.example.com")).to.be.null;
});
describe("#addChannel(newChan)", function () {
it("should add channel", function () {
const chan = new Chan({name: "#thelounge"});
it("should not remove client certs if TLS is disabled", function () {
Config.values.public = false;
const network = new Network({
channels: [chan],
});
// Lobby and initial channel
expect(network.channels.length).to.equal(2);
const client = {idMsg: 1, emit() {}, messageStorage: []};
const newChan = new Chan({name: "#foo"});
network.addChannel(newChan);
const network = new Network({host: "irc.example.com", sasl: "external"});
(network as any).createIrcFramework(client);
expect(network.irc).to.not.be.null;
expect(network.channels.length).to.equal(3);
});
const client_cert = network.irc?.options?.client_certificate;
expect(client_cert).to.not.be.null;
expect(ClientCertificate.get(network.uuid)).to.deep.equal(client_cert);
it("should add channel alphabetically", function () {
const chan1 = new Chan({name: "#abc"});
const chan2 = new Chan({name: "#thelounge"});
const chan3 = new Chan({name: "#zero"});
expect(network.validate(client as any)).to.be.true;
const network = new Network({
channels: [chan1, chan2, chan3],
name: "foo",
});
expect(ClientCertificate.get(network.uuid)).to.deep.equal(client_cert); // Should be unchanged
const newChan = new Chan({name: "#foo"});
network.addChannel(newChan);
ClientCertificate.remove(network.uuid);
Config.values.public = true;
});
expect(network.channels[0].name).to.equal("foo");
expect(network.channels[1]).to.equal(chan1);
expect(network.channels[2]).to.equal(newChan);
expect(network.channels[3]).to.equal(chan2);
expect(network.channels[4]).to.equal(chan3);
});
it("should not remove client certs if there is a STS policy", function () {
Config.values.public = false;
it("should sort case-insensitively", function () {
const chan1 = new Chan({name: "#abc"});
const chan2 = new Chan({name: "#THELOUNGE"});
const client = {idMsg: 1, emit() {}, messageStorage: []};
STSPolicies.update("irc.example.com", 7000, 3600);
expect(STSPolicies.get("irc.example.com")).to.not.be.null;
const network = new Network({
channels: [chan1, chan2],
});
const network = new Network({host: "irc.example.com", sasl: "external"});
(network as any).createIrcFramework(client);
expect(network.irc).to.not.be.null;
const newChan = new Chan({name: "#foo"});
network.addChannel(newChan);
const client_cert = network.irc?.options?.client_certificate;
expect(client_cert).to.not.be.null;
expect(ClientCertificate.get(network.uuid)).to.deep.equal(client_cert);
expect(network.channels[1]).to.equal(chan1);
expect(network.channels[2]).to.equal(newChan);
expect(network.channels[3]).to.equal(chan2);
});
expect(network.validate(client as any)).to.be.true;
it("should sort users separately from channels", function () {
const chan1 = new Chan({name: "#abc"});
const chan2 = new Chan({name: "#THELOUNGE"});
expect(ClientCertificate.get(network.uuid)).to.deep.equal(client_cert); // Should be unchanged
const network = new Network({
channels: [chan1, chan2],
});
ClientCertificate.remove(network.uuid);
Config.values.public = true;
const newUser = new Chan({name: "mcinkay", type: ChanType.QUERY});
network.addChannel(newUser);
expect(network.channels[1]).to.equal(chan1);
expect(network.channels[2]).to.equal(chan2);
expect(network.channels[3]).to.equal(newUser);
});
it("should sort users alphabetically", function () {
const chan1 = new Chan({name: "#abc"});
const chan2 = new Chan({name: "#THELOUNGE"});
const user1 = new Chan({name: "astorije", type: ChanType.QUERY});
const user2 = new Chan({name: "xpaw", type: ChanType.QUERY});
const network = new Network({
channels: [chan1, chan2, user1, user2],
});
const newUser = new Chan({name: "mcinkay", type: ChanType.QUERY});
network.addChannel(newUser);
expect(network.channels[1]).to.equal(chan1);
expect(network.channels[2]).to.equal(chan2);
expect(network.channels[3]).to.equal(user1);
expect(network.channels[4]).to.equal(newUser);
expect(network.channels[5]).to.equal(user2);
});
it("should not sort special channels", function () {
const chan1 = new Chan({name: "#abc"});
const chan2 = new Chan({name: "#THELOUNGE"});
const user1 = new Chan({name: "astorije", type: ChanType.QUERY});
const user2 = new Chan({name: "xpaw", type: ChanType.QUERY});
const network = new Network({
channels: [chan1, chan2, user1, user2],
});
const newBanlist = new Chan({name: "Banlist for #THELOUNGE", type: ChanType.SPECIAL});
network.addChannel(newBanlist);
expect(network.channels[1]).to.equal(chan1);
expect(network.channels[2]).to.equal(chan2);
expect(network.channels[3]).to.equal(user1);
expect(network.channels[4]).to.equal(user2);
expect(network.channels[5]).to.equal(newBanlist);
});
it("should not compare against special channels", function () {
const chan1 = new Chan({name: "#abc"});
const chan2 = new Chan({name: "#THELOUNGE"});
const user1 = new Chan({name: "astorije", type: ChanType.QUERY});
const network = new Network({
channels: [chan1, chan2, user1],
});
const newBanlist = new Chan({name: "Banlist for #THELOUNGE", type: ChanType.SPECIAL});
network.addChannel(newBanlist);
const newUser = new Chan({name: "mcinkay", type: ChanType.QUERY});
network.addChannel(newUser);
expect(network.channels[1]).to.equal(chan1);
expect(network.channels[2]).to.equal(chan2);
expect(network.channels[3]).to.equal(user1);
expect(network.channels[4]).to.equal(newUser);
expect(network.channels[5]).to.equal(newBanlist);
});
it("should insert before first special channel", function () {
const banlist = new Chan({name: "Banlist for #THELOUNGE", type: ChanType.SPECIAL});
const chan1 = new Chan({name: "#thelounge"});
const user1 = new Chan({name: "astorije", type: ChanType.QUERY});
const network = new Network({
channels: [banlist, chan1, user1],
});
const newChan = new Chan({name: "#foo"});
network.addChannel(newChan);
expect(network.channels[1]).to.equal(newChan);
expect(network.channels[2]).to.equal(banlist);
expect(network.channels[3]).to.equal(chan1);
expect(network.channels[4]).to.equal(user1);
});
it("should never add something in front of the lobby", function () {
const network = new Network({
name: "foo",
channels: [],
});
const newUser = new Chan({name: "astorije"});
network.addChannel(newUser);
expect(network.channels[1]).to.equal(newUser);
});
STSPolicies.update("irc.example.com", 7000, 0); // Cleanup
expect(STSPolicies.get("irc.example.com")).to.be.null;
});
});
describe("#createIrcFramework(client)", function () {
it("should generate and use a client certificate when using SASL external", function () {
Config.values.public = false;
const client = {idMsg: 1, emit() {}};
STSPolicies.update("irc.example.com", 7000, 3600);
expect(STSPolicies.get("irc.example.com")).to.not.be.null;
let network: any = new Network({host: "irc.example.com"});
network.createIrcFramework(client);
expect(network.irc).to.not.be.null;
expect(network.irc.options.client_certificate).to.be.null;
network = new Network({host: "irc.example.com", sasl: "external"});
network.createIrcFramework(client);
expect(network.irc).to.not.be.null;
expect(network.irc.options.client_certificate).to.not.be.null;
ClientCertificate.remove(network.uuid);
Config.values.public = true;
STSPolicies.update("irc.example.com", 7000, 0); // Cleanup
expect(STSPolicies.get("irc.example.com")).to.be.null;
});
});
describe("#edit(client, args)", function () {
it("lockNetwork should allow networks that are not the first one", function () {
Config.values.lockNetwork = true;
Config.values.defaults.push({
name: "Other Example Network",
host: "irc2.example.com",
port: 6667,
tls: false,
rejectUnauthorized: false,
password: "",
nick: "",
username: "",
realname: "",
leaveMessage: "",
sasl: "",
saslAccount: "",
saslPassword: "",
});
// Make sure we lock in private mode
Config.values.public = false;
const network = new Network({
name: "Other Example Network",
host: "illegal.example.com",
port: 1337,
tls: true,
rejectUnauthorized: true,
});
expect(network.validate({} as any)).to.be.true;
expect(network.host).to.equal("irc2.example.com");
expect(network.port).to.equal(6667);
expect(network.tls).to.be.false;
expect(network.rejectUnauthorized).to.be.false;
// Make sure lock in public mode defaults to the first network when
// the hostname does not match (also resets public=true and config.defaults
// for other tests)
Config.values.public = true;
const network2 = new Network({
host: "some.fake.tld",
});
expect(network2.validate({} as any)).to.be.true;
expect(network2.host).to.equal("irc.example.com");
Config.values.lockNetwork = false;
Config.values.defaults.pop();
});
it("should enforce correct types", function () {
let saveCalled = false;
let nameEmitCalled = false;
const network = new Network();
(network as any).edit(
{
emit(name, data) {
if (name === "network:name") {
nameEmitCalled = true;
expect(data.uuid).to.equal(network.uuid);
expect(data.name).to.equal("Lounge Test Network");
}
},
save() {
saveCalled = true;
},
},
{
nick: "newNick",
host: "new.tld",
name: "Lounge Test Network",
port: "1337",
tls: undefined,
rejectUnauthorized: undefined,
username: 1234,
password: 4567,
realname: 8901,
sasl: "something",
saslAccount: 1337,
saslPassword: 1337,
commands: "/command 1 2 3\r\n/ping HELLO\r\r\r\r/whois test\r\n\r\n",
ip: "newIp",
hostname: "newHostname",
uuid: "newuuid",
}
);
expect(saveCalled).to.be.true;
expect(nameEmitCalled).to.be.true;
expect(network.uuid).to.not.equal("newuuid");
// @ts-expect-error Property 'ip' does not exist on type 'Network'.
expect(network.ip).to.be.undefined;
// @ts-expect-error Property 'hostname' does not exist on type 'Network'.
expect(network.hostname).to.be.undefined;
expect(network.name).to.equal("Lounge Test Network");
expect(network.channels[0].name).to.equal("Lounge Test Network");
expect(network.nick).to.equal("newNick");
expect(network.host).to.equal("new.tld");
expect(network.port).to.equal(1337);
expect(network.tls).to.be.false;
expect(network.rejectUnauthorized).to.be.false;
expect(network.username).to.equal("1234");
expect(network.password).to.equal("4567");
expect(network.realname).to.equal("8901");
expect(network.sasl).to.equal("");
expect(network.saslAccount).to.equal("1337");
expect(network.saslPassword).to.equal("1337");
expect(network.commands).to.deep.equal(["/command 1 2 3", "/ping HELLO", "/whois test"]);
});
});
describe("#getFilteredClone(lastActiveChannel, lastMessage)", function () {
it("should filter channels", function () {
const chan = new Chan();
chan.setUser(new User({nick: "test"}));
const network = new Network({
channels: [chan],
});
expect(network.channels[0].users).to.be.empty;
});
it("should keep necessary properties", function () {
const network = new Network();
const clone = network.getFilteredClone();
expect(clone)
.to.be.an("object")
.that.has.all.keys("channels", "status", "nick", "name", "serverOptions", "uuid");
expect(clone.status).to.be.an("object").that.has.all.keys("connected", "secure");
});
});
describe("#addChannel(newChan)", function () {
it("should add channel", function () {
const chan = new Chan({name: "#thelounge"});
const network = new Network({
channels: [chan],
});
// Lobby and initial channel
expect(network.channels.length).to.equal(2);
const newChan = new Chan({name: "#foo"});
network.addChannel(newChan);
expect(network.channels.length).to.equal(3);
});
it("should add channel alphabetically", function () {
const chan1 = new Chan({name: "#abc"});
const chan2 = new Chan({name: "#thelounge"});
const chan3 = new Chan({name: "#zero"});
const network = new Network({
channels: [chan1, chan2, chan3],
name: "foo",
});
const newChan = new Chan({name: "#foo"});
network.addChannel(newChan);
expect(network.channels[0].name).to.equal("foo");
expect(network.channels[1]).to.equal(chan1);
expect(network.channels[2]).to.equal(newChan);
expect(network.channels[3]).to.equal(chan2);
expect(network.channels[4]).to.equal(chan3);
});
it("should sort case-insensitively", function () {
const chan1 = new Chan({name: "#abc"});
const chan2 = new Chan({name: "#THELOUNGE"});
const network = new Network({
channels: [chan1, chan2],
});
const newChan = new Chan({name: "#foo"});
network.addChannel(newChan);
expect(network.channels[1]).to.equal(chan1);
expect(network.channels[2]).to.equal(newChan);
expect(network.channels[3]).to.equal(chan2);
});
it("should sort users separately from channels", function () {
const chan1 = new Chan({name: "#abc"});
const chan2 = new Chan({name: "#THELOUNGE"});
const network = new Network({
channels: [chan1, chan2],
});
const newUser = new Chan({name: "mcinkay", type: ChanType.QUERY});
network.addChannel(newUser);
expect(network.channels[1]).to.equal(chan1);
expect(network.channels[2]).to.equal(chan2);
expect(network.channels[3]).to.equal(newUser);
});
it("should sort users alphabetically", function () {
const chan1 = new Chan({name: "#abc"});
const chan2 = new Chan({name: "#THELOUNGE"});
const user1 = new Chan({name: "astorije", type: ChanType.QUERY});
const user2 = new Chan({name: "xpaw", type: ChanType.QUERY});
const network = new Network({
channels: [chan1, chan2, user1, user2],
});
const newUser = new Chan({name: "mcinkay", type: ChanType.QUERY});
network.addChannel(newUser);
expect(network.channels[1]).to.equal(chan1);
expect(network.channels[2]).to.equal(chan2);
expect(network.channels[3]).to.equal(user1);
expect(network.channels[4]).to.equal(newUser);
expect(network.channels[5]).to.equal(user2);
});
it("should not sort special channels", function () {
const chan1 = new Chan({name: "#abc"});
const chan2 = new Chan({name: "#THELOUNGE"});
const user1 = new Chan({name: "astorije", type: ChanType.QUERY});
const user2 = new Chan({name: "xpaw", type: ChanType.QUERY});
const network = new Network({
channels: [chan1, chan2, user1, user2],
});
const newBanlist = new Chan({name: "Banlist for #THELOUNGE", type: ChanType.SPECIAL});
network.addChannel(newBanlist);
expect(network.channels[1]).to.equal(chan1);
expect(network.channels[2]).to.equal(chan2);
expect(network.channels[3]).to.equal(user1);
expect(network.channels[4]).to.equal(user2);
expect(network.channels[5]).to.equal(newBanlist);
});
it("should not compare against special channels", function () {
const chan1 = new Chan({name: "#abc"});
const chan2 = new Chan({name: "#THELOUNGE"});
const user1 = new Chan({name: "astorije", type: ChanType.QUERY});
const network = new Network({
channels: [chan1, chan2, user1],
});
const newBanlist = new Chan({name: "Banlist for #THELOUNGE", type: ChanType.SPECIAL});
network.addChannel(newBanlist);
const newUser = new Chan({name: "mcinkay", type: ChanType.QUERY});
network.addChannel(newUser);
expect(network.channels[1]).to.equal(chan1);
expect(network.channels[2]).to.equal(chan2);
expect(network.channels[3]).to.equal(user1);
expect(network.channels[4]).to.equal(newUser);
expect(network.channels[5]).to.equal(newBanlist);
});
it("should insert before first special channel", function () {
const banlist = new Chan({name: "Banlist for #THELOUNGE", type: ChanType.SPECIAL});
const chan1 = new Chan({name: "#thelounge"});
const user1 = new Chan({name: "astorije", type: ChanType.QUERY});
const network = new Network({
channels: [banlist, chan1, user1],
});
const newChan = new Chan({name: "#foo"});
network.addChannel(newChan);
expect(network.channels[1]).to.equal(newChan);
expect(network.channels[2]).to.equal(banlist);
expect(network.channels[3]).to.equal(chan1);
expect(network.channels[4]).to.equal(user1);
});
it("should never add something in front of the lobby", function () {
const network = new Network({
name: "foo",
channels: [],
});
const newUser = new Chan({name: "astorije"});
network.addChannel(newUser);
expect(network.channels[1]).to.equal(newUser);
});
});