e115934061
Publish `@librechat/data-schemas` to NPM / pack (push) Failing after 8s
Publish `@librechat/client` to NPM / pack (push) Failing after 0s
GitNexus Index / index (push) Failing after 1s
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been skipped
Sync Helm Chart Tags / Sync chart tags (push) Failing after 2s
Publish `librechat-data-provider` to NPM / pack (push) Failing after 1s
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Failing after 1s
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Failing after 0s
Sync Helm Chart Tags / Ignore non-main push (push) Has been skipped
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Failing after 1s
Publish `@librechat/client` to NPM / publish-npm (push) Has been cancelled
Publish `librechat-data-provider` to NPM / publish-npm (push) Has been cancelled
GitNexus Index / post-index (push) Has been cancelled
Publish `@librechat/data-schemas` to NPM / publish-npm (push) Has been cancelled
192 lines
6.0 KiB
JavaScript
192 lines
6.0 KiB
JavaScript
const express = require('express');
|
|
const request = require('supertest');
|
|
|
|
const originalDomainClient = process.env.DOMAIN_CLIENT;
|
|
process.env.DOMAIN_CLIENT = 'http://client.test';
|
|
|
|
const mockLogger = {
|
|
warn: jest.fn(),
|
|
error: jest.fn(),
|
|
info: jest.fn(),
|
|
debug: jest.fn(),
|
|
};
|
|
|
|
const mockOAuthHandler = jest.fn((_req, res) => res.status(204).end());
|
|
const mockOpenIDCallbackMiddleware = jest.fn((_req, _res, next) => next());
|
|
let mockOpenIDCallbackAuthenticatorOptions;
|
|
const mockCreateOpenIDCallbackAuthenticator = jest.fn((options) => {
|
|
mockOpenIDCallbackAuthenticatorOptions = options;
|
|
return mockOpenIDCallbackMiddleware;
|
|
});
|
|
const mockBuildOAuthFailureLog = jest.fn(({ provider, req, err, info, defaultMessage }) => ({
|
|
provider,
|
|
code: err?.code ?? info?.code ?? info?.error ?? req.query?.error,
|
|
name: err?.name ?? info?.name,
|
|
message:
|
|
err?.message ??
|
|
info?.message ??
|
|
info?.error_description ??
|
|
req.query?.error_description ??
|
|
defaultMessage,
|
|
cause_code: err?.cause?.code ?? info?.cause?.code,
|
|
cause_name: err?.cause?.name ?? info?.cause?.name,
|
|
has_code: req.query?.code != null,
|
|
has_state: req.query?.state != null,
|
|
query_error: req.query?.error,
|
|
query_error_description: req.query?.error_description,
|
|
path: req.path,
|
|
forwarded_for: req.headers?.['x-forwarded-for'],
|
|
user_agent: req.headers?.['user-agent'],
|
|
}));
|
|
const mockGetOAuthFailureMessage = jest.fn(
|
|
(req) =>
|
|
req.session?.messages?.pop() ??
|
|
req.query?.error_description ??
|
|
req.query?.error ??
|
|
'OAuth authentication failed',
|
|
);
|
|
const mockRedirectToAuthFailure = jest.fn((res, { clientDomain, authFailedError }) =>
|
|
res.redirect(`${clientDomain}/login?redirect=false&error=${authFailedError}`),
|
|
);
|
|
const mockPassportAuthenticate = jest.fn(() => (_req, _res, next) => next());
|
|
|
|
jest.mock('passport', () => ({
|
|
authenticate: (...args) => mockPassportAuthenticate(...args),
|
|
}));
|
|
|
|
jest.mock('openid-client', () => ({
|
|
randomState: jest.fn(() => 'random-state'),
|
|
}));
|
|
|
|
jest.mock('@librechat/data-schemas', () => ({
|
|
logger: mockLogger,
|
|
}));
|
|
|
|
jest.mock('librechat-data-provider', () => ({
|
|
...jest.requireActual('librechat-data-provider'),
|
|
ErrorTypes: {
|
|
AUTH_FAILED: 'auth_failed',
|
|
},
|
|
}));
|
|
|
|
jest.mock('@librechat/api', () => ({
|
|
buildOAuthFailureLog: (...args) => mockBuildOAuthFailureLog(...args),
|
|
createOpenIDCallbackAuthenticator: (...args) => mockCreateOpenIDCallbackAuthenticator(...args),
|
|
createSetBalanceConfig: jest.fn(() => (_req, _res, next) => next()),
|
|
getOAuthFailureMessage: (...args) => mockGetOAuthFailureMessage(...args),
|
|
redirectToAuthFailure: (...args) => mockRedirectToAuthFailure(...args),
|
|
}));
|
|
|
|
jest.mock('~/server/middleware', () => ({
|
|
checkDomainAllowed: jest.fn((_req, _res, next) => next()),
|
|
loginLimiter: jest.fn((_req, _res, next) => next()),
|
|
logHeaders: jest.fn((_req, _res, next) => next()),
|
|
}));
|
|
|
|
jest.mock('~/server/controllers/auth/oauth', () => ({
|
|
createOAuthHandler: jest.fn(() => mockOAuthHandler),
|
|
}));
|
|
|
|
jest.mock('~/models', () => ({
|
|
findBalanceByUser: jest.fn(),
|
|
upsertBalanceFields: jest.fn(),
|
|
}));
|
|
|
|
jest.mock('~/server/services/Config', () => ({
|
|
getAppConfig: jest.fn(),
|
|
}));
|
|
|
|
afterAll(() => {
|
|
if (originalDomainClient === undefined) {
|
|
delete process.env.DOMAIN_CLIENT;
|
|
return;
|
|
}
|
|
process.env.DOMAIN_CLIENT = originalDomainClient;
|
|
});
|
|
|
|
function getOAuthRouter() {
|
|
jest.resetModules();
|
|
return require('./oauth');
|
|
}
|
|
|
|
function createApp(sessionMessages) {
|
|
const app = express();
|
|
app.use((req, _res, next) => {
|
|
if (sessionMessages) {
|
|
req.session = { messages: [...sessionMessages] };
|
|
}
|
|
next();
|
|
});
|
|
app.use('/oauth', getOAuthRouter());
|
|
app.use((err, _req, res, _next) => {
|
|
res.status(500).json({ message: err.message });
|
|
});
|
|
return app;
|
|
}
|
|
|
|
describe('OAuth route failure logging', () => {
|
|
beforeEach(() => {
|
|
mockLogger.warn.mockClear();
|
|
mockLogger.error.mockClear();
|
|
mockLogger.info.mockClear();
|
|
mockLogger.debug.mockClear();
|
|
mockOAuthHandler.mockClear();
|
|
mockOpenIDCallbackMiddleware.mockClear();
|
|
mockBuildOAuthFailureLog.mockClear();
|
|
mockGetOAuthFailureMessage.mockClear();
|
|
mockRedirectToAuthFailure.mockClear();
|
|
mockPassportAuthenticate.mockClear();
|
|
mockOpenIDCallbackAuthenticatorOptions = undefined;
|
|
mockPassportAuthenticate.mockImplementation(() => (_req, _res, next) => next());
|
|
mockOpenIDCallbackMiddleware.mockImplementation((_req, _res, next) => next());
|
|
});
|
|
|
|
it('wires the package OpenID callback middleware into the route', async () => {
|
|
const app = createApp();
|
|
|
|
await request(app)
|
|
.get('/oauth/openid/callback?code=secret-code&state=secret-state')
|
|
.expect(204);
|
|
|
|
expect(mockOpenIDCallbackAuthenticatorOptions).toEqual({
|
|
passport: expect.objectContaining({ authenticate: expect.any(Function) }),
|
|
logger: mockLogger,
|
|
clientDomain: 'http://client.test',
|
|
authFailedError: 'auth_failed',
|
|
});
|
|
expect(mockOpenIDCallbackMiddleware).toHaveBeenCalledWith(
|
|
expect.any(Object),
|
|
expect.any(Object),
|
|
expect.any(Function),
|
|
);
|
|
expect(mockOAuthHandler).toHaveBeenCalled();
|
|
});
|
|
|
|
it('logs structured fallback errors without using Unknown OAuth error', async () => {
|
|
const app = createApp();
|
|
|
|
const response = await request(app)
|
|
.get('/oauth/error?error=access_denied&error_description=Denied%20by%20provider')
|
|
.set('x-forwarded-for', '203.0.113.10')
|
|
.expect(302);
|
|
|
|
expect(response.headers.location).toBe(
|
|
'http://client.test/login?redirect=false&error=auth_failed',
|
|
);
|
|
expect(mockLogger.warn).toHaveBeenCalledWith(
|
|
'[OAuth] Authentication failed',
|
|
expect.objectContaining({
|
|
provider: 'unknown',
|
|
code: 'access_denied',
|
|
message: 'Denied by provider',
|
|
query_error: 'access_denied',
|
|
query_error_description: 'Denied by provider',
|
|
has_code: false,
|
|
has_state: false,
|
|
forwarded_for: '203.0.113.10',
|
|
}),
|
|
);
|
|
expect(JSON.stringify(mockLogger.warn.mock.calls[0])).not.toContain('Unknown OAuth error');
|
|
});
|
|
});
|