serverless-ircd 0.8.0 → 0.10.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/.github/workflows/deploy-aws.yml +156 -32
- package/.github/workflows/deploy-cf-tcp.yml +11 -9
- package/.github/workflows/deploy-cf.yml +14 -14
- package/CHANGELOG.md +550 -0
- package/README.md +275 -222
- package/apps/aws-stack/README.md +3 -5
- package/apps/aws-stack/bin/aws.ts +82 -9
- package/apps/aws-stack/cdk.json +0 -3
- package/apps/aws-stack/package.json +3 -4
- package/apps/aws-stack/src/aws-stack.ts +177 -52
- package/apps/aws-stack/src/static-site.ts +323 -0
- package/apps/aws-stack/tests/smoke-helpers.test.ts +1 -1
- package/apps/aws-stack/tests/stack.test.ts +267 -92
- package/apps/aws-stack/tests/static-site.test.ts +491 -0
- package/apps/aws-stack/tests/synth-no-bundle.test.ts +0 -1
- package/apps/cf-tcp-container/package.json +2 -3
- package/apps/cf-tcp-container/src/container-server.ts +33 -10
- 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-tcp-container/wrangler.toml +1 -10
- package/apps/cf-worker/package.json +3 -4
- package/apps/cf-worker/wrangler.toml +12 -71
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/server.ts +115 -48
- 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/favicon.ico +0 -0
- package/apps/web/landing/index.html +227 -3
- package/apps/web/package.json +3 -2
- package/apps/web/scripts/build.mjs +91 -6
- package/apps/web/src/build-env.ts +125 -4
- package/apps/web/src/config-schema.ts +20 -6
- package/apps/web/src/render-docs.ts +292 -0
- package/apps/web/static/{config.staging.json → config.prod-aws.json} +3 -2
- package/apps/web/tests/build-env.test.ts +210 -9
- package/apps/web/tests/build-smoke.test.ts +33 -4
- package/apps/web/tests/config-schema.test.ts +149 -25
- 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/AWS-Deployment.md +670 -96
- package/docs/AWS-TCP-Deployment.md +20 -45
- package/docs/Cloudflare-Deployment-Guide.md +87 -113
- package/docs/Cloudflare-TCP-Deployment.md +25 -49
- package/docs/Release-Process.md +27 -23
- package/docs/Services.md +102 -23
- package/docs/WebClientGuide.md +35 -26
- package/package.json +7 -10
- package/packages/aws-adapter/package.json +1 -1
- package/packages/aws-adapter/src/aws-runtime.ts +15 -1
- package/packages/aws-adapter/src/cdk-table-defs.ts +6 -11
- package/packages/aws-adapter/src/config-loader.ts +19 -2
- package/packages/aws-adapter/src/dynamo-services-store.ts +7 -0
- package/packages/aws-adapter/src/handlers/connect.ts +26 -0
- package/packages/aws-adapter/src/handlers/default.ts +190 -123
- package/packages/aws-adapter/src/handlers/index.ts +67 -23
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +13 -8
- package/packages/aws-adapter/src/index.ts +5 -7
- package/packages/aws-adapter/src/origin-allowlist.ts +94 -0
- package/packages/aws-adapter/src/serialize.ts +15 -0
- package/packages/aws-adapter/src/tables.ts +2 -12
- package/packages/aws-adapter/tests/aws-harness.ts +0 -1
- package/packages/aws-adapter/tests/aws-runtime.test.ts +23 -1
- package/packages/aws-adapter/tests/config-loader.test.ts +66 -0
- package/packages/aws-adapter/tests/connect.test.ts +124 -1
- package/packages/aws-adapter/tests/connection-counter.test.ts +17 -0
- package/packages/aws-adapter/tests/default-occ.test.ts +219 -0
- package/packages/aws-adapter/tests/dynamo-services-store-unit.test.ts +11 -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/handlers.test.ts +117 -11
- package/packages/aws-adapter/tests/migrate-accounts-to-services.test.ts +164 -0
- package/packages/aws-adapter/tests/nlb-stream.test.ts +29 -1
- package/packages/aws-adapter/tests/origin-allowlist.test.ts +110 -0
- package/packages/aws-adapter/tests/ping-checker.test.ts +0 -1
- package/packages/aws-adapter/tests/stats.test.ts +0 -3
- package/packages/aws-adapter/tests/sweeper.test.ts +20 -1
- package/packages/aws-adapter/tests/tables.test.ts +1 -8
- package/packages/aws-adapter/tests/transactions.test.ts +0 -1
- package/packages/cf-adapter/package.json +1 -5
- package/packages/cf-adapter/src/cf-runtime.ts +59 -8
- package/packages/cf-adapter/src/channel-do.ts +13 -3
- package/packages/cf-adapter/src/connection-do.ts +284 -115
- package/packages/cf-adapter/src/d1-services-store.ts +63 -26
- package/packages/cf-adapter/src/env.ts +11 -10
- package/packages/cf-adapter/src/index.ts +0 -6
- package/packages/cf-adapter/tests/cf-runtime.test.ts +101 -1
- package/packages/cf-adapter/tests/channel-do.test.ts +118 -1
- package/packages/cf-adapter/tests/connection-do-coverage.test.ts +460 -0
- package/packages/cf-adapter/tests/connection-do-pure.test.ts +222 -51
- package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +62 -38
- package/packages/cf-adapter/tests/d1-services-store.test.ts +53 -2
- package/packages/cf-adapter/tests/serialize.test.ts +25 -0
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/irc-core/package.json +1 -1
- package/packages/irc-core/src/account-migration.ts +140 -0
- package/packages/irc-core/src/commands/account-auth.ts +60 -35
- 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/memoserv.ts +1 -1
- package/packages/irc-core/src/commands/nickserv.ts +138 -15
- package/packages/irc-core/src/commands/registration.ts +28 -17
- package/packages/irc-core/src/commands/sasl.ts +22 -31
- 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/config.ts +35 -9
- package/packages/irc-core/src/credential-hashing.ts +11 -54
- package/packages/irc-core/src/index.ts +1 -0
- package/packages/irc-core/src/ports.ts +159 -179
- package/packages/irc-core/src/state/channel.ts +17 -0
- package/packages/irc-core/src/types.ts +38 -10
- package/packages/irc-core/tests/account-migration.test.ts +133 -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/markread.test.ts +54 -0
- package/packages/irc-core/tests/commands/memoserv.test.ts +19 -0
- package/packages/irc-core/tests/commands/nickserv.test.ts +422 -3
- package/packages/irc-core/tests/commands/oper.test.ts +15 -0
- package/packages/irc-core/tests/commands/registration.test.ts +336 -108
- package/packages/irc-core/tests/commands/sasl.test.ts +194 -169
- package/packages/irc-core/tests/commands/service-aliases.test.ts +52 -0
- package/packages/irc-core/tests/commands/unified-account.test.ts +102 -84
- package/packages/irc-core/tests/credential-hashing.test.ts +0 -78
- package/packages/irc-core/tests/message-store.test.ts +5 -0
- package/packages/irc-core/tests/persistent-services-store.test.ts +71 -12
- package/packages/irc-core/tests/ports.test.ts +71 -0
- package/packages/irc-core/tests/services-store.test.ts +204 -0
- package/packages/irc-core/vitest.config.ts +6 -1
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +80 -44
- package/packages/irc-server/tests/actor.test.ts +384 -50
- 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/pnpm-workspace.yaml +1 -0
- package/scripts/__tests__/deploy-web-aws.test.ts +491 -0
- package/scripts/deploy-web-aws.mjs +290 -0
- package/scripts/package.json +23 -0
- package/scripts/tsconfig.test.json +12 -0
- package/scripts/vitest.config.ts +19 -0
- package/tools/ci-hardening/package.json +1 -1
- package/tools/ci-hardening/src/index.ts +2 -0
- package/tools/ci-hardening/src/validate.ts +57 -0
- package/tools/ci-hardening/tests/deploy-aws-oidc.test.ts +96 -0
- package/tools/ci-hardening/tests/validate.test.ts +42 -0
- 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/migrate-accounts-to-services.ts +270 -0
- package/tools/package.json +2 -1
- package/tools/seed-aws-accounts.ts +35 -10
- package/tools/seed-cf-accounts.ts +42 -9
- 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
- package/packages/aws-adapter/src/account-store.ts +0 -121
- package/packages/aws-adapter/src/dynamo-account-store.ts +0 -95
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +0 -223
- package/packages/aws-adapter/tests/account-store.test.ts +0 -276
- package/packages/cf-adapter/src/d1-account-store.ts +0 -198
- package/packages/cf-adapter/tests/d1-account-store.test.ts +0 -274
- package/packages/irc-core/tests/account-store.test.ts +0 -131
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import pkg from '../../package.json' with { type: 'json' };
|
|
3
3
|
import { cloakHost } from '../../src/cloak';
|
|
4
4
|
import {
|
|
@@ -27,14 +27,7 @@ import {
|
|
|
27
27
|
SequentialIdFactory,
|
|
28
28
|
StaticMotdProvider,
|
|
29
29
|
} from '../../src/ports';
|
|
30
|
-
import type {
|
|
31
|
-
AccountStore,
|
|
32
|
-
AwayStore,
|
|
33
|
-
MotdProvider,
|
|
34
|
-
SaslPayload,
|
|
35
|
-
SaslResult,
|
|
36
|
-
ServicesStore,
|
|
37
|
-
} from '../../src/ports';
|
|
30
|
+
import type { AwayStore, MotdProvider, ServicesStore } from '../../src/ports';
|
|
38
31
|
import { type ConnectionState, createConnection } from '../../src/state/connection';
|
|
39
32
|
import { type Ctx, type ServerConfig, buildCtx } from '../../src/types';
|
|
40
33
|
|
|
@@ -577,12 +570,15 @@ describe('passReducer', () => {
|
|
|
577
570
|
expect(out.effects).toEqual([]);
|
|
578
571
|
});
|
|
579
572
|
|
|
580
|
-
it('
|
|
573
|
+
it('emits 462 when PASS is sent twice before registration completes', () => {
|
|
581
574
|
const state = makeState();
|
|
582
575
|
state.passAttempt = 'first';
|
|
583
576
|
const ctx = makeCtx(state);
|
|
584
577
|
const out = passReducer(state, { command: 'PASS', params: ['second'], tags: {} }, ctx);
|
|
585
|
-
expect(out.
|
|
578
|
+
expect(out.effects).toEqual<EffectType[]>([
|
|
579
|
+
Effect.send('c1', [L(':irc.example.com 462 * :You may not reregister')]),
|
|
580
|
+
]);
|
|
581
|
+
expect(out.state.passAttempt).toBe('first');
|
|
586
582
|
});
|
|
587
583
|
|
|
588
584
|
it('emits 461 when no password is supplied', () => {
|
|
@@ -595,14 +591,129 @@ describe('passReducer', () => {
|
|
|
595
591
|
expect(out.state.passAttempt).toBeUndefined();
|
|
596
592
|
});
|
|
597
593
|
|
|
598
|
-
it('emits
|
|
594
|
+
it('emits 461 when no password is supplied even after registration', () => {
|
|
599
595
|
const state = registeredState();
|
|
600
596
|
const ctx = makeCtx(state);
|
|
601
|
-
const out = passReducer(state, { command: 'PASS', params: [
|
|
597
|
+
const out = passReducer(state, { command: 'PASS', params: [], tags: {} }, ctx);
|
|
602
598
|
expect(out.effects).toEqual<EffectType[]>([
|
|
603
|
-
Effect.send('c1', [L(':irc.example.com
|
|
599
|
+
Effect.send('c1', [L(':irc.example.com 461 alice PASS :Not enough parameters')]),
|
|
604
600
|
]);
|
|
605
|
-
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
// ============================================================================
|
|
605
|
+
// passReducer — post-registration PASS (late account login)
|
|
606
|
+
// ============================================================================
|
|
607
|
+
|
|
608
|
+
describe('passReducer — post-registration PASS <nick>:<password>', () => {
|
|
609
|
+
it('logs in when PASS <nick>:<password> matches an account after registration', () => {
|
|
610
|
+
const state = registeredState();
|
|
611
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
612
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
613
|
+
const ctx = makeCtxWithServices(state, services);
|
|
614
|
+
const out = passReducer(state, { command: 'PASS', params: ['alice:hunter2'], tags: {} }, ctx);
|
|
615
|
+
|
|
616
|
+
expect(out.state.account).toBe('alice');
|
|
617
|
+
expect(out.state.userModes.registered).toBe(true);
|
|
618
|
+
const texts = sentTexts(out.effects);
|
|
619
|
+
expect(texts).toContain(
|
|
620
|
+
`:${SV} 900 alice alice!alice@example.com alice :You are now logged in as alice`,
|
|
621
|
+
);
|
|
622
|
+
expect(texts).toContain(`:${SV} 903 alice :SASL authentication successful`);
|
|
623
|
+
expect(texts.some((t) => t.includes(' 462 '))).toBe(false);
|
|
624
|
+
expect(out.effects.some((e) => e.tag === 'Disconnect')).toBe(false);
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
it('does not log in or disconnect when the password is wrong', () => {
|
|
628
|
+
const state = registeredState();
|
|
629
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
630
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
631
|
+
const ctx = makeCtxWithServices(state, services);
|
|
632
|
+
const out = passReducer(state, { command: 'PASS', params: ['alice:wrong'], tags: {} }, ctx);
|
|
633
|
+
|
|
634
|
+
expect(out.state.account).toBeUndefined();
|
|
635
|
+
expect(out.state.userModes.registered).toBe(false);
|
|
636
|
+
expect(out.effects).toEqual([]);
|
|
637
|
+
expect(out.effects.some((e) => e.tag === 'Disconnect')).toBe(false);
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it('is indistinguishable from an unknown nick (no auth numerics, no disconnect)', () => {
|
|
641
|
+
const state = registeredState();
|
|
642
|
+
state.nick = 'nobody';
|
|
643
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
644
|
+
const ctx = makeCtxWithServices(state, services);
|
|
645
|
+
const out = passReducer(state, { command: 'PASS', params: ['nobody:wrong'], tags: {} }, ctx);
|
|
646
|
+
|
|
647
|
+
expect(out.state.account).toBeUndefined();
|
|
648
|
+
expect(out.effects).toEqual([]);
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
it('equalises timing with an extra dummy verify on failure', () => {
|
|
652
|
+
const state = registeredState();
|
|
653
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
654
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
655
|
+
const verifyNick = vi.spyOn(services, 'verifyNick');
|
|
656
|
+
const ctx = makeCtxWithServices(state, services);
|
|
657
|
+
passReducer(state, { command: 'PASS', params: ['alice:wrong'], tags: {} }, ctx);
|
|
658
|
+
|
|
659
|
+
// Two calls: the real (failed) verify against `alice`, then the
|
|
660
|
+
// timing-equalising dummy verify.
|
|
661
|
+
expect(verifyNick).toHaveBeenCalledTimes(2);
|
|
662
|
+
expect(verifyNick.mock.calls[0]?.[0]).toBe('alice');
|
|
663
|
+
expect(verifyNick.mock.calls[0]?.[1]).toBe('wrong');
|
|
664
|
+
// The second call is the dummy; its nick is the sentinel, not 'alice'.
|
|
665
|
+
expect(verifyNick.mock.calls[1]?.[0]).not.toBe('alice');
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
it('does not log in when the payload nick does not match state.nick', () => {
|
|
669
|
+
const state = registeredState();
|
|
670
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
671
|
+
services.registerNick('bob', 'hunter2', 'bob@example.com');
|
|
672
|
+
const verifyNick = vi.spyOn(services, 'verifyNick');
|
|
673
|
+
const ctx = makeCtxWithServices(state, services);
|
|
674
|
+
const out = passReducer(state, { command: 'PASS', params: ['bob:hunter2'], tags: {} }, ctx);
|
|
675
|
+
|
|
676
|
+
expect(out.state.account).toBeUndefined();
|
|
677
|
+
expect(verifyNick).not.toHaveBeenCalled();
|
|
678
|
+
expect(out.effects).toEqual([]);
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
it('ignores a bare PASS value (no colon) after registration — no 462, no disconnect', () => {
|
|
682
|
+
const state = registeredState();
|
|
683
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
684
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
685
|
+
const verifyNick = vi.spyOn(services, 'verifyNick');
|
|
686
|
+
const ctx = makeCtxWithServices(state, services);
|
|
687
|
+
const out = passReducer(state, { command: 'PASS', params: ['serverpw'], tags: {} }, ctx);
|
|
688
|
+
|
|
689
|
+
expect(out.state.account).toBeUndefined();
|
|
690
|
+
expect(out.effects).toEqual([]);
|
|
691
|
+
expect(verifyNick).not.toHaveBeenCalled();
|
|
692
|
+
expect(out.effects.some((e) => e.tag === 'Disconnect')).toBe(false);
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
it('ignores a late PASS when the connection is already identified', () => {
|
|
696
|
+
const state = registeredState();
|
|
697
|
+
state.account = 'alice';
|
|
698
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
699
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
700
|
+
const verifyNick = vi.spyOn(services, 'verifyNick');
|
|
701
|
+
const ctx = makeCtxWithServices(state, services);
|
|
702
|
+
const out = passReducer(state, { command: 'PASS', params: ['alice:hunter2'], tags: {} }, ctx);
|
|
703
|
+
|
|
704
|
+
expect(verifyNick).not.toHaveBeenCalled();
|
|
705
|
+
expect(out.effects).toEqual([]);
|
|
706
|
+
expect(out.effects.some((e) => e.tag === 'Disconnect')).toBe(false);
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
it('ignores a late PASS <nick>:<password> when no ServicesStore is bound', () => {
|
|
710
|
+
const state = registeredState();
|
|
711
|
+
const ctx = makeCtx(state);
|
|
712
|
+
const out = passReducer(state, { command: 'PASS', params: ['alice:hunter2'], tags: {} }, ctx);
|
|
713
|
+
|
|
714
|
+
expect(out.state.account).toBeUndefined();
|
|
715
|
+
expect(out.effects).toEqual([]);
|
|
716
|
+
expect(out.effects.some((e) => e.tag === 'Disconnect')).toBe(false);
|
|
606
717
|
});
|
|
607
718
|
});
|
|
608
719
|
|
|
@@ -1522,32 +1633,6 @@ describe('emitWelcomeIfReady — OperServ AKILL admission', () => {
|
|
|
1522
1633
|
// PASS-based account auth (NICK + PASS <nick>:<password> + USER → identified)
|
|
1523
1634
|
// ============================================================================
|
|
1524
1635
|
|
|
1525
|
-
/** A capture-and-respond fake AccountStore (mirrors the sasl.test.ts fake). */
|
|
1526
|
-
class FakeAccountStore implements AccountStore {
|
|
1527
|
-
readonly calls: Array<{ mech: string; payload: SaslPayload }> = [];
|
|
1528
|
-
constructor(private readonly result: SaslResult) {}
|
|
1529
|
-
verify(mech: string, payload: SaslPayload): SaslResult {
|
|
1530
|
-
this.calls.push({ mech, payload });
|
|
1531
|
-
return this.result;
|
|
1532
|
-
}
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
/** Like {@link makeCtx} but also threads a bound AccountStore. */
|
|
1536
|
-
function makeCtxWithAccounts(
|
|
1537
|
-
state: ConnectionState,
|
|
1538
|
-
accounts: AccountStore,
|
|
1539
|
-
clock = new FakeClock(1_000),
|
|
1540
|
-
): Ctx {
|
|
1541
|
-
return buildCtx({
|
|
1542
|
-
serverConfig,
|
|
1543
|
-
clock,
|
|
1544
|
-
ids: new SequentialIdFactory(),
|
|
1545
|
-
motd: EmptyMotdProvider,
|
|
1546
|
-
connection: state,
|
|
1547
|
-
accounts,
|
|
1548
|
-
});
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
1636
|
const SV = 'irc.example.com';
|
|
1552
1637
|
const L2 = (text: string): RawLine => ({ text });
|
|
1553
1638
|
|
|
@@ -1558,8 +1643,9 @@ describe('emitWelcomeIfReady — PASS-based account auth (success)', () => {
|
|
|
1558
1643
|
state.user = 'alice';
|
|
1559
1644
|
state.host = 'example.com';
|
|
1560
1645
|
state.passAttempt = 'alice:hunter2';
|
|
1561
|
-
const
|
|
1562
|
-
|
|
1646
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1647
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1648
|
+
const ctx = makeCtxWithServices(state, services);
|
|
1563
1649
|
|
|
1564
1650
|
const effects = emitWelcomeIfReady(state, ctx);
|
|
1565
1651
|
|
|
@@ -1576,11 +1662,10 @@ describe('emitWelcomeIfReady — PASS-based account auth (success)', () => {
|
|
|
1576
1662
|
|
|
1577
1663
|
// ---- helpers for the broader PASS-auth coverage ----
|
|
1578
1664
|
|
|
1579
|
-
/** Flexible ctx that binds any combination of
|
|
1665
|
+
/** Flexible ctx that binds any combination of services/away stores. */
|
|
1580
1666
|
function makeCtxPassAuth(
|
|
1581
1667
|
state: ConnectionState,
|
|
1582
1668
|
opts: {
|
|
1583
|
-
accounts?: AccountStore;
|
|
1584
1669
|
services?: ServicesStore;
|
|
1585
1670
|
away?: AwayStore;
|
|
1586
1671
|
serverPassword?: string;
|
|
@@ -1595,7 +1680,6 @@ function makeCtxPassAuth(
|
|
|
1595
1680
|
ids: new SequentialIdFactory(),
|
|
1596
1681
|
motd: EmptyMotdProvider,
|
|
1597
1682
|
connection: state,
|
|
1598
|
-
...(opts.accounts !== undefined ? { accounts: opts.accounts } : {}),
|
|
1599
1683
|
...(opts.services !== undefined ? { services: opts.services } : {}),
|
|
1600
1684
|
...(opts.away !== undefined ? { away: opts.away } : {}),
|
|
1601
1685
|
});
|
|
@@ -1607,16 +1691,16 @@ function sentTexts(effects: EffectType[]): string[] {
|
|
|
1607
1691
|
}
|
|
1608
1692
|
|
|
1609
1693
|
describe('emitWelcomeIfReady — PASS-based account auth (account-effects parity with SASL)', () => {
|
|
1610
|
-
it('forwards the parsed credentials to
|
|
1694
|
+
it('forwards the parsed credentials to services.verifyNick as a PLAIN verify', () => {
|
|
1611
1695
|
const state = makeState();
|
|
1612
1696
|
state.nick = 'alice';
|
|
1613
1697
|
state.user = 'alice';
|
|
1614
1698
|
state.passAttempt = 'alice:hunter2';
|
|
1615
|
-
const
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1699
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1700
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1701
|
+
const verifyNick = vi.spyOn(services, 'verifyNick');
|
|
1702
|
+
emitWelcomeIfReady(state, makeCtxPassAuth(state, { services }));
|
|
1703
|
+
expect(verifyNick).toHaveBeenCalledWith('alice', 'hunter2');
|
|
1620
1704
|
});
|
|
1621
1705
|
|
|
1622
1706
|
it('stamps read-only user mode `r` on a successful PASS-auth', () => {
|
|
@@ -1624,8 +1708,9 @@ describe('emitWelcomeIfReady — PASS-based account auth (account-effects parity
|
|
|
1624
1708
|
state.nick = 'alice';
|
|
1625
1709
|
state.user = 'alice';
|
|
1626
1710
|
state.passAttempt = 'alice:hunter2';
|
|
1627
|
-
const
|
|
1628
|
-
|
|
1711
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1712
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1713
|
+
emitWelcomeIfReady(state, makeCtxPassAuth(state, { services }));
|
|
1629
1714
|
expect(state.userModes.registered).toBe(true);
|
|
1630
1715
|
});
|
|
1631
1716
|
|
|
@@ -1634,10 +1719,10 @@ describe('emitWelcomeIfReady — PASS-based account auth (account-effects parity
|
|
|
1634
1719
|
state.nick = 'alice';
|
|
1635
1720
|
state.user = 'alice';
|
|
1636
1721
|
state.passAttempt = 'alice:hunter2';
|
|
1637
|
-
const accounts = new FakeAccountStore({ ok: true, account: 'alice' });
|
|
1638
1722
|
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1723
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1639
1724
|
services.setLastReadMarker('alice', '#foo', 'm1');
|
|
1640
|
-
emitWelcomeIfReady(state, makeCtxPassAuth(state, {
|
|
1725
|
+
emitWelcomeIfReady(state, makeCtxPassAuth(state, { services }));
|
|
1641
1726
|
expect(state.lastReadMarkers?.get('#foo')).toBe('m1');
|
|
1642
1727
|
});
|
|
1643
1728
|
|
|
@@ -1646,10 +1731,11 @@ describe('emitWelcomeIfReady — PASS-based account auth (account-effects parity
|
|
|
1646
1731
|
state.nick = 'alice';
|
|
1647
1732
|
state.user = 'alice';
|
|
1648
1733
|
state.passAttempt = 'alice:hunter2';
|
|
1649
|
-
const
|
|
1734
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1735
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1650
1736
|
const away = new InMemoryAwayStore();
|
|
1651
1737
|
away.set('alice', 'brb');
|
|
1652
|
-
const effects = emitWelcomeIfReady(state, makeCtxPassAuth(state, {
|
|
1738
|
+
const effects = emitWelcomeIfReady(state, makeCtxPassAuth(state, { services, away }));
|
|
1653
1739
|
expect(state.away).toBe('brb');
|
|
1654
1740
|
expect(sentTexts(effects)).toContain(`:${SV} 306 alice :You have been marked as being away`);
|
|
1655
1741
|
});
|
|
@@ -1663,18 +1749,87 @@ describe('emitWelcomeIfReady — PASS-based account auth (account-effects parity
|
|
|
1663
1749
|
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1664
1750
|
services.registerNick('bob', 'pw', 'bob@example.com');
|
|
1665
1751
|
services.recordMemo({ from: 'bob', to: 'alice', body: 'hi', sentAt: 0 });
|
|
1666
|
-
const
|
|
1667
|
-
const effects = emitWelcomeIfReady(state, makeCtxPassAuth(state, { accounts, services }));
|
|
1752
|
+
const effects = emitWelcomeIfReady(state, makeCtxPassAuth(state, { services }));
|
|
1668
1753
|
expect(sentTexts(effects).some((t) => t.includes('[Memo #') && t.includes('hi'))).toBe(true);
|
|
1669
1754
|
});
|
|
1670
1755
|
|
|
1756
|
+
it('applies the assigned HostServ vhost on PASS-auth success so 001 carries the vhost', () => {
|
|
1757
|
+
const state = makeState();
|
|
1758
|
+
state.nick = 'alice';
|
|
1759
|
+
state.user = 'alice';
|
|
1760
|
+
state.host = 'real.example.net';
|
|
1761
|
+
state.passAttempt = 'alice:hunter2';
|
|
1762
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1763
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1764
|
+
services.setVhost('alice', 'cool.example.net');
|
|
1765
|
+
const ctx = makeCtxPassAuth(state, { services });
|
|
1766
|
+
|
|
1767
|
+
const effects = emitWelcomeIfReady(state, ctx);
|
|
1768
|
+
|
|
1769
|
+
expect(state.account).toBe('alice');
|
|
1770
|
+
expect(state.vhostActive).toBe(true);
|
|
1771
|
+
expect(state.host).toBe('cool.example.net');
|
|
1772
|
+
expect(state.preVhostHost).toBe('real.example.net');
|
|
1773
|
+
const welcome = sentTexts(effects).find((t) => t.startsWith(`:${SV} 001 `));
|
|
1774
|
+
expect(welcome).toBeDefined();
|
|
1775
|
+
expect(welcome).toContain('cool.example.net');
|
|
1776
|
+
});
|
|
1777
|
+
|
|
1778
|
+
it('does not apply a vhost on PASS-auth success when none is assigned', () => {
|
|
1779
|
+
const state = makeState();
|
|
1780
|
+
state.nick = 'alice';
|
|
1781
|
+
state.user = 'alice';
|
|
1782
|
+
state.host = 'real.example.net';
|
|
1783
|
+
state.passAttempt = 'alice:hunter2';
|
|
1784
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1785
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1786
|
+
const ctx = makeCtxPassAuth(state, { services });
|
|
1787
|
+
|
|
1788
|
+
emitWelcomeIfReady(state, ctx);
|
|
1789
|
+
|
|
1790
|
+
expect(state.vhostActive).toBeUndefined();
|
|
1791
|
+
expect(state.host).toBe('real.example.net');
|
|
1792
|
+
});
|
|
1793
|
+
|
|
1794
|
+
it('does not cloak over an active vhost applied during PASS-auth (vhost wins)', () => {
|
|
1795
|
+
const state = makeState();
|
|
1796
|
+
state.nick = 'alice';
|
|
1797
|
+
state.user = 'alice';
|
|
1798
|
+
state.host = '203.0.113.5';
|
|
1799
|
+
state.passAttempt = 'alice:hunter2';
|
|
1800
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1801
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1802
|
+
services.setVhost('alice', 'cool.example.net');
|
|
1803
|
+
const ctx: Ctx = buildCtx({
|
|
1804
|
+
serverConfig: { ...serverConfig, cloaking: { enabled: true, secret: 's3cret' } },
|
|
1805
|
+
clock: new FakeClock(1_000),
|
|
1806
|
+
ids: new SequentialIdFactory(),
|
|
1807
|
+
motd: EmptyMotdProvider,
|
|
1808
|
+
connection: state,
|
|
1809
|
+
services,
|
|
1810
|
+
});
|
|
1811
|
+
|
|
1812
|
+
const effects = emitWelcomeIfReady(state, ctx);
|
|
1813
|
+
|
|
1814
|
+
// The vhost was applied by the PASS-auth success chain; cloaking must not
|
|
1815
|
+
// clobber it, and the 001 welcome must carry the vhost, not the cloak.
|
|
1816
|
+
expect(state.vhostActive).toBe(true);
|
|
1817
|
+
expect(state.host).toBe('cool.example.net');
|
|
1818
|
+
const welcome = sentTexts(effects).find((t) => t.startsWith(`:${SV} 001 `));
|
|
1819
|
+
expect(welcome).toContain('cool.example.net');
|
|
1820
|
+
expect(welcome).not.toContain(
|
|
1821
|
+
cloakHost('203.0.113.5', { enabled: true, secret: 's3cret' }, 'ExampleNet'),
|
|
1822
|
+
);
|
|
1823
|
+
});
|
|
1824
|
+
|
|
1671
1825
|
it('records the payload nick as the canonical account name', () => {
|
|
1672
1826
|
const state = makeState();
|
|
1673
1827
|
state.nick = 'alice';
|
|
1674
1828
|
state.user = 'alice';
|
|
1675
1829
|
state.passAttempt = 'alice:hunter2';
|
|
1676
|
-
const
|
|
1677
|
-
|
|
1830
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1831
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1832
|
+
emitWelcomeIfReady(state, makeCtxPassAuth(state, { services }));
|
|
1678
1833
|
expect(state.account).toBe('alice');
|
|
1679
1834
|
});
|
|
1680
1835
|
});
|
|
@@ -1685,8 +1840,9 @@ describe('emitWelcomeIfReady — PASS-based account auth (failures)', () => {
|
|
|
1685
1840
|
state.nick = 'alice';
|
|
1686
1841
|
state.user = 'alice';
|
|
1687
1842
|
state.passAttempt = 'alice:wrong';
|
|
1688
|
-
const
|
|
1689
|
-
|
|
1843
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1844
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1845
|
+
const effects = emitWelcomeIfReady(state, makeCtxPassAuth(state, { services }));
|
|
1690
1846
|
|
|
1691
1847
|
expect(state.account).toBeUndefined();
|
|
1692
1848
|
expect(state.userModes.registered).toBe(false);
|
|
@@ -1702,8 +1858,8 @@ describe('emitWelcomeIfReady — PASS-based account auth (failures)', () => {
|
|
|
1702
1858
|
state.nick = 'nobody';
|
|
1703
1859
|
state.user = 'nobody';
|
|
1704
1860
|
state.passAttempt = 'nobody:wrong';
|
|
1705
|
-
const
|
|
1706
|
-
const effects = emitWelcomeIfReady(state, makeCtxPassAuth(state, {
|
|
1861
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1862
|
+
const effects = emitWelcomeIfReady(state, makeCtxPassAuth(state, { services }));
|
|
1707
1863
|
|
|
1708
1864
|
expect(state.account).toBeUndefined();
|
|
1709
1865
|
expect(state.registration).toBe('registered');
|
|
@@ -1719,17 +1875,15 @@ describe('emitWelcomeIfReady — PASS-based account auth (failures)', () => {
|
|
|
1719
1875
|
state.nick = 'alice';
|
|
1720
1876
|
state.user = 'alice';
|
|
1721
1877
|
state.passAttempt = 'alice:wrong';
|
|
1722
|
-
const
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
expect(accounts.calls[1]?.mech).toBe('PLAIN');
|
|
1732
|
-
expect(accounts.calls[1]?.payload.kind).toBe('PLAIN');
|
|
1878
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1879
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1880
|
+
const verifyNick = vi.spyOn(services, 'verifyNick');
|
|
1881
|
+
emitWelcomeIfReady(state, makeCtxPassAuth(state, { services }));
|
|
1882
|
+
|
|
1883
|
+
expect(verifyNick).toHaveBeenCalledTimes(2);
|
|
1884
|
+
expect(verifyNick.mock.calls[0]?.[0]).toBe('alice');
|
|
1885
|
+
expect(verifyNick.mock.calls[0]?.[1]).toBe('wrong');
|
|
1886
|
+
expect(verifyNick.mock.calls[1]?.[0]).not.toBe('alice');
|
|
1733
1887
|
});
|
|
1734
1888
|
|
|
1735
1889
|
it('treats a payload nick that does not match state.nick as a failed auth (no silent login)', () => {
|
|
@@ -1737,19 +1891,21 @@ describe('emitWelcomeIfReady — PASS-based account auth (failures)', () => {
|
|
|
1737
1891
|
state.nick = 'alice';
|
|
1738
1892
|
state.user = 'alice';
|
|
1739
1893
|
state.passAttempt = 'bob:hunter2';
|
|
1740
|
-
const
|
|
1741
|
-
|
|
1894
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1895
|
+
services.registerNick('bob', 'hunter2', 'bob@example.com');
|
|
1896
|
+
const verifyNick = vi.spyOn(services, 'verifyNick');
|
|
1897
|
+
const effects = emitWelcomeIfReady(state, makeCtxPassAuth(state, { services }));
|
|
1742
1898
|
|
|
1743
1899
|
expect(state.account).toBeUndefined();
|
|
1744
1900
|
expect(state.registration).toBe('registered');
|
|
1745
1901
|
// The store is never consulted on a nick mismatch.
|
|
1746
|
-
expect(
|
|
1902
|
+
expect(verifyNick).not.toHaveBeenCalled();
|
|
1747
1903
|
expect(sentTexts(effects).some((t) => t.includes(' 900 '))).toBe(false);
|
|
1748
1904
|
});
|
|
1749
1905
|
});
|
|
1750
1906
|
|
|
1751
1907
|
describe('emitWelcomeIfReady — PASS-auth interaction with the server-password gate', () => {
|
|
1752
|
-
it('ignores the <nick>:<password> form when no
|
|
1908
|
+
it('ignores the <nick>:<password> form when no ServicesStore is bound (open gate)', () => {
|
|
1753
1909
|
const state = makeState();
|
|
1754
1910
|
state.nick = 'alice';
|
|
1755
1911
|
state.user = 'alice';
|
|
@@ -1761,7 +1917,28 @@ describe('emitWelcomeIfReady — PASS-auth interaction with the server-password
|
|
|
1761
1917
|
expect(sentTexts(effects).some((t) => t.includes(' 464 '))).toBe(false);
|
|
1762
1918
|
});
|
|
1763
1919
|
|
|
1764
|
-
it('
|
|
1920
|
+
it('a SASL-identified connection (state.account set) satisfies the gate with no credential-store read', () => {
|
|
1921
|
+
const state = makeState();
|
|
1922
|
+
state.nick = 'alice';
|
|
1923
|
+
state.user = 'alice';
|
|
1924
|
+
state.account = 'alice'; // SASL-identified before registration completion.
|
|
1925
|
+
state.passAttempt = 'alice:hunter2';
|
|
1926
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1927
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1928
|
+
const verifyNick = vi.spyOn(services, 'verifyNick');
|
|
1929
|
+
const effects = emitWelcomeIfReady(
|
|
1930
|
+
state,
|
|
1931
|
+
makeCtxPassAuth(state, { services, serverPassword: 's3cret' }),
|
|
1932
|
+
);
|
|
1933
|
+
|
|
1934
|
+
// The gate is satisfied by state.account; no PASS-auth verify fires.
|
|
1935
|
+
expect(verifyNick).not.toHaveBeenCalled();
|
|
1936
|
+
expect(state.registration).toBe('registered');
|
|
1937
|
+
expect(sentTexts(effects).some((t) => t.includes(' 464 '))).toBe(false);
|
|
1938
|
+
expect(sentTexts(effects).some((t) => t.includes(' 900 '))).toBe(false);
|
|
1939
|
+
});
|
|
1940
|
+
|
|
1941
|
+
it('collapses to the server-password gate (464) when no ServicesStore is bound and the colon form does not match', () => {
|
|
1765
1942
|
const state = makeState();
|
|
1766
1943
|
state.nick = 'alice';
|
|
1767
1944
|
state.user = 'alice';
|
|
@@ -1780,10 +1957,11 @@ describe('emitWelcomeIfReady — PASS-auth interaction with the server-password
|
|
|
1780
1957
|
state.nick = 'alice';
|
|
1781
1958
|
state.user = 'alice';
|
|
1782
1959
|
state.passAttempt = 'alice:wrong';
|
|
1783
|
-
const
|
|
1960
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1961
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1784
1962
|
const effects = emitWelcomeIfReady(
|
|
1785
1963
|
state,
|
|
1786
|
-
makeCtxPassAuth(state, {
|
|
1964
|
+
makeCtxPassAuth(state, { services, serverPassword: 's3cret' }),
|
|
1787
1965
|
);
|
|
1788
1966
|
|
|
1789
1967
|
expect(state.registration).not.toBe('registered');
|
|
@@ -1800,10 +1978,12 @@ describe('emitWelcomeIfReady — PASS-auth interaction with the server-password
|
|
|
1800
1978
|
state.user = 'alice';
|
|
1801
1979
|
state.account = 'alice'; // SASL-identified before registration completion
|
|
1802
1980
|
state.passAttempt = 'alice:hunter2';
|
|
1803
|
-
const
|
|
1804
|
-
|
|
1981
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1982
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1983
|
+
const verifyNick = vi.spyOn(services, 'verifyNick');
|
|
1984
|
+
const effects = emitWelcomeIfReady(state, makeCtxPassAuth(state, { services }));
|
|
1805
1985
|
|
|
1806
|
-
expect(
|
|
1986
|
+
expect(verifyNick).not.toHaveBeenCalled();
|
|
1807
1987
|
expect(state.registration).toBe('registered');
|
|
1808
1988
|
// No 900/903 are re-emitted by PASS-auth; SASL already emitted them.
|
|
1809
1989
|
expect(sentTexts(effects).some((t) => t.includes(' 900 '))).toBe(false);
|
|
@@ -1814,13 +1994,15 @@ describe('emitWelcomeIfReady — PASS-auth interaction with the server-password
|
|
|
1814
1994
|
state.nick = 'alice';
|
|
1815
1995
|
state.user = 'alice';
|
|
1816
1996
|
state.passAttempt = 's3cret';
|
|
1817
|
-
const
|
|
1997
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
1998
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
1999
|
+
const verifyNick = vi.spyOn(services, 'verifyNick');
|
|
1818
2000
|
const effects = emitWelcomeIfReady(
|
|
1819
2001
|
state,
|
|
1820
|
-
makeCtxPassAuth(state, {
|
|
2002
|
+
makeCtxPassAuth(state, { services, serverPassword: 's3cret' }),
|
|
1821
2003
|
);
|
|
1822
2004
|
|
|
1823
|
-
expect(
|
|
2005
|
+
expect(verifyNick).not.toHaveBeenCalled();
|
|
1824
2006
|
expect(state.account).toBeUndefined();
|
|
1825
2007
|
expect(state.registration).toBe('registered');
|
|
1826
2008
|
expect(sentTexts(effects).some((t) => t.includes(' 464 '))).toBe(false);
|
|
@@ -1833,8 +2015,9 @@ describe('passBasedAccountAuth — unit (PASS before NICK, parsing, edge cases)'
|
|
|
1833
2015
|
state.user = 'alice';
|
|
1834
2016
|
// state.nick intentionally undefined — PASS arrived before NICK.
|
|
1835
2017
|
state.passAttempt = 'alice:hunter2';
|
|
1836
|
-
const
|
|
1837
|
-
|
|
2018
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
2019
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
2020
|
+
const effects = passBasedAccountAuth(state, makeCtxPassAuth(state, { services }));
|
|
1838
2021
|
|
|
1839
2022
|
expect(state.account).toBe('alice');
|
|
1840
2023
|
expect(effects.some((e) => e.tag === 'Send')).toBe(true);
|
|
@@ -1843,26 +2026,31 @@ describe('passBasedAccountAuth — unit (PASS before NICK, parsing, edge cases)'
|
|
|
1843
2026
|
it('returns no effects when no PASS was sent', () => {
|
|
1844
2027
|
const state = makeState();
|
|
1845
2028
|
state.nick = 'alice';
|
|
1846
|
-
const
|
|
1847
|
-
|
|
2029
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
2030
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
2031
|
+
expect(passBasedAccountAuth(state, makeCtxPassAuth(state, { services }))).toEqual([]);
|
|
1848
2032
|
});
|
|
1849
2033
|
|
|
1850
2034
|
it('returns no effects for a bare PASS value (no colon)', () => {
|
|
1851
2035
|
const state = makeState();
|
|
1852
2036
|
state.nick = 'alice';
|
|
1853
2037
|
state.passAttempt = 's3cret';
|
|
1854
|
-
const
|
|
1855
|
-
|
|
1856
|
-
|
|
2038
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
2039
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
2040
|
+
const verifyNick = vi.spyOn(services, 'verifyNick');
|
|
2041
|
+
expect(passBasedAccountAuth(state, makeCtxPassAuth(state, { services }))).toEqual([]);
|
|
2042
|
+
expect(verifyNick).not.toHaveBeenCalled();
|
|
1857
2043
|
});
|
|
1858
2044
|
|
|
1859
2045
|
it('returns no effects when the nick portion is empty (`:password`)', () => {
|
|
1860
2046
|
const state = makeState();
|
|
1861
2047
|
state.nick = 'alice';
|
|
1862
2048
|
state.passAttempt = ':hunter2';
|
|
1863
|
-
const
|
|
1864
|
-
|
|
1865
|
-
|
|
2049
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
2050
|
+
services.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
2051
|
+
const verifyNick = vi.spyOn(services, 'verifyNick');
|
|
2052
|
+
expect(passBasedAccountAuth(state, makeCtxPassAuth(state, { services }))).toEqual([]);
|
|
2053
|
+
expect(verifyNick).not.toHaveBeenCalled();
|
|
1866
2054
|
});
|
|
1867
2055
|
|
|
1868
2056
|
it('splits only on the first colon so a password may contain colons', () => {
|
|
@@ -1870,16 +2058,13 @@ describe('passBasedAccountAuth — unit (PASS before NICK, parsing, edge cases)'
|
|
|
1870
2058
|
state.nick = 'alice';
|
|
1871
2059
|
state.user = 'alice';
|
|
1872
2060
|
state.passAttempt = 'alice:p:ass:word';
|
|
1873
|
-
const
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
username: 'alice',
|
|
1878
|
-
password: 'p:ass:word',
|
|
1879
|
-
});
|
|
2061
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
2062
|
+
services.registerNick('alice', 'p:ass:word', 'alice@example.com');
|
|
2063
|
+
passBasedAccountAuth(state, makeCtxPassAuth(state, { services }));
|
|
2064
|
+
expect(state.account).toBe('alice');
|
|
1880
2065
|
});
|
|
1881
2066
|
|
|
1882
|
-
it('returns no effects when no
|
|
2067
|
+
it('returns no effects when no ServicesStore is bound', () => {
|
|
1883
2068
|
const state = makeState();
|
|
1884
2069
|
state.nick = 'alice';
|
|
1885
2070
|
state.passAttempt = 'alice:hunter2';
|
|
@@ -1940,3 +2125,46 @@ describe('applyAccountSuccess — shared account-login helper', () => {
|
|
|
1940
2125
|
expect(texts).toContain(`:${SV} 903 alice :SASL authentication successful`);
|
|
1941
2126
|
});
|
|
1942
2127
|
});
|
|
2128
|
+
|
|
2129
|
+
// ============================================================================
|
|
2130
|
+
// applyAccountSuccess — HostServ vhost auto-apply (SASL + PASS-auth share it)
|
|
2131
|
+
// ============================================================================
|
|
2132
|
+
|
|
2133
|
+
describe('applyAccountSuccess — HostServ vhost auto-apply', () => {
|
|
2134
|
+
it('applies an assigned vhost to state.host and sets vhostActive when services hold a vhost for the account', () => {
|
|
2135
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
2136
|
+
services.setVhost('alice', 'cool.example.net');
|
|
2137
|
+
const state = registeredState();
|
|
2138
|
+
state.host = 'real.example.net';
|
|
2139
|
+
const ctx = makeCtxWithServices(state, services);
|
|
2140
|
+
|
|
2141
|
+
applyAccountSuccess(state, ctx, 'alice');
|
|
2142
|
+
|
|
2143
|
+
expect(state.vhostActive).toBe(true);
|
|
2144
|
+
expect(state.host).toBe('cool.example.net');
|
|
2145
|
+
expect(state.preVhostHost).toBe('real.example.net');
|
|
2146
|
+
});
|
|
2147
|
+
|
|
2148
|
+
it('leaves state.host unchanged when no vhost is assigned to the account', () => {
|
|
2149
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
2150
|
+
const state = registeredState();
|
|
2151
|
+
state.host = 'real.example.net';
|
|
2152
|
+
const ctx = makeCtxWithServices(state, services);
|
|
2153
|
+
|
|
2154
|
+
applyAccountSuccess(state, ctx, 'alice');
|
|
2155
|
+
|
|
2156
|
+
expect(state.vhostActive).toBeUndefined();
|
|
2157
|
+
expect(state.host).toBe('real.example.net');
|
|
2158
|
+
});
|
|
2159
|
+
|
|
2160
|
+
it('does not emit a CHGHOST broadcast when no channels are joined (pre-JOIN identify)', () => {
|
|
2161
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(1_000) });
|
|
2162
|
+
services.setVhost('alice', 'cool.example.net');
|
|
2163
|
+
const state = registeredState();
|
|
2164
|
+
const ctx = makeCtxWithServices(state, services);
|
|
2165
|
+
|
|
2166
|
+
const effects = applyAccountSuccess(state, ctx, 'alice');
|
|
2167
|
+
|
|
2168
|
+
expect(effects.filter((e) => e.tag === 'Broadcast')).toEqual<EffectType[]>([]);
|
|
2169
|
+
});
|
|
2170
|
+
});
|