mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Completes fluent switching across signing methods (NIP-07 / nsec / NIP-46):
- NIP-46 switching: it had the same bug — switchProfile called the provider with no
reuseStored, so it started a fresh handshake (needs a new connect URI/QR) instead of
reconnecting. Now handleProfileSwitchRequest sends { providerId: "nip46",
reuseStored: true } for NIP-46 targets; authService.switchProfile forwards reuseStored;
the nip46 provider's useStoredRemoteSigner reconnects the stored session and activates
the account. On failure -> a clear "reconnect your remote signer" toast.
- Wrong-account guard (all methods): requestLogin now enforces expectPubkey (only set by
switchProfile). A single-slot stored nsec/NIP-46 session resolving to a DIFFERENT
account than requested is rejected with `pubkey-mismatch` instead of silently switching
to the wrong profile. Also protects NIP-07 (switching to an account the extension isn't
currently on fails clearly instead of grabbing the extension's active account).
Tests: tests/authService.test.mjs (+3 — unlockStored/passphrase + reuseStored forwarding,
expectPubkey rejection). Build + lint clean; auth suites green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
244 lines
9.2 KiB
JavaScript
244 lines
9.2 KiB
JavaScript
import "./test-helpers/setup-localstorage.mjs";
|
|
import { test, describe, beforeEach, afterEach, mock } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import AuthService from "../js/services/authService.js";
|
|
import {
|
|
getPubkey,
|
|
setPubkey,
|
|
getCurrentUserNpub,
|
|
setCurrentUserNpub,
|
|
} from "../js/state/appState.js";
|
|
import {
|
|
getSavedProfiles,
|
|
mutateSavedProfiles,
|
|
getActiveProfilePubkey,
|
|
setActiveProfilePubkey,
|
|
} from "../js/state/cache.js";
|
|
|
|
// Mock dependencies
|
|
const mockNostrClient = {
|
|
pubkey: null,
|
|
logout: mock.fn(),
|
|
pool: { list: mock.fn(async () => []) },
|
|
relays: [],
|
|
handleEvent: mock.fn(),
|
|
};
|
|
|
|
const mockUserBlocks = {
|
|
loadBlocks: mock.fn(async () => true),
|
|
reset: mock.fn(),
|
|
};
|
|
|
|
const mockRelayManager = {
|
|
loadRelayList: mock.fn(async () => true),
|
|
reset: mock.fn(),
|
|
};
|
|
|
|
const mockAccessControl = {
|
|
isLockdownActive: mock.fn(() => false),
|
|
isAdminEditor: mock.fn(() => false),
|
|
canAccess: mock.fn(() => true),
|
|
isBlacklisted: mock.fn(() => false),
|
|
};
|
|
|
|
const mockLogger = mock.fn();
|
|
|
|
const mockAuthProvider = {
|
|
login: mock.fn(async () => ({ pubkey: "0000000000000000000000000000000000000000000000000000000000000001", authType: "nip07" })),
|
|
};
|
|
|
|
const mockGetAuthProvider = mock.fn((id) => mockAuthProvider);
|
|
|
|
describe("AuthService Coverage", () => {
|
|
let authService;
|
|
|
|
beforeEach(() => {
|
|
// Reset global state
|
|
setPubkey(null);
|
|
setCurrentUserNpub(null);
|
|
setActiveProfilePubkey(null, { persist: false });
|
|
mutateSavedProfiles(() => [], { persist: false, persistActive: false });
|
|
|
|
// Reset mocks
|
|
mockNostrClient.logout.mock.resetCalls();
|
|
mockUserBlocks.loadBlocks.mock.resetCalls();
|
|
mockUserBlocks.reset.mock.resetCalls();
|
|
mockRelayManager.loadRelayList.mock.resetCalls();
|
|
mockRelayManager.reset.mock.resetCalls();
|
|
mockAccessControl.isLockdownActive.mock.resetCalls();
|
|
mockAuthProvider.login.mock.resetCalls();
|
|
|
|
authService = new AuthService({
|
|
nostrClient: mockNostrClient,
|
|
userBlocks: mockUserBlocks,
|
|
relayManager: mockRelayManager,
|
|
logger: mockLogger,
|
|
accessControl: mockAccessControl,
|
|
getAuthProvider: mockGetAuthProvider,
|
|
});
|
|
});
|
|
|
|
describe("login", () => {
|
|
test("successfully logs in with valid pubkey", async () => {
|
|
const pubkey = "0000000000000000000000000000000000000000000000000000000000000001";
|
|
const result = await authService.login(pubkey);
|
|
|
|
assert.equal(getPubkey(), pubkey);
|
|
assert.equal(getActiveProfilePubkey(), pubkey);
|
|
assert.equal(result.pubkey, pubkey);
|
|
|
|
const saved = getSavedProfiles();
|
|
assert.equal(saved.length, 1);
|
|
assert.equal(saved[0].pubkey, pubkey);
|
|
});
|
|
|
|
test("updates global state and calls post-login hooks", async () => {
|
|
const pubkey = "0000000000000000000000000000000000000000000000000000000000000002";
|
|
await authService.login(pubkey);
|
|
|
|
assert.strictEqual(mockRelayManager.loadRelayList.mock.callCount(), 1);
|
|
// Blocks are deferred by default in login flow, so loadBlocks should not be called immediately
|
|
assert.strictEqual(mockUserBlocks.loadBlocks.mock.callCount(), 0);
|
|
});
|
|
|
|
test("throws if lockdown is active and user is not admin", async () => {
|
|
mockAccessControl.isLockdownActive.mock.mockImplementation(() => true);
|
|
mockAccessControl.isAdminEditor.mock.mockImplementation(() => false);
|
|
|
|
await assert.rejects(
|
|
async () => await authService.login("0000000000000000000000000000000000000000000000000000000000000003"),
|
|
{ code: "site-lockdown" }
|
|
);
|
|
});
|
|
|
|
test("allows login if lockdown is active but user is admin", async () => {
|
|
mockAccessControl.isLockdownActive.mock.mockImplementation(() => true);
|
|
mockAccessControl.isAdminEditor.mock.mockImplementation(() => true);
|
|
|
|
const pubkey = "0000000000000000000000000000000000000000000000000000000000000004";
|
|
await authService.login(pubkey);
|
|
assert.equal(getPubkey(), pubkey);
|
|
});
|
|
});
|
|
|
|
describe("logout", () => {
|
|
test("clears state and calls reset on dependencies", async () => {
|
|
// Setup initial state
|
|
const pubkey = "0000000000000000000000000000000000000000000000000000000000000005";
|
|
await authService.login(pubkey);
|
|
|
|
await authService.logout();
|
|
|
|
assert.equal(getPubkey(), null);
|
|
assert.equal(getActiveProfilePubkey(), null);
|
|
|
|
assert.strictEqual(mockNostrClient.logout.mock.callCount(), 1);
|
|
assert.strictEqual(mockUserBlocks.reset.mock.callCount(), 1);
|
|
assert.strictEqual(mockRelayManager.reset.mock.callCount(), 1);
|
|
});
|
|
});
|
|
|
|
describe("requestLogin", () => {
|
|
test("uses provider to login", async () => {
|
|
const pubkey = "0000000000000000000000000000000000000000000000000000000000000001";
|
|
mockAuthProvider.login.mock.mockImplementation(async () => ({ pubkey, authType: "nip07" }));
|
|
|
|
const result = await authService.requestLogin({ providerId: "nip07" });
|
|
|
|
assert.equal(result.pubkey, pubkey);
|
|
assert.strictEqual(mockAuthProvider.login.mock.callCount(), 1);
|
|
// requestLogin calls login() internally if autoApply is true (default)
|
|
assert.equal(getPubkey(), pubkey);
|
|
});
|
|
|
|
test("does not call login if autoApply is false", async () => {
|
|
const pubkey = "0000000000000000000000000000000000000000000000000000000000000001";
|
|
mockAuthProvider.login.mock.mockImplementation(async () => ({ pubkey, authType: "nip07" }));
|
|
|
|
await authService.requestLogin({ providerId: "nip07", autoApply: false });
|
|
|
|
assert.equal(getPubkey(), null);
|
|
});
|
|
});
|
|
|
|
describe("switchProfile", () => {
|
|
test("switches to an existing profile", async () => {
|
|
const pubkey1 = "0000000000000000000000000000000000000000000000000000000000000001";
|
|
const pubkey2 = "0000000000000000000000000000000000000000000000000000000000000002";
|
|
|
|
await authService.login(pubkey1);
|
|
// Reset mocks to clear login calls
|
|
mockRelayManager.loadRelayList.mock.resetCalls();
|
|
|
|
// Mock login for switch
|
|
mockAuthProvider.login.mock.mockImplementation(async () => ({ pubkey: pubkey2, authType: "nip07" }));
|
|
|
|
await authService.switchProfile(pubkey2, { providerId: "nip07" });
|
|
|
|
assert.equal(getPubkey(), pubkey2);
|
|
assert.equal(getActiveProfilePubkey(), pubkey2);
|
|
});
|
|
|
|
test("reorders saved profiles on switch", async () => {
|
|
const pubkey1 = "0000000000000000000000000000000000000000000000000000000000000001";
|
|
const pubkey2 = "0000000000000000000000000000000000000000000000000000000000000002";
|
|
|
|
await authService.login(pubkey1);
|
|
await authService.login(pubkey2);
|
|
|
|
// At this point pubkey2 is active and likely last or first in savedProfiles depending on implementation.
|
|
// mutateSavedProfiles pushes to end if new.
|
|
// So [pubkey1, pubkey2].
|
|
|
|
let saved = getSavedProfiles();
|
|
assert.equal(saved.length, 2);
|
|
assert.equal(saved[1].pubkey, pubkey2);
|
|
|
|
// Switch back to pubkey1
|
|
mockAuthProvider.login.mock.mockImplementation(async () => ({ pubkey: pubkey1, authType: "nip07" }));
|
|
await authService.switchProfile(pubkey1);
|
|
|
|
saved = getSavedProfiles();
|
|
// Should be moved to front?
|
|
// Code says: if (index > 0) { const [moved] = draft.splice(index, 1); draft.unshift(moved); }
|
|
// So yes, moved to front.
|
|
assert.equal(saved[0].pubkey, pubkey1);
|
|
});
|
|
|
|
// #48: fluent switching across signing methods — switchProfile must forward the
|
|
// per-method restore options to the provider.
|
|
test("forwards unlockStored + passphrase to the provider (nsec switch)", async () => {
|
|
const target = "00000000000000000000000000000000000000000000000000000000000000a1";
|
|
mockAuthProvider.login.mock.mockImplementation(async () => ({ pubkey: target, authType: "nsec" }));
|
|
await authService.switchProfile(target, {
|
|
providerId: "nsec",
|
|
unlockStored: true,
|
|
passphrase: "pin1234",
|
|
});
|
|
const opts = mockAuthProvider.login.mock.calls.at(-1).arguments[0].options;
|
|
assert.equal(opts.unlockStored, true);
|
|
assert.equal(opts.passphrase, "pin1234");
|
|
});
|
|
|
|
test("forwards reuseStored to the provider (NIP-46 switch)", async () => {
|
|
const target = "00000000000000000000000000000000000000000000000000000000000000b2";
|
|
mockAuthProvider.login.mock.mockImplementation(async () => ({ pubkey: target, authType: "nip46" }));
|
|
await authService.switchProfile(target, { providerId: "nip46", reuseStored: true });
|
|
const opts = mockAuthProvider.login.mock.calls.at(-1).arguments[0].options;
|
|
assert.equal(opts.reuseStored, true);
|
|
});
|
|
|
|
test("rejects and does NOT activate a DIFFERENT account than requested (expectPubkey)", async () => {
|
|
const target = "00000000000000000000000000000000000000000000000000000000000000c3";
|
|
const other = "00000000000000000000000000000000000000000000000000000000000000d4";
|
|
// A single-slot stored session that resolves to another account:
|
|
mockAuthProvider.login.mock.mockImplementation(async () => ({ pubkey: other, authType: "nip46" }));
|
|
await assert.rejects(
|
|
() => authService.switchProfile(target, { providerId: "nip46", reuseStored: true }),
|
|
{ code: "pubkey-mismatch" },
|
|
);
|
|
assert.notEqual(getPubkey(), other, "must not silently switch to the wrong account");
|
|
});
|
|
});
|
|
});
|