Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion src/components/security/__tests__/methods.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
AUTH_ERROR_REFRESH_TOKEN_NETWORK_ERROR,
} from '../constants';

import { refreshAccessToken, retryWithBackoff } from '../methods';
import { refreshAccessToken, retryWithBackoff, getAccessToken, setAccessTokenResolver } from '../methods';

// Mock utils/methods imports used by security/methods
jest.mock('../../../utils/methods', () => ({
Expand Down Expand Up @@ -370,3 +370,30 @@ describe('retryWithBackoff', () => {
setTimeoutSpy.mockRestore();
});
});

describe('setAccessTokenResolver / getAccessToken', () => {
afterEach(() => setAccessTokenResolver(null));

it('delegates to a registered resolver', async () => {
const resolver = jest.fn().mockResolvedValue('tok-A');
setAccessTokenResolver(resolver);
await expect(getAccessToken()).resolves.toBe('tok-A');
expect(resolver).toHaveBeenCalledTimes(1);
});

it('a later resolver replaces the previous one', async () => {
setAccessTokenResolver(jest.fn().mockResolvedValue('tok-A'));
const next = jest.fn().mockResolvedValue('tok-B');
setAccessTokenResolver(next);
await expect(getAccessToken()).resolves.toBe('tok-B');
expect(next).toHaveBeenCalledTimes(1);
});

it('a non-function argument clears the resolver (built-in flow runs)', async () => {
const resolver = jest.fn().mockResolvedValue('tok-A');
setAccessTokenResolver(resolver);
setAccessTokenResolver(undefined);
await getAccessToken().catch(() => {});
expect(resolver).not.toHaveBeenCalled();
});
});
13 changes: 13 additions & 0 deletions src/components/security/methods.js
Original file line number Diff line number Diff line change
Expand Up @@ -342,10 +342,23 @@ const _getAccessToken = async () => {
return accessToken;
}

/**
* Optional resolver for getAccessToken, set via setAccessTokenResolver. When
* present, getAccessToken delegates to it; otherwise the built-in flow runs.
* Pass a non-function (or nothing) to reset to the built-in.
*/
let _resolveAccessToken = null;

export const setAccessTokenResolver = (resolver) => {
_resolveAccessToken = typeof resolver === 'function' ? resolver : null;
};

/**
* @returns {Promise<*|undefined>}
*/
export const getAccessToken = async () => {
if (_resolveAccessToken) return _resolveAccessToken();

if (typeof navigator !== 'undefined' && navigator.locks) {
return await navigator.locks.request(GET_TOKEN_SILENTLY_LOCK_KEY, async lock => {
console.log(`openstack-uicore-foundation::Security::methods::getAccessToken web lock api`, lock);
Expand Down
25 changes: 23 additions & 2 deletions webpack.common.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,27 @@ const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const path = require('path');
const nodeExternals = require('webpack-node-externals');

const { name: PKG_NAME } = require('./package.json');

// Externalize uicore's OWN token module so every UMD entry shares one instance:
// methods.js is stateful and must be a singleton. Without this, each importer
// (e.g. query-actions) inlines its own copy with separate state. Other internal
// modules are stateless, so they stay inlined.
const METHODS_SRC = path.resolve(__dirname, 'src/components/security/methods.js');
const METHODS_MODULE = `${PKG_NAME}/lib/security/methods`;

// Redirect imports of methods.js to the shared lib file. The issuer guard skips
// the methods entry itself (an entry has no issuer), so it still builds its real
// implementation instead of requiring itself.
const shareSecurityMethods = ({ request, context, contextInfo }, cb) => {
if (!contextInfo || !contextInfo.issuer || !request.startsWith('.')) return cb();
const resolved = path.resolve(context, request);
if (resolved === METHODS_SRC || `${resolved}.js` === METHODS_SRC) {
return cb(null, METHODS_MODULE);
}
cb();
};

module.exports = {
entry: {
// security
Expand Down Expand Up @@ -98,7 +119,7 @@ module.exports = {
output: {
path: path.resolve(__dirname, 'lib'),
filename: '[name].js',
library: 'openstack-uicore-foundation',
library: PKG_NAME,
libraryTarget: 'umd',
umdNamedDefine: true,
globalObject: 'this',
Expand Down Expand Up @@ -191,5 +212,5 @@ module.exports = {
}
]
},
externals: [nodeExternals()]
externals: [nodeExternals(), shareSecurityMethods]
};
Loading