mirror of
https://github.com/HeyPuter/puter.git
synced 2026-11-05 21:46:51 +00:00
fix(puter-js): coordinate sign-in callbacks and filesystem sockets (#4375)
This commit is contained in:
1 parent
c8f7b1ceda
commit
f1964778b4
12 files changed
+458
-11
No files matched your search
@@ -191,8 +191,11 @@ async function UIWindowLogin (options) {
|
||||
// returns it as a plain sign-in popup, losing whatever the popup
|
||||
// was opened to do — and, for a permission prompt, handing the
|
||||
// opener a token instead of a decision. Don't offer it there.
|
||||
// A cross-origin-isolated opener can only get the token through
|
||||
// the session relay, which the provider's return doesn't keep.
|
||||
if ( window.embedded_in_popup
|
||||
&& ! offersFederatedSignInInPopup(window.gui_action) ) {
|
||||
&& ( ! offersFederatedSignInInPopup(window.gui_action)
|
||||
|| window.url_query_params?.get('cross_origin_isolated') === 'true' ) ) {
|
||||
return;
|
||||
}
|
||||
const res = await fetch(`${window.api_origin}/auth/oidc/providers`);
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import jQuery from '../lib/jquery-3.6.1/jquery-3.6.1.min.js';
|
||||
|
||||
const state = vi.hoisted(() => ({ el: null }));
|
||||
vi.mock('./UIWindowRecoverPassword.js', () => ({ default: vi.fn() }));
|
||||
vi.mock('./UIWindowEmailConfirmationRequired.js', () => ({ default: vi.fn() }));
|
||||
vi.mock('./UIWindowPhoneVerificationRequired.js', () => ({ default: vi.fn() }));
|
||||
vi.mock('./UIWindowCardVerificationRequired.js', () => ({ default: vi.fn() }));
|
||||
vi.mock('../helpers/authLogoHeader.js', () => ({
|
||||
authLogoHeader: () => '',
|
||||
wireAuthLogoHeader: vi.fn(),
|
||||
}));
|
||||
vi.mock('./UIWindow.js', () => ({
|
||||
default: async (options) => {
|
||||
state.el = document.createElement('div');
|
||||
state.el.innerHTML = options.body_content;
|
||||
document.body.append(state.el);
|
||||
options.onAppend?.(state.el);
|
||||
return state.el;
|
||||
},
|
||||
}));
|
||||
globalThis.def = globalThis.def ?? ((value) => value);
|
||||
const { default: UIWindowLogin } = await import('./UIWindowLogin.js');
|
||||
const { default: UIWindowSignup } = await import('./UIWindowSignup.js');
|
||||
|
||||
const PROVIDERS_URL = 'https://api.puter.test/auth/oidc/providers';
|
||||
const RELAY_POPUP =
|
||||
'embedded_in_popup=true&msg_id=1&cross_origin_isolated=true&signin_session=s';
|
||||
const PLAIN_POPUP = 'embedded_in_popup=true&msg_id=1';
|
||||
|
||||
const asPopup = (query) => {
|
||||
window.embedded_in_popup = true;
|
||||
window.gui_action = 'sign-in';
|
||||
window.url_query_params = new URLSearchParams(query);
|
||||
};
|
||||
|
||||
const askedForProviders = () =>
|
||||
fetch.mock.calls.some(([url]) => url === PROVIDERS_URL);
|
||||
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
globalThis.$ = jQuery;
|
||||
jQuery.fn.close = vi.fn();
|
||||
globalThis.i18n = (key) => key;
|
||||
globalThis.html_encode = (value) => jQuery('<div>').text(value).html();
|
||||
globalThis.fetch = vi.fn(
|
||||
async () => new Response(JSON.stringify({ providers: ['google'] })),
|
||||
);
|
||||
window.gui_origin = 'https://puter.test';
|
||||
window.api_origin = 'https://api.puter.test';
|
||||
window.gui_params = { turnstileSiteKey: 'site-key' };
|
||||
window.turnstile = { render: vi.fn() };
|
||||
window.icons = {};
|
||||
window.uuidv4 = () => 'oidc-popup-test';
|
||||
window.embedded_in_popup = false;
|
||||
window.gui_action = undefined;
|
||||
window.url_query_params = new URLSearchParams('');
|
||||
state.el = null;
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe.each([
|
||||
[
|
||||
'login',
|
||||
() => UIWindowLogin({ reload_on_success: false, redirect_url: '/' }),
|
||||
'.login-btn',
|
||||
],
|
||||
[
|
||||
'signup',
|
||||
() => UIWindowSignup({ reload_on_success: false, redirect_url: '/' }),
|
||||
'.signup-btn',
|
||||
],
|
||||
])('OIDC buttons in the %s window', (_name, open, formButton) => {
|
||||
const rendered = async () => {
|
||||
void open();
|
||||
await vi.waitFor(() =>
|
||||
expect(jQuery(state.el).find(formButton)).toHaveLength(1),
|
||||
);
|
||||
};
|
||||
|
||||
// The provider's return drops the relay that hands the token to an
|
||||
// isolated opener, so only password sign-in can finish there.
|
||||
it('hides them in a cross-origin-isolated relay popup', async () => {
|
||||
asPopup(RELAY_POPUP);
|
||||
await rendered();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
expect(askedForProviders()).toBe(false);
|
||||
expect(jQuery(state.el).find('.oidc-google-btn').css('display')).toBe(
|
||||
'none',
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps them in a regular sign-in popup', async () => {
|
||||
asPopup(PLAIN_POPUP);
|
||||
await rendered();
|
||||
|
||||
await vi.waitFor(() =>
|
||||
expect(
|
||||
jQuery(state.el).find('.oidc-google-btn').css('display'),
|
||||
).toBe('flex'),
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps them outside a popup', async () => {
|
||||
await rendered();
|
||||
|
||||
await vi.waitFor(() =>
|
||||
expect(
|
||||
jQuery(state.el).find('.oidc-google-btn').css('display'),
|
||||
).toBe('flex'),
|
||||
);
|
||||
expect(askedForProviders()).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -285,10 +285,16 @@ function UIWindowSignup(options) {
|
||||
// provider returns it as a plain sign-in popup, losing
|
||||
// whatever the popup was opened to do — and, for a
|
||||
// permission prompt, handing the opener a token instead
|
||||
// of a decision. Don't offer it there.
|
||||
// of a decision. Don't offer it there. A
|
||||
// cross-origin-isolated opener can only get the token
|
||||
// through the session relay, which the provider's
|
||||
// return doesn't keep.
|
||||
if (
|
||||
window.embedded_in_popup &&
|
||||
!offersFederatedSignInInPopup(window.gui_action)
|
||||
(!offersFederatedSignInInPopup(window.gui_action) ||
|
||||
window.url_query_params?.get(
|
||||
'cross_origin_isolated',
|
||||
) === 'true')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
vi.mock('socket.io-client', () => ({
|
||||
io: () => ({
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
emit: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
vi.mock('./lib/polyfills/localStorage.js', () => ({ default: {} }));
|
||||
vi.mock('./lib/polyfills/xhrshim.js', () => ({ default: class {} }));
|
||||
|
||||
const origDefine = customElements.define.bind(customElements);
|
||||
customElements.define = (name, cls) =>
|
||||
customElements.get(name) || origDefine(name, cls);
|
||||
|
||||
const origParent = Object.getOwnPropertyDescriptor(globalThis, 'parent');
|
||||
const origXHR = globalThis.XMLHttpRequest;
|
||||
const urls = [];
|
||||
class CountingXHR {
|
||||
_listeners = {};
|
||||
status = 200;
|
||||
readyState = 0;
|
||||
responseType = '';
|
||||
upload = { addEventListener() {} };
|
||||
open(method, url) {
|
||||
this.url = url;
|
||||
urls.push(`${method} ${url}`);
|
||||
}
|
||||
setRequestHeader() {}
|
||||
addEventListener(type, fn) {
|
||||
(this._listeners[type] ??= []).push(fn);
|
||||
}
|
||||
getResponseHeader(n) {
|
||||
return n.toLowerCase() === 'content-type' ? 'application/json' : null;
|
||||
}
|
||||
abort() {}
|
||||
send() {
|
||||
queueMicrotask(() => {
|
||||
this.responseText = JSON.stringify({ username: 'u' });
|
||||
this.response = this.responseText;
|
||||
this.readyState = 4;
|
||||
for (const fn of this._listeners.readystatechange ?? [])
|
||||
fn.call(this);
|
||||
for (const fn of this._listeners.load ?? [])
|
||||
fn.call(this, { target: this });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
urls.length = 0;
|
||||
Object.defineProperty(globalThis, 'parent', {
|
||||
value: globalThis,
|
||||
configurable: true,
|
||||
});
|
||||
globalThis.XMLHttpRequest = CountingXHR;
|
||||
});
|
||||
afterEach(() => {
|
||||
if (origParent) Object.defineProperty(globalThis, 'parent', origParent);
|
||||
globalThis.XMLHttpRequest = origXHR;
|
||||
vi.unstubAllGlobals();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
const flush = async () => {
|
||||
for (let i = 0; i < 10; i++) await new Promise((r) => setTimeout(r, 0));
|
||||
};
|
||||
|
||||
describe('implicit sign-in on a web page', () => {
|
||||
it('runs onAuth twice for one sign-in', async () => {
|
||||
history.replaceState(null, '', '/site/index.html');
|
||||
vi.resetModules();
|
||||
// Hide `process` from the SDK's environment probe only, so the
|
||||
// module's own instance boots as a top-level web page.
|
||||
const realProcess = globalThis.process;
|
||||
Object.defineProperty(globalThis, 'process', {
|
||||
configurable: true,
|
||||
get() {
|
||||
const stack = new Error().stack ?? '';
|
||||
return /src\/index\.js:\d+/.test(stack.split('\n')[2] ?? '')
|
||||
? undefined
|
||||
: realProcess;
|
||||
},
|
||||
});
|
||||
let puter;
|
||||
try {
|
||||
({ puter } = await import('./index.js'));
|
||||
} finally {
|
||||
Object.defineProperty(globalThis, 'process', {
|
||||
value: realProcess,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
expect(puter.env).toBe('web');
|
||||
expect(puter.authToken).toBeFalsy();
|
||||
|
||||
const popup = { closed: false };
|
||||
vi.spyOn(window, 'open').mockReturnValue(popup);
|
||||
Object.defineProperty(navigator, 'userActivation', {
|
||||
value: { hasBeenActive: true, isActive: true },
|
||||
configurable: true,
|
||||
});
|
||||
vi.spyOn(crypto, 'randomUUID').mockReturnValue('sess-1');
|
||||
|
||||
const onAuth = vi.fn();
|
||||
puter.onAuth = onAuth;
|
||||
|
||||
const signedIn = puter.ui.authenticateWithPuter();
|
||||
window.dispatchEvent(
|
||||
new MessageEvent('message', {
|
||||
origin: puter.defaultGUIOrigin,
|
||||
source: popup,
|
||||
data: {
|
||||
msg: 'puter.token',
|
||||
msg_id: 'sess-1',
|
||||
success: true,
|
||||
token: 'tok',
|
||||
},
|
||||
}),
|
||||
);
|
||||
await signedIn;
|
||||
await flush();
|
||||
|
||||
expect(puter.authToken).toBe('tok');
|
||||
expect(onAuth).toHaveBeenCalledTimes(1);
|
||||
const whoami = urls.filter((u) => u.includes('/whoami'));
|
||||
expect(whoami.length).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -722,8 +722,6 @@ export class Puter {
|
||||
// When SDK is loaded in GUI the initiation process should start when the DOM is ready. This is because
|
||||
// the SDK needs to show a dialog to the user to ask for permission to access their Puter account.
|
||||
else if (this.env === 'web') {
|
||||
// initialize submodules
|
||||
this.initSubmodules();
|
||||
try {
|
||||
const storedToken = this.normalizeAuthTokenCandidate(
|
||||
localStorage.getItem(STORAGE_KEY_V2),
|
||||
@@ -754,6 +752,8 @@ export class Puter {
|
||||
console.error('Error accessing localStorage:', error);
|
||||
}
|
||||
|
||||
this.initSubmodules();
|
||||
|
||||
// Print a CTA for developers to publish their app on the Puter App Store
|
||||
this.printDevCTA();
|
||||
|
||||
@@ -2015,9 +2015,14 @@ globalThis.addEventListener &&
|
||||
'*',
|
||||
);
|
||||
} else if (event.data.msg === 'puter.token') {
|
||||
if (puter.auth?.pendingSignInMessageIds_?.has(event.data.msg_id))
|
||||
return;
|
||||
if (!puter.tokenSourceAllowed_(event.source)) {
|
||||
return;
|
||||
}
|
||||
// Could be a late reply from an old popup; the flow that owns the
|
||||
// prompt settles its own failures.
|
||||
if (event.data.success === false) return;
|
||||
// Set the authToken property
|
||||
puter.setAuthToken(event.data.token);
|
||||
// update appID only when token does not include app identity
|
||||
@@ -2038,7 +2043,8 @@ globalThis.addEventListener &&
|
||||
// Not awaited: the waiting sign-ins below settle first.
|
||||
(async () => {
|
||||
try {
|
||||
puter.onAuth(await puter.getUser());
|
||||
const user = await puter.whoamiCache_;
|
||||
puter.onAuth(user ?? (await puter.getUser()));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
@@ -328,6 +328,29 @@ describe('GUI message listener', () => {
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('ignores a late failure message without touching a pending sign-in', async () => {
|
||||
const { puter, handler } = await bootWithListener();
|
||||
const previous = puter.authToken;
|
||||
const waiter = { resolve: vi.fn(), reject: vi.fn() };
|
||||
puter.puterAuthState.isPromptOpen = true;
|
||||
puter.puterAuthState.resolvers.push(waiter);
|
||||
await handler({
|
||||
origin: puter.defaultGUIOrigin,
|
||||
source: globalThis.parent,
|
||||
data: {
|
||||
msg: 'puter.token',
|
||||
success: false,
|
||||
token: 'invalid',
|
||||
error: 'auth_canceled',
|
||||
},
|
||||
});
|
||||
expect(puter.authToken).toBe(previous);
|
||||
expect(waiter.reject).not.toHaveBeenCalled();
|
||||
expect(waiter.resolve).not.toHaveBeenCalled();
|
||||
expect(puter.puterAuthState.resolvers).toEqual([waiter]);
|
||||
expect(puter.puterAuthState.isPromptOpen).toBe(true);
|
||||
});
|
||||
|
||||
it('reports a failed user lookup after sign-in', async () => {
|
||||
const { puter, handler } = await bootWithListener();
|
||||
const error = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const ioCalls = [];
|
||||
vi.mock('socket.io-client', () => ({
|
||||
io: (origin, opts) => {
|
||||
ioCalls.push({ origin, token: opts?.auth?.auth_token ?? null });
|
||||
return {
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
emit: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
};
|
||||
},
|
||||
}));
|
||||
vi.mock('./lib/polyfills/localStorage.js', () => ({ default: {} }));
|
||||
vi.mock('./lib/polyfills/xhrshim.js', () => ({ default: class {} }));
|
||||
|
||||
const STORAGE_KEY = 'puter.auth.token.v2';
|
||||
const ORIGIN_KEY = 'puter.auth.token.origin.v2';
|
||||
const b64url = (value) => Buffer.from(value).toString('base64url');
|
||||
const APP_TOKEN = `${b64url('{"alg":"HS256"}')}.${b64url(JSON.stringify({ t: 'au', v: '2', au: Buffer.alloc(16, 1).toString('base64') }))}.sig`;
|
||||
|
||||
const origDefine = customElements.define.bind(customElements);
|
||||
customElements.define = (name, cls) =>
|
||||
customElements.get(name) || origDefine(name, cls);
|
||||
|
||||
const origParent = Object.getOwnPropertyDescriptor(globalThis, 'parent');
|
||||
const origXHR = globalThis.XMLHttpRequest;
|
||||
class StubXHR {
|
||||
open() {}
|
||||
setRequestHeader() {}
|
||||
addEventListener() {}
|
||||
send() {}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
ioCalls.length = 0;
|
||||
Object.defineProperty(globalThis, 'parent', {
|
||||
value: globalThis,
|
||||
configurable: true,
|
||||
});
|
||||
globalThis.XMLHttpRequest = StubXHR;
|
||||
});
|
||||
afterEach(() => {
|
||||
if (origParent) Object.defineProperty(globalThis, 'parent', origParent);
|
||||
globalThis.XMLHttpRequest = origXHR;
|
||||
});
|
||||
|
||||
describe('web-env boot with a stored token', () => {
|
||||
it('opens only one authenticated socket', async () => {
|
||||
localStorage.setItem(STORAGE_KEY, APP_TOKEN);
|
||||
localStorage.setItem(ORIGIN_KEY, 'https://api.puter.com');
|
||||
history.replaceState(null, '', '/site/index.html');
|
||||
vi.resetModules();
|
||||
const { Puter } = await import('./index.js');
|
||||
ioCalls.length = 0;
|
||||
|
||||
const realProcess = globalThis.process;
|
||||
Object.defineProperty(globalThis, 'process', {
|
||||
configurable: true,
|
||||
get() {
|
||||
const caller = (new Error().stack ?? '').split('\n')[2] ?? '';
|
||||
return /src\/index\.js:\d+/.test(caller)
|
||||
? undefined
|
||||
: realProcess;
|
||||
},
|
||||
});
|
||||
let puter;
|
||||
try {
|
||||
puter = new Puter();
|
||||
} finally {
|
||||
Object.defineProperty(globalThis, 'process', {
|
||||
value: realProcess,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
}
|
||||
|
||||
expect(puter.env).toBe('web');
|
||||
expect(puter.authToken).toBe(APP_TOKEN);
|
||||
expect(ioCalls).toEqual([
|
||||
{ origin: 'https://api.puter.com', token: APP_TOKEN },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -116,6 +116,9 @@ import {
|
||||
* are for apps that drive the sign-in flow themselves.
|
||||
*/
|
||||
export class AuthModule extends PuterModule {
|
||||
/** @internal */
|
||||
pendingSignInMessageIds_ = new Set();
|
||||
|
||||
/**
|
||||
* Signs the user in, opening a popup with the appropriate authentication
|
||||
* method. Must be called from a user gesture (such as a click) — without
|
||||
@@ -164,6 +167,7 @@ export class AuthModule extends PuterModule {
|
||||
return new Promise((resolve, reject) => {
|
||||
const signinsession = crypto.randomUUID();
|
||||
const msg_id = signinsession;
|
||||
this.pendingSignInMessageIds_.add(msg_id);
|
||||
const popupName = `Puter-sign-in-${signinsession}`;
|
||||
const url = `${puter.defaultGUIOrigin}/action/sign-in?embedded_in_popup=true&msg_id=${msg_id}${window.crossOriginIsolated ? `&cross_origin_isolated=true&signin_session=${signinsession}` : ''}${options.attempt_temp_user_creation ? '&attempt_temp_user_creation=true' : ''}${options.request_auth ? '&request_auth=true' : ''}`;
|
||||
|
||||
@@ -177,6 +181,7 @@ export class AuthModule extends PuterModule {
|
||||
let popupWindow = null;
|
||||
|
||||
const cleanup = () => {
|
||||
this.pendingSignInMessageIds_.delete(msg_id);
|
||||
if (checkClosed) {
|
||||
clearInterval(checkClosed);
|
||||
checkClosed = null;
|
||||
@@ -184,6 +189,18 @@ export class AuthModule extends PuterModule {
|
||||
window.removeEventListener('message', messageHandler);
|
||||
};
|
||||
|
||||
const notifyAuth = () => {
|
||||
if (typeof puter.onAuth !== 'function') return;
|
||||
void (async () => {
|
||||
try {
|
||||
const user = await puter.whoamiCache_;
|
||||
puter.onAuth(user ?? (await puter.getUser()));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
if (window.crossOriginIsolated) {
|
||||
(async () => {
|
||||
// Bounded too: isolated with a gesture, nothing watches.
|
||||
@@ -223,6 +240,7 @@ export class AuthModule extends PuterModule {
|
||||
settled = true;
|
||||
cleanup();
|
||||
puter.setAuthToken(auth_token);
|
||||
notifyAuth();
|
||||
resolve({ success: true, token: auth_token });
|
||||
return '';
|
||||
}
|
||||
@@ -262,6 +280,7 @@ export class AuthModule extends PuterModule {
|
||||
if (e.data.success) {
|
||||
// set the auth token
|
||||
puter.setAuthToken(e.data.token);
|
||||
notifyAuth();
|
||||
resolve(e.data);
|
||||
} else {
|
||||
reject(e.data);
|
||||
|
||||
@@ -131,6 +131,18 @@ describe('signIn popup binding', () => {
|
||||
},
|
||||
);
|
||||
|
||||
it('notifies explicit sign-in once using the cached user lookup', async () => {
|
||||
const user = { username: 'alice' };
|
||||
puter.whoamiCache_ = Promise.resolve(user);
|
||||
puter.onAuth = vi.fn();
|
||||
puter.getUser = vi.fn();
|
||||
const signedIn = auth.signIn();
|
||||
respond(popups[0], 'token');
|
||||
await signedIn;
|
||||
expect(puter.onAuth).toHaveBeenCalledExactlyOnceWith(user);
|
||||
expect(puter.getUser).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keeps the isolated-page relay independent of popup messages', async () => {
|
||||
vi.stubGlobal('crossOriginIsolated', true);
|
||||
let answer;
|
||||
|
||||
@@ -103,6 +103,10 @@ export class PuterJSFileSystemModule extends PuterModule {
|
||||
this.socket.disconnect();
|
||||
}
|
||||
|
||||
this.socket = null;
|
||||
if (!this.authToken) return;
|
||||
this.socketToken_ = this.authToken;
|
||||
this.socketOrigin_ = this.APIOrigin;
|
||||
this.socket = io(this.APIOrigin, {
|
||||
auth: {
|
||||
auth_token: this.authToken,
|
||||
@@ -212,6 +216,9 @@ export class PuterJSFileSystemModule extends PuterModule {
|
||||
}
|
||||
|
||||
if ( this.puter.socketEnabled ) {
|
||||
if (this.socket?.active &&
|
||||
this.socketToken_ === this.authToken &&
|
||||
this.socketOrigin_ === this.APIOrigin) return;
|
||||
this.initializeSocket();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,6 +137,43 @@ describe('auth state', () => {
|
||||
expect(fs.socket).not.toBe(previousSocket);
|
||||
});
|
||||
|
||||
it('keeps an active socket when credentials and origin have not changed', () => {
|
||||
const fs = makeModule('web');
|
||||
const previous = fs.socket;
|
||||
previous.active = true;
|
||||
fs.onAuthStateChanged();
|
||||
expect(fs.socket).toBe(previous);
|
||||
expect(previous.disconnect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('replaces an inactive socket even when credentials are unchanged', () => {
|
||||
const fs = makeModule('web');
|
||||
const previous = fs.socket;
|
||||
previous.active = false;
|
||||
fs.onAuthStateChanged();
|
||||
expect(fs.socket).not.toBe(previous);
|
||||
expect(previous.disconnect).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('closes the socket on sign-out without opening an anonymous replacement', () => {
|
||||
const fs = makeModule('web');
|
||||
const previous = fs.socket;
|
||||
fs.puter.authToken = null;
|
||||
fs.onAuthStateChanged();
|
||||
expect(previous.disconnect).toHaveBeenCalledOnce();
|
||||
expect(fs.socket).toBeNull();
|
||||
});
|
||||
|
||||
it('replaces an active socket when the API origin changes', () => {
|
||||
const fs = makeModule('web');
|
||||
const previous = fs.socket;
|
||||
previous.active = true;
|
||||
fs.puter.APIOrigin = 'https://new-api.test';
|
||||
fs.onAuthStateChanged();
|
||||
expect(fs.socket).not.toBe(previous);
|
||||
expect(previous.disconnect).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('never opens a socket when the client opted out', () => {
|
||||
const fs = makeModule('web', false);
|
||||
|
||||
|
||||
@@ -2615,11 +2615,6 @@ export class UIModule extends EventListener {
|
||||
return puter.auth.signIn({ request_auth: true }).then(
|
||||
() => {
|
||||
settle(true);
|
||||
if ( puter.onAuth && typeof puter.onAuth === 'function' ) {
|
||||
puter.getUser().then((user) => {
|
||||
puter.onAuth(user);
|
||||
});
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
settle(false);
|
||||
|
||||
Reference in new issue
Block a user