serverless-ircd 0.8.0 → 0.9.0
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/.github/workflows/ci.yml +4 -0
- package/CHANGELOG.md +245 -0
- package/README.md +160 -200
- package/apps/aws-stack/package.json +1 -1
- package/apps/cf-tcp-container/package.json +1 -1
- package/apps/cf-tcp-container/src/container-server.ts +21 -1
- package/apps/cf-tcp-container/tests/config-loader.test.ts +43 -0
- package/apps/cf-tcp-container/tests/container-server.test.ts +249 -1
- package/apps/cf-tcp-container/tests/persistence.test.ts +9 -0
- package/apps/cf-tcp-container/tests/tls-e2e.test.ts +24 -5
- package/apps/cf-worker/package.json +1 -1
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/server.ts +94 -31
- package/apps/local-cli/tests/config-resolution.test.ts +65 -0
- package/apps/local-cli/tests/motd-file-non-error.test.ts +29 -0
- package/apps/local-cli/tests/rehash.test.ts +147 -0
- package/apps/local-cli/tests/server-helpers.test.ts +63 -0
- package/apps/local-cli/tests/tcp.test.ts +89 -0
- package/apps/local-cli/tests/ws-subprotocol.test.ts +92 -0
- package/apps/web/landing/index.html +226 -3
- package/apps/web/package.json +2 -1
- package/apps/web/scripts/build.mjs +25 -2
- package/apps/web/src/render-docs.ts +292 -0
- package/apps/web/tests/build-smoke.test.ts +31 -2
- package/apps/web/tests/landing-content.test.ts +103 -0
- package/apps/web/tests/render-docs.test.ts +198 -0
- package/docs/AWS-Adapter-Architecture.md +3 -2
- package/docs/Services.md +33 -1
- package/package.json +2 -2
- package/packages/aws-adapter/package.json +1 -1
- package/packages/aws-adapter/src/aws-runtime.ts +15 -1
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +10 -2
- package/packages/aws-adapter/tests/aws-runtime.test.ts +23 -1
- package/packages/aws-adapter/tests/connection-counter.test.ts +17 -0
- package/packages/aws-adapter/tests/global-setup.ts +28 -1
- package/packages/aws-adapter/tests/gone-exception.test.ts +21 -2
- package/packages/aws-adapter/tests/nlb-stream.test.ts +29 -1
- package/packages/aws-adapter/tests/sweeper.test.ts +20 -0
- package/packages/cf-adapter/package.json +1 -1
- package/packages/cf-adapter/src/connection-do.ts +18 -6
- package/packages/cf-adapter/tests/connection-do-pure.test.ts +130 -0
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/irc-core/package.json +1 -1
- package/packages/irc-core/src/commands/account-auth.ts +46 -18
- package/packages/irc-core/src/commands/chanserv.ts +288 -4
- package/packages/irc-core/src/commands/hostserv.ts +38 -3
- package/packages/irc-core/src/commands/index.ts +1 -0
- package/packages/irc-core/src/commands/join.ts +41 -35
- package/packages/irc-core/src/commands/nickserv.ts +16 -4
- package/packages/irc-core/src/commands/registration.ts +27 -16
- package/packages/irc-core/src/commands/service-aliases.ts +52 -0
- package/packages/irc-core/src/commands/topic.ts +23 -10
- package/packages/irc-core/src/state/channel.ts +17 -0
- package/packages/irc-core/tests/commands/chanserv.test.ts +668 -1
- package/packages/irc-core/tests/commands/hostserv.test.ts +71 -0
- package/packages/irc-core/tests/commands/join.test.ts +179 -0
- package/packages/irc-core/tests/commands/nickserv.test.ts +185 -2
- package/packages/irc-core/tests/commands/registration.test.ts +227 -6
- package/packages/irc-core/tests/commands/sasl.test.ts +44 -0
- package/packages/irc-core/tests/commands/service-aliases.test.ts +52 -0
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +80 -30
- package/packages/irc-server/tests/actor.test.ts +365 -3
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/in-memory-harness.ts +8 -5
- package/packages/irc-test-support/src/scenarios.ts +21 -6
- package/packages/irc-test-support/tests/in-memory-harness.test.ts +19 -0
- package/packages/irc-test-support/vitest.config.ts +6 -1
- package/tools/ci-hardening/package.json +1 -1
- package/tools/load-test/package.json +1 -1
- package/tools/load-test/src/client.ts +13 -13
- package/tools/load-test/tests/client.test.ts +258 -2
- package/tools/load-test/tests/config.test.ts +39 -0
- package/tools/load-test/tests/harness.test.ts +21 -0
- package/tools/load-test/tests/metrics.test.ts +7 -0
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/tools/tcp-ws-forwarder/tests/close-error.test.ts +40 -0
- package/tools/tcp-ws-forwarder/tests/defensive-branches.test.ts +78 -0
- package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +51 -0
- package/tools/tcp-ws-forwarder/tests/logger.test.ts +31 -1
|
@@ -5,7 +5,9 @@ import {
|
|
|
5
5
|
SHORTHAND_LEVELS,
|
|
6
6
|
chanservReducer,
|
|
7
7
|
positiveMlockLetters,
|
|
8
|
+
resolveMembershipTarget,
|
|
8
9
|
} from '../../src/commands/chanserv';
|
|
10
|
+
import { Effect } from '../../src/effects';
|
|
9
11
|
import type { Effect as EffectType } from '../../src/effects';
|
|
10
12
|
import type { ServicesStore } from '../../src/ports';
|
|
11
13
|
import {
|
|
@@ -15,6 +17,7 @@ import {
|
|
|
15
17
|
SequentialIdFactory,
|
|
16
18
|
} from '../../src/ports';
|
|
17
19
|
import type { IrcMessage } from '../../src/protocol/messages';
|
|
20
|
+
import { type RosterEntry, createChannel } from '../../src/state/channel';
|
|
18
21
|
import { type ConnectionState, createConnection } from '../../src/state/connection';
|
|
19
22
|
import { type Ctx, type ServerConfig, buildCtx } from '../../src/types';
|
|
20
23
|
|
|
@@ -778,7 +781,7 @@ describe('chanservReducer — SET misc', () => {
|
|
|
778
781
|
const out = chanservReducer(conn, privmsg('FROBNICATE'), ctx);
|
|
779
782
|
|
|
780
783
|
expect(sentTexts(out.effects)).toContain(
|
|
781
|
-
':ChanServ!ChanServ@services NOTICE alice :Unknown command. Available: REGISTER, DROP, INFO, SET, ACCESS, LEVELS, SOP, AOP, HOP, VOP',
|
|
784
|
+
':ChanServ!ChanServ@services NOTICE alice :Unknown command. Available: REGISTER, DROP, INFO, SET, ACCESS, LEVELS, SOP, AOP, HOP, VOP, OP, DEOP, VOICE, DEVOICE, KICK, BAN, UNBAN',
|
|
782
785
|
);
|
|
783
786
|
});
|
|
784
787
|
|
|
@@ -1666,3 +1669,667 @@ describe('SHORTHAND_LEVELS / DEFAULT_CHANNEL_LEVELS constants', () => {
|
|
|
1666
1669
|
}
|
|
1667
1670
|
});
|
|
1668
1671
|
});
|
|
1672
|
+
|
|
1673
|
+
// ============================================================================
|
|
1674
|
+
// OP / DEOP / VOICE / DEVOICE
|
|
1675
|
+
// ============================================================================
|
|
1676
|
+
//
|
|
1677
|
+
// These four commands share a single prefix-broadcast + ApplyChannelDelta
|
|
1678
|
+
// shape. ChanServ cannot resolve nick → connId (it operates on
|
|
1679
|
+
// ConnectionState, not ChannelState), so the reducer emits a membership
|
|
1680
|
+
// delta carrying a `targetNick` hint and a placeholder `conn: ''`. The
|
|
1681
|
+
// actor layer resolves the nick against the channel roster at apply time:
|
|
1682
|
+
// - resolved → the present op/voice fields are *patched* onto the
|
|
1683
|
+
// existing roster entry (so op-ing a voiced user does not clear voice)
|
|
1684
|
+
// - absent → the delta is a no-op and ChanServ NOTICEs the caller
|
|
1685
|
+
//
|
|
1686
|
+
// The patch semantics (only fields present on the delta are applied) are
|
|
1687
|
+
// documented on `MembershipDelta.targetNick` in state/channel.ts and are
|
|
1688
|
+
// exercised by the `resolveMembershipTarget` helper tests at the bottom
|
|
1689
|
+
// of this file.
|
|
1690
|
+
|
|
1691
|
+
/** Extracts the first ApplyChannelDelta effect from a reducer output. */
|
|
1692
|
+
function firstApplyChannelDelta(effects: EffectType[]): EffectType | undefined {
|
|
1693
|
+
return effects.find((e) => e.tag === 'ApplyChannelDelta');
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
/** Extracts the first Broadcast effect from a reducer output. */
|
|
1697
|
+
function firstBroadcast(effects: EffectType[]): EffectType | undefined {
|
|
1698
|
+
return effects.find((e) => e.tag === 'Broadcast');
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
describe('chanservReducer — OP', () => {
|
|
1702
|
+
it('emits a +o broadcast, a targetNick-keyed delta, and a confirmation NOTICE from the founder', () => {
|
|
1703
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1704
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1705
|
+
services.registerChannel('#home', 'alice');
|
|
1706
|
+
const conn = makeConn();
|
|
1707
|
+
conn.account = 'alice';
|
|
1708
|
+
conn.userModes.registered = true;
|
|
1709
|
+
const ctx = makeCtx(conn, services);
|
|
1710
|
+
|
|
1711
|
+
const out = chanservReducer(conn, privmsg('OP #home bob'), ctx);
|
|
1712
|
+
|
|
1713
|
+
expect(firstBroadcast(out.effects)).toEqual(
|
|
1714
|
+
Effect.broadcast('#home', [{ text: ':ChanServ!ChanServ@services MODE #home +o bob' }]),
|
|
1715
|
+
);
|
|
1716
|
+
expect(firstApplyChannelDelta(out.effects)).toEqual(
|
|
1717
|
+
Effect.applyChannelDelta('#home', {
|
|
1718
|
+
memberships: [{ type: 'add', conn: '', nick: 'bob', targetNick: 'bob', op: true }],
|
|
1719
|
+
}),
|
|
1720
|
+
);
|
|
1721
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1722
|
+
':ChanServ!ChanServ@services NOTICE alice :Set mode +o on bob on #home.',
|
|
1723
|
+
);
|
|
1724
|
+
});
|
|
1725
|
+
|
|
1726
|
+
it('allows OP from an AUTOOP-level access-list user', () => {
|
|
1727
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1728
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1729
|
+
services.registerNick('carol', 'pw', 'c@example.com');
|
|
1730
|
+
services.registerChannel('#home', 'alice');
|
|
1731
|
+
services.setChannelAccess('#home', 'carol', DEFAULT_CHANNEL_LEVELS.AUTOOP);
|
|
1732
|
+
const conn = makeConn('c1', 'carol');
|
|
1733
|
+
conn.account = 'carol';
|
|
1734
|
+
conn.userModes.registered = true;
|
|
1735
|
+
const ctx = makeCtx(conn, services);
|
|
1736
|
+
|
|
1737
|
+
const out = chanservReducer(conn, privmsg('OP #home bob'), ctx);
|
|
1738
|
+
|
|
1739
|
+
expect(firstBroadcast(out.effects)).toBeDefined();
|
|
1740
|
+
});
|
|
1741
|
+
|
|
1742
|
+
it('rejects OP from a non-privileged identified account with Permission denied', () => {
|
|
1743
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1744
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1745
|
+
services.registerNick('mallory', 'pw', 'm@example.com');
|
|
1746
|
+
services.registerChannel('#home', 'alice');
|
|
1747
|
+
services.setChannelAccess('#home', 'mallory', 1);
|
|
1748
|
+
const conn = makeConn('c1', 'mallory');
|
|
1749
|
+
conn.account = 'mallory';
|
|
1750
|
+
conn.userModes.registered = true;
|
|
1751
|
+
const ctx = makeCtx(conn, services);
|
|
1752
|
+
|
|
1753
|
+
const out = chanservReducer(conn, privmsg('OP #home bob'), ctx);
|
|
1754
|
+
|
|
1755
|
+
expect(firstBroadcast(out.effects)).toBeUndefined();
|
|
1756
|
+
expect(firstApplyChannelDelta(out.effects)).toBeUndefined();
|
|
1757
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1758
|
+
':ChanServ!ChanServ@services NOTICE mallory :Permission denied.',
|
|
1759
|
+
);
|
|
1760
|
+
});
|
|
1761
|
+
|
|
1762
|
+
it('rejects OP on an unregistered channel', () => {
|
|
1763
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1764
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1765
|
+
const conn = makeConn();
|
|
1766
|
+
conn.account = 'alice';
|
|
1767
|
+
conn.userModes.registered = true;
|
|
1768
|
+
const ctx = makeCtx(conn, services);
|
|
1769
|
+
|
|
1770
|
+
const out = chanservReducer(conn, privmsg('OP #home bob'), ctx);
|
|
1771
|
+
|
|
1772
|
+
expect(firstBroadcast(out.effects)).toBeUndefined();
|
|
1773
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1774
|
+
':ChanServ!ChanServ@services NOTICE alice :Channel #home is not registered.',
|
|
1775
|
+
);
|
|
1776
|
+
});
|
|
1777
|
+
|
|
1778
|
+
it('rejects OP when the caller is not identified', () => {
|
|
1779
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1780
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1781
|
+
services.registerChannel('#home', 'alice');
|
|
1782
|
+
const conn = makeConn();
|
|
1783
|
+
const ctx = makeCtx(conn, services);
|
|
1784
|
+
|
|
1785
|
+
const out = chanservReducer(conn, privmsg('OP #home bob'), ctx);
|
|
1786
|
+
|
|
1787
|
+
expect(firstBroadcast(out.effects)).toBeUndefined();
|
|
1788
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1789
|
+
':ChanServ!ChanServ@services NOTICE alice :You must identify before changing channel settings.',
|
|
1790
|
+
);
|
|
1791
|
+
});
|
|
1792
|
+
|
|
1793
|
+
it('rejects OP with a missing channel parameter', () => {
|
|
1794
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1795
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1796
|
+
services.registerChannel('#home', 'alice');
|
|
1797
|
+
const conn = makeConn();
|
|
1798
|
+
conn.account = 'alice';
|
|
1799
|
+
conn.userModes.registered = true;
|
|
1800
|
+
const ctx = makeCtx(conn, services);
|
|
1801
|
+
|
|
1802
|
+
const out = chanservReducer(conn, privmsg('OP'), ctx);
|
|
1803
|
+
|
|
1804
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1805
|
+
':ChanServ!ChanServ@services NOTICE alice :Syntax: OP <#channel> <nick>.',
|
|
1806
|
+
);
|
|
1807
|
+
});
|
|
1808
|
+
|
|
1809
|
+
it('rejects OP with a missing target nick', () => {
|
|
1810
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1811
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1812
|
+
services.registerChannel('#home', 'alice');
|
|
1813
|
+
const conn = makeConn();
|
|
1814
|
+
conn.account = 'alice';
|
|
1815
|
+
conn.userModes.registered = true;
|
|
1816
|
+
const ctx = makeCtx(conn, services);
|
|
1817
|
+
|
|
1818
|
+
const out = chanservReducer(conn, privmsg('OP #home'), ctx);
|
|
1819
|
+
|
|
1820
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1821
|
+
':ChanServ!ChanServ@services NOTICE alice :Syntax: OP <#channel> <nick>.',
|
|
1822
|
+
);
|
|
1823
|
+
});
|
|
1824
|
+
|
|
1825
|
+
it('rejects OP with an invalid channel name', () => {
|
|
1826
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1827
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1828
|
+
services.registerChannel('#home', 'alice');
|
|
1829
|
+
const conn = makeConn();
|
|
1830
|
+
conn.account = 'alice';
|
|
1831
|
+
conn.userModes.registered = true;
|
|
1832
|
+
const ctx = makeCtx(conn, services);
|
|
1833
|
+
|
|
1834
|
+
const out = chanservReducer(conn, privmsg('OP bogus bob'), ctx);
|
|
1835
|
+
|
|
1836
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1837
|
+
':ChanServ!ChanServ@services NOTICE alice :Syntax: OP <#channel> <nick>.',
|
|
1838
|
+
);
|
|
1839
|
+
});
|
|
1840
|
+
|
|
1841
|
+
it('is case-insensitive on the verb', () => {
|
|
1842
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1843
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1844
|
+
services.registerChannel('#home', 'alice');
|
|
1845
|
+
const conn = makeConn();
|
|
1846
|
+
conn.account = 'alice';
|
|
1847
|
+
conn.userModes.registered = true;
|
|
1848
|
+
const ctx = makeCtx(conn, services);
|
|
1849
|
+
|
|
1850
|
+
const out = chanservReducer(conn, privmsg('op #home bob'), ctx);
|
|
1851
|
+
|
|
1852
|
+
expect(firstBroadcast(out.effects)).toBeDefined();
|
|
1853
|
+
});
|
|
1854
|
+
});
|
|
1855
|
+
|
|
1856
|
+
describe('chanservReducer — DEOP', () => {
|
|
1857
|
+
it('emits a -o broadcast and a targetNick-keyned delta from the founder', () => {
|
|
1858
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1859
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1860
|
+
services.registerChannel('#home', 'alice');
|
|
1861
|
+
const conn = makeConn();
|
|
1862
|
+
conn.account = 'alice';
|
|
1863
|
+
conn.userModes.registered = true;
|
|
1864
|
+
const ctx = makeCtx(conn, services);
|
|
1865
|
+
|
|
1866
|
+
const out = chanservReducer(conn, privmsg('DEOP #home bob'), ctx);
|
|
1867
|
+
|
|
1868
|
+
expect(firstBroadcast(out.effects)).toEqual(
|
|
1869
|
+
Effect.broadcast('#home', [{ text: ':ChanServ!ChanServ@services MODE #home -o bob' }]),
|
|
1870
|
+
);
|
|
1871
|
+
expect(firstApplyChannelDelta(out.effects)).toEqual(
|
|
1872
|
+
Effect.applyChannelDelta('#home', {
|
|
1873
|
+
memberships: [{ type: 'add', conn: '', nick: 'bob', targetNick: 'bob', op: false }],
|
|
1874
|
+
}),
|
|
1875
|
+
);
|
|
1876
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1877
|
+
':ChanServ!ChanServ@services NOTICE alice :Set mode -o on bob on #home.',
|
|
1878
|
+
);
|
|
1879
|
+
});
|
|
1880
|
+
|
|
1881
|
+
it('rejects DEOP from a non-privileged caller', () => {
|
|
1882
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1883
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1884
|
+
services.registerNick('mallory', 'pw', 'm@example.com');
|
|
1885
|
+
services.registerChannel('#home', 'alice');
|
|
1886
|
+
const conn = makeConn('c1', 'mallory');
|
|
1887
|
+
conn.account = 'mallory';
|
|
1888
|
+
conn.userModes.registered = true;
|
|
1889
|
+
const ctx = makeCtx(conn, services);
|
|
1890
|
+
|
|
1891
|
+
const out = chanservReducer(conn, privmsg('DEOP #home bob'), ctx);
|
|
1892
|
+
|
|
1893
|
+
expect(firstBroadcast(out.effects)).toBeUndefined();
|
|
1894
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1895
|
+
':ChanServ!ChanServ@services NOTICE mallory :Permission denied.',
|
|
1896
|
+
);
|
|
1897
|
+
});
|
|
1898
|
+
});
|
|
1899
|
+
|
|
1900
|
+
describe('chanservReducer — VOICE', () => {
|
|
1901
|
+
it('emits a +v broadcast and a targetNick-keyned delta from the founder', () => {
|
|
1902
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1903
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1904
|
+
services.registerChannel('#home', 'alice');
|
|
1905
|
+
const conn = makeConn();
|
|
1906
|
+
conn.account = 'alice';
|
|
1907
|
+
conn.userModes.registered = true;
|
|
1908
|
+
const ctx = makeCtx(conn, services);
|
|
1909
|
+
|
|
1910
|
+
const out = chanservReducer(conn, privmsg('VOICE #home bob'), ctx);
|
|
1911
|
+
|
|
1912
|
+
expect(firstBroadcast(out.effects)).toEqual(
|
|
1913
|
+
Effect.broadcast('#home', [{ text: ':ChanServ!ChanServ@services MODE #home +v bob' }]),
|
|
1914
|
+
);
|
|
1915
|
+
expect(firstApplyChannelDelta(out.effects)).toEqual(
|
|
1916
|
+
Effect.applyChannelDelta('#home', {
|
|
1917
|
+
memberships: [{ type: 'add', conn: '', nick: 'bob', targetNick: 'bob', voice: true }],
|
|
1918
|
+
}),
|
|
1919
|
+
);
|
|
1920
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1921
|
+
':ChanServ!ChanServ@services NOTICE alice :Set mode +v on bob on #home.',
|
|
1922
|
+
);
|
|
1923
|
+
});
|
|
1924
|
+
});
|
|
1925
|
+
|
|
1926
|
+
describe('chanservReducer — DEVOICE', () => {
|
|
1927
|
+
it('emits a -v broadcast and a targetNick-keyned delta from the founder', () => {
|
|
1928
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1929
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1930
|
+
services.registerChannel('#home', 'alice');
|
|
1931
|
+
const conn = makeConn();
|
|
1932
|
+
conn.account = 'alice';
|
|
1933
|
+
conn.userModes.registered = true;
|
|
1934
|
+
const ctx = makeCtx(conn, services);
|
|
1935
|
+
|
|
1936
|
+
const out = chanservReducer(conn, privmsg('DEVOICE #home bob'), ctx);
|
|
1937
|
+
|
|
1938
|
+
expect(firstBroadcast(out.effects)).toEqual(
|
|
1939
|
+
Effect.broadcast('#home', [{ text: ':ChanServ!ChanServ@services MODE #home -v bob' }]),
|
|
1940
|
+
);
|
|
1941
|
+
expect(firstApplyChannelDelta(out.effects)).toEqual(
|
|
1942
|
+
Effect.applyChannelDelta('#home', {
|
|
1943
|
+
memberships: [{ type: 'add', conn: '', nick: 'bob', targetNick: 'bob', voice: false }],
|
|
1944
|
+
}),
|
|
1945
|
+
);
|
|
1946
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1947
|
+
':ChanServ!ChanServ@services NOTICE alice :Set mode -v on bob on #home.',
|
|
1948
|
+
);
|
|
1949
|
+
});
|
|
1950
|
+
});
|
|
1951
|
+
|
|
1952
|
+
// ============================================================================
|
|
1953
|
+
// KICK
|
|
1954
|
+
// ============================================================================
|
|
1955
|
+
|
|
1956
|
+
describe('chanservReducer — KICK', () => {
|
|
1957
|
+
it('emits a ChanServ KICK broadcast and a targetNick-keyned remove delta from the founder', () => {
|
|
1958
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1959
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1960
|
+
services.registerChannel('#home', 'alice');
|
|
1961
|
+
const conn = makeConn();
|
|
1962
|
+
conn.account = 'alice';
|
|
1963
|
+
conn.userModes.registered = true;
|
|
1964
|
+
const ctx = makeCtx(conn, services);
|
|
1965
|
+
|
|
1966
|
+
const out = chanservReducer(conn, privmsg('KICK #home bob :trolling'), ctx);
|
|
1967
|
+
|
|
1968
|
+
expect(firstBroadcast(out.effects)).toEqual(
|
|
1969
|
+
Effect.broadcast('#home', [{ text: ':ChanServ!ChanServ@services KICK #home bob :trolling' }]),
|
|
1970
|
+
);
|
|
1971
|
+
expect(firstApplyChannelDelta(out.effects)).toEqual(
|
|
1972
|
+
Effect.applyChannelDelta('#home', {
|
|
1973
|
+
memberships: [{ type: 'remove', conn: '', targetNick: 'bob' }],
|
|
1974
|
+
}),
|
|
1975
|
+
);
|
|
1976
|
+
expect(sentTexts(out.effects)).toContain(
|
|
1977
|
+
':ChanServ!ChanServ@services NOTICE alice :Kicked bob from #home.',
|
|
1978
|
+
);
|
|
1979
|
+
});
|
|
1980
|
+
|
|
1981
|
+
it('broadcasts KICK without a trailing reason when none is supplied', () => {
|
|
1982
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1983
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1984
|
+
services.registerChannel('#home', 'alice');
|
|
1985
|
+
const conn = makeConn();
|
|
1986
|
+
conn.account = 'alice';
|
|
1987
|
+
conn.userModes.registered = true;
|
|
1988
|
+
const ctx = makeCtx(conn, services);
|
|
1989
|
+
|
|
1990
|
+
const out = chanservReducer(conn, privmsg('KICK #home bob'), ctx);
|
|
1991
|
+
|
|
1992
|
+
expect(firstBroadcast(out.effects)).toEqual(
|
|
1993
|
+
Effect.broadcast('#home', [{ text: ':ChanServ!ChanServ@services KICK #home bob' }]),
|
|
1994
|
+
);
|
|
1995
|
+
});
|
|
1996
|
+
|
|
1997
|
+
it('rejects KICK from a non-founder AUTOOP user (founder-only)', () => {
|
|
1998
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
1999
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
2000
|
+
services.registerNick('carol', 'pw', 'c@example.com');
|
|
2001
|
+
services.registerChannel('#home', 'alice');
|
|
2002
|
+
services.setChannelAccess('#home', 'carol', DEFAULT_CHANNEL_LEVELS.AUTOOP);
|
|
2003
|
+
const conn = makeConn('c1', 'carol');
|
|
2004
|
+
conn.account = 'carol';
|
|
2005
|
+
conn.userModes.registered = true;
|
|
2006
|
+
const ctx = makeCtx(conn, services);
|
|
2007
|
+
|
|
2008
|
+
const out = chanservReducer(conn, privmsg('KICK #home bob'), ctx);
|
|
2009
|
+
|
|
2010
|
+
expect(firstBroadcast(out.effects)).toBeUndefined();
|
|
2011
|
+
expect(firstApplyChannelDelta(out.effects)).toBeUndefined();
|
|
2012
|
+
expect(sentTexts(out.effects)).toContain(
|
|
2013
|
+
':ChanServ!ChanServ@services NOTICE carol :Permission denied.',
|
|
2014
|
+
);
|
|
2015
|
+
});
|
|
2016
|
+
|
|
2017
|
+
it('rejects KICK on an unregistered channel', () => {
|
|
2018
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
2019
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
2020
|
+
const conn = makeConn();
|
|
2021
|
+
conn.account = 'alice';
|
|
2022
|
+
conn.userModes.registered = true;
|
|
2023
|
+
const ctx = makeCtx(conn, services);
|
|
2024
|
+
|
|
2025
|
+
const out = chanservReducer(conn, privmsg('KICK #home bob'), ctx);
|
|
2026
|
+
|
|
2027
|
+
expect(firstBroadcast(out.effects)).toBeUndefined();
|
|
2028
|
+
expect(sentTexts(out.effects)).toContain(
|
|
2029
|
+
':ChanServ!ChanServ@services NOTICE alice :Channel #home is not registered.',
|
|
2030
|
+
);
|
|
2031
|
+
});
|
|
2032
|
+
|
|
2033
|
+
it('rejects KICK when the caller is not identified', () => {
|
|
2034
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
2035
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
2036
|
+
services.registerChannel('#home', 'alice');
|
|
2037
|
+
const conn = makeConn();
|
|
2038
|
+
const ctx = makeCtx(conn, services);
|
|
2039
|
+
|
|
2040
|
+
const out = chanservReducer(conn, privmsg('KICK #home bob'), ctx);
|
|
2041
|
+
|
|
2042
|
+
expect(firstBroadcast(out.effects)).toBeUndefined();
|
|
2043
|
+
expect(sentTexts(out.effects)).toContain(
|
|
2044
|
+
':ChanServ!ChanServ@services NOTICE alice :You must identify before changing channel settings.',
|
|
2045
|
+
);
|
|
2046
|
+
});
|
|
2047
|
+
|
|
2048
|
+
it('rejects KICK with a missing target nick', () => {
|
|
2049
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
2050
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
2051
|
+
services.registerChannel('#home', 'alice');
|
|
2052
|
+
const conn = makeConn();
|
|
2053
|
+
conn.account = 'alice';
|
|
2054
|
+
conn.userModes.registered = true;
|
|
2055
|
+
const ctx = makeCtx(conn, services);
|
|
2056
|
+
|
|
2057
|
+
const out = chanservReducer(conn, privmsg('KICK #home'), ctx);
|
|
2058
|
+
|
|
2059
|
+
expect(sentTexts(out.effects)).toContain(
|
|
2060
|
+
':ChanServ!ChanServ@services NOTICE alice :Syntax: KICK <#channel> <nick> [:<reason>].',
|
|
2061
|
+
);
|
|
2062
|
+
});
|
|
2063
|
+
|
|
2064
|
+
it('rejects KICK with an invalid channel name', () => {
|
|
2065
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
2066
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
2067
|
+
services.registerChannel('#home', 'alice');
|
|
2068
|
+
const conn = makeConn();
|
|
2069
|
+
conn.account = 'alice';
|
|
2070
|
+
conn.userModes.registered = true;
|
|
2071
|
+
const ctx = makeCtx(conn, services);
|
|
2072
|
+
|
|
2073
|
+
const out = chanservReducer(conn, privmsg('KICK bogus bob'), ctx);
|
|
2074
|
+
|
|
2075
|
+
expect(sentTexts(out.effects)).toContain(
|
|
2076
|
+
':ChanServ!ChanServ@services NOTICE alice :Syntax: KICK <#channel> <nick> [:<reason>].',
|
|
2077
|
+
);
|
|
2078
|
+
});
|
|
2079
|
+
});
|
|
2080
|
+
|
|
2081
|
+
// ============================================================================
|
|
2082
|
+
// BAN / UNBAN
|
|
2083
|
+
// ============================================================================
|
|
2084
|
+
|
|
2085
|
+
describe('chanservReducer — BAN', () => {
|
|
2086
|
+
it('emits a +b broadcast, a banMaskChanges delta, and a confirmation NOTICE from the founder', () => {
|
|
2087
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
2088
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
2089
|
+
services.registerChannel('#home', 'alice');
|
|
2090
|
+
const conn = makeConn();
|
|
2091
|
+
conn.account = 'alice';
|
|
2092
|
+
conn.userModes.registered = true;
|
|
2093
|
+
const ctx = makeCtx(conn, services);
|
|
2094
|
+
|
|
2095
|
+
const out = chanservReducer(conn, privmsg('BAN #home *!*@badhost'), ctx);
|
|
2096
|
+
|
|
2097
|
+
expect(firstBroadcast(out.effects)).toEqual(
|
|
2098
|
+
Effect.broadcast('#home', [
|
|
2099
|
+
{ text: ':ChanServ!ChanServ@services MODE #home +b *!*@badhost' },
|
|
2100
|
+
]),
|
|
2101
|
+
);
|
|
2102
|
+
expect(firstApplyChannelDelta(out.effects)).toEqual(
|
|
2103
|
+
Effect.applyChannelDelta('#home', {
|
|
2104
|
+
banMaskChanges: [{ type: 'add', mask: '*!*@badhost' }],
|
|
2105
|
+
}),
|
|
2106
|
+
);
|
|
2107
|
+
expect(sentTexts(out.effects)).toContain(
|
|
2108
|
+
':ChanServ!ChanServ@services NOTICE alice :Set ban *!*@badhost on #home.',
|
|
2109
|
+
);
|
|
2110
|
+
});
|
|
2111
|
+
|
|
2112
|
+
it('does NOT kick matching present members (documented: Atheme-style kick-on-ban is a follow-up)', () => {
|
|
2113
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
2114
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
2115
|
+
services.registerChannel('#home', 'alice');
|
|
2116
|
+
const conn = makeConn();
|
|
2117
|
+
conn.account = 'alice';
|
|
2118
|
+
conn.userModes.registered = true;
|
|
2119
|
+
const ctx = makeCtx(conn, services);
|
|
2120
|
+
|
|
2121
|
+
const out = chanservReducer(conn, privmsg('BAN #home *!*@badhost'), ctx);
|
|
2122
|
+
|
|
2123
|
+
// The reducer emits no membership deltas (no kick), only the ban-mask add.
|
|
2124
|
+
for (const e of out.effects) {
|
|
2125
|
+
if (e.tag === 'ApplyChannelDelta') {
|
|
2126
|
+
expect(e.delta.memberships ?? []).toEqual([]);
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
});
|
|
2130
|
+
|
|
2131
|
+
it('rejects BAN from a non-founder AUTOOP user (founder-only)', () => {
|
|
2132
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
2133
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
2134
|
+
services.registerNick('carol', 'pw', 'c@example.com');
|
|
2135
|
+
services.registerChannel('#home', 'alice');
|
|
2136
|
+
services.setChannelAccess('#home', 'carol', DEFAULT_CHANNEL_LEVELS.AUTOOP);
|
|
2137
|
+
const conn = makeConn('c1', 'carol');
|
|
2138
|
+
conn.account = 'carol';
|
|
2139
|
+
conn.userModes.registered = true;
|
|
2140
|
+
const ctx = makeCtx(conn, services);
|
|
2141
|
+
|
|
2142
|
+
const out = chanservReducer(conn, privmsg('BAN #home *!*@badhost'), ctx);
|
|
2143
|
+
|
|
2144
|
+
expect(firstApplyChannelDelta(out.effects)).toBeUndefined();
|
|
2145
|
+
expect(sentTexts(out.effects)).toContain(
|
|
2146
|
+
':ChanServ!ChanServ@services NOTICE carol :Permission denied.',
|
|
2147
|
+
);
|
|
2148
|
+
});
|
|
2149
|
+
|
|
2150
|
+
it('rejects BAN on an unregistered channel', () => {
|
|
2151
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
2152
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
2153
|
+
const conn = makeConn();
|
|
2154
|
+
conn.account = 'alice';
|
|
2155
|
+
conn.userModes.registered = true;
|
|
2156
|
+
const ctx = makeCtx(conn, services);
|
|
2157
|
+
|
|
2158
|
+
const out = chanservReducer(conn, privmsg('BAN #home *!*@badhost'), ctx);
|
|
2159
|
+
|
|
2160
|
+
expect(firstApplyChannelDelta(out.effects)).toBeUndefined();
|
|
2161
|
+
expect(sentTexts(out.effects)).toContain(
|
|
2162
|
+
':ChanServ!ChanServ@services NOTICE alice :Channel #home is not registered.',
|
|
2163
|
+
);
|
|
2164
|
+
});
|
|
2165
|
+
|
|
2166
|
+
it('rejects BAN when the caller is not identified', () => {
|
|
2167
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
2168
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
2169
|
+
services.registerChannel('#home', 'alice');
|
|
2170
|
+
const conn = makeConn();
|
|
2171
|
+
const ctx = makeCtx(conn, services);
|
|
2172
|
+
|
|
2173
|
+
const out = chanservReducer(conn, privmsg('BAN #home *!*@badhost'), ctx);
|
|
2174
|
+
|
|
2175
|
+
expect(firstApplyChannelDelta(out.effects)).toBeUndefined();
|
|
2176
|
+
expect(sentTexts(out.effects)).toContain(
|
|
2177
|
+
':ChanServ!ChanServ@services NOTICE alice :You must identify before changing channel settings.',
|
|
2178
|
+
);
|
|
2179
|
+
});
|
|
2180
|
+
|
|
2181
|
+
it('rejects BAN with a missing mask', () => {
|
|
2182
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
2183
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
2184
|
+
services.registerChannel('#home', 'alice');
|
|
2185
|
+
const conn = makeConn();
|
|
2186
|
+
conn.account = 'alice';
|
|
2187
|
+
conn.userModes.registered = true;
|
|
2188
|
+
const ctx = makeCtx(conn, services);
|
|
2189
|
+
|
|
2190
|
+
const out = chanservReducer(conn, privmsg('BAN #home'), ctx);
|
|
2191
|
+
|
|
2192
|
+
expect(sentTexts(out.effects)).toContain(
|
|
2193
|
+
':ChanServ!ChanServ@services NOTICE alice :Syntax: BAN <#channel> <mask>.',
|
|
2194
|
+
);
|
|
2195
|
+
});
|
|
2196
|
+
|
|
2197
|
+
it('rejects BAN with an invalid channel name', () => {
|
|
2198
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
2199
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
2200
|
+
services.registerChannel('#home', 'alice');
|
|
2201
|
+
const conn = makeConn();
|
|
2202
|
+
conn.account = 'alice';
|
|
2203
|
+
conn.userModes.registered = true;
|
|
2204
|
+
const ctx = makeCtx(conn, services);
|
|
2205
|
+
|
|
2206
|
+
const out = chanservReducer(conn, privmsg('BAN bogus *!*@badhost'), ctx);
|
|
2207
|
+
|
|
2208
|
+
expect(sentTexts(out.effects)).toContain(
|
|
2209
|
+
':ChanServ!ChanServ@services NOTICE alice :Syntax: BAN <#channel> <mask>.',
|
|
2210
|
+
);
|
|
2211
|
+
});
|
|
2212
|
+
});
|
|
2213
|
+
|
|
2214
|
+
describe('chanservReducer — UNBAN', () => {
|
|
2215
|
+
it('emits a -b broadcast and a remove banMaskChanges delta from the founder', () => {
|
|
2216
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
2217
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
2218
|
+
services.registerChannel('#home', 'alice');
|
|
2219
|
+
const conn = makeConn();
|
|
2220
|
+
conn.account = 'alice';
|
|
2221
|
+
conn.userModes.registered = true;
|
|
2222
|
+
const ctx = makeCtx(conn, services);
|
|
2223
|
+
|
|
2224
|
+
const out = chanservReducer(conn, privmsg('UNBAN #home *!*@badhost'), ctx);
|
|
2225
|
+
|
|
2226
|
+
expect(firstBroadcast(out.effects)).toEqual(
|
|
2227
|
+
Effect.broadcast('#home', [
|
|
2228
|
+
{ text: ':ChanServ!ChanServ@services MODE #home -b *!*@badhost' },
|
|
2229
|
+
]),
|
|
2230
|
+
);
|
|
2231
|
+
expect(firstApplyChannelDelta(out.effects)).toEqual(
|
|
2232
|
+
Effect.applyChannelDelta('#home', {
|
|
2233
|
+
banMaskChanges: [{ type: 'remove', mask: '*!*@badhost' }],
|
|
2234
|
+
}),
|
|
2235
|
+
);
|
|
2236
|
+
expect(sentTexts(out.effects)).toContain(
|
|
2237
|
+
':ChanServ!ChanServ@services NOTICE alice :Removed ban *!*@badhost from #home.',
|
|
2238
|
+
);
|
|
2239
|
+
});
|
|
2240
|
+
|
|
2241
|
+
it('rejects UNBAN from a non-founder caller', () => {
|
|
2242
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
2243
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
2244
|
+
services.registerNick('carol', 'pw', 'c@example.com');
|
|
2245
|
+
services.registerChannel('#home', 'alice');
|
|
2246
|
+
services.setChannelAccess('#home', 'carol', DEFAULT_CHANNEL_LEVELS.AUTOOP);
|
|
2247
|
+
const conn = makeConn('c1', 'carol');
|
|
2248
|
+
conn.account = 'carol';
|
|
2249
|
+
conn.userModes.registered = true;
|
|
2250
|
+
const ctx = makeCtx(conn, services);
|
|
2251
|
+
|
|
2252
|
+
const out = chanservReducer(conn, privmsg('UNBAN #home *!*@badhost'), ctx);
|
|
2253
|
+
|
|
2254
|
+
expect(firstApplyChannelDelta(out.effects)).toBeUndefined();
|
|
2255
|
+
expect(sentTexts(out.effects)).toContain(
|
|
2256
|
+
':ChanServ!ChanServ@services NOTICE carol :Permission denied.',
|
|
2257
|
+
);
|
|
2258
|
+
});
|
|
2259
|
+
|
|
2260
|
+
it('rejects UNBAN with a missing mask', () => {
|
|
2261
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
2262
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
2263
|
+
services.registerChannel('#home', 'alice');
|
|
2264
|
+
const conn = makeConn();
|
|
2265
|
+
conn.account = 'alice';
|
|
2266
|
+
conn.userModes.registered = true;
|
|
2267
|
+
const ctx = makeCtx(conn, services);
|
|
2268
|
+
|
|
2269
|
+
const out = chanservReducer(conn, privmsg('UNBAN #home'), ctx);
|
|
2270
|
+
|
|
2271
|
+
expect(sentTexts(out.effects)).toContain(
|
|
2272
|
+
':ChanServ!ChanServ@services NOTICE alice :Syntax: UNBAN <#channel> <mask>.',
|
|
2273
|
+
);
|
|
2274
|
+
});
|
|
2275
|
+
});
|
|
2276
|
+
|
|
2277
|
+
// ============================================================================
|
|
2278
|
+
// Help / unknown notices include the new commands
|
|
2279
|
+
// ============================================================================
|
|
2280
|
+
|
|
2281
|
+
describe('chanservReducer — help notice includes the new commands', () => {
|
|
2282
|
+
it('lists OP/DEOP/VOICE/DEVOICE/KICK/BAN/UNBAN on empty body', () => {
|
|
2283
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
2284
|
+
const conn = makeConn();
|
|
2285
|
+
const ctx = makeCtx(conn, services);
|
|
2286
|
+
|
|
2287
|
+
const out = chanservReducer(conn, privmsg(''), ctx);
|
|
2288
|
+
|
|
2289
|
+
const texts = sentTexts(out.effects);
|
|
2290
|
+
for (const verb of ['OP', 'DEOP', 'VOICE', 'DEVOICE', 'KICK', 'BAN', 'UNBAN']) {
|
|
2291
|
+
expect(texts.some((t) => t.includes(verb))).toBe(true);
|
|
2292
|
+
}
|
|
2293
|
+
});
|
|
2294
|
+
|
|
2295
|
+
it('lists the new commands in the unknown-command notice', () => {
|
|
2296
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(NOW) });
|
|
2297
|
+
const conn = makeConn();
|
|
2298
|
+
const ctx = makeCtx(conn, services);
|
|
2299
|
+
|
|
2300
|
+
const out = chanservReducer(conn, privmsg('FROBNICATE'), ctx);
|
|
2301
|
+
|
|
2302
|
+
const texts = sentTexts(out.effects);
|
|
2303
|
+
for (const verb of ['OP', 'DEOP', 'VOICE', 'DEVOICE', 'KICK', 'BAN', 'UNBAN']) {
|
|
2304
|
+
expect(texts.some((t) => t.includes(verb))).toBe(true);
|
|
2305
|
+
}
|
|
2306
|
+
});
|
|
2307
|
+
});
|
|
2308
|
+
|
|
2309
|
+
// ============================================================================
|
|
2310
|
+
// resolveMembershipTarget helper
|
|
2311
|
+
// ============================================================================
|
|
2312
|
+
|
|
2313
|
+
describe('resolveMembershipTarget', () => {
|
|
2314
|
+
it('resolves a present nick (case-insensitive) to its connId', () => {
|
|
2315
|
+
const chan = createChannel({ name: '#foo', nameLower: '#foo', createdAt: 0 });
|
|
2316
|
+
const entry: RosterEntry = { conn: 'c2', nick: 'Bob', op: false, voice: false };
|
|
2317
|
+
chan.members.set('c2', entry);
|
|
2318
|
+
|
|
2319
|
+
expect(resolveMembershipTarget(chan.members, 'bob')).toBe('c2');
|
|
2320
|
+
expect(resolveMembershipTarget(chan.members, 'BOB')).toBe('c2');
|
|
2321
|
+
});
|
|
2322
|
+
|
|
2323
|
+
it('returns undefined when the nick is not on the channel', () => {
|
|
2324
|
+
const chan = createChannel({ name: '#foo', nameLower: '#foo', createdAt: 0 });
|
|
2325
|
+
chan.members.set('c2', { conn: 'c2', nick: 'bob', op: false, voice: false });
|
|
2326
|
+
|
|
2327
|
+
expect(resolveMembershipTarget(chan.members, 'carol')).toBeUndefined();
|
|
2328
|
+
});
|
|
2329
|
+
|
|
2330
|
+
it('returns undefined for an empty roster', () => {
|
|
2331
|
+
const chan = createChannel({ name: '#foo', nameLower: '#foo', createdAt: 0 });
|
|
2332
|
+
|
|
2333
|
+
expect(resolveMembershipTarget(chan.members, 'bob')).toBeUndefined();
|
|
2334
|
+
});
|
|
2335
|
+
});
|