Skip to content

Commit

Permalink
test(core): add unit test cases
Browse files Browse the repository at this point in the history
add unit test cases
  • Loading branch information
simeng-li committed Oct 14, 2024
1 parent 54c6f7a commit 4eb15aa
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 5 deletions.
14 changes: 14 additions & 0 deletions packages/core/src/__mocks__/sso.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,17 @@ export const wellConfiguredSsoConnector = {
syncProfile: true,
createdAt: Date.now(),
} satisfies SsoConnector;

export const mockSamlSsoConnector = {
id: 'mock-saml-sso-connector',
tenantId: 'mock-tenant',
providerName: SsoProviderName.SAML,
connectorName: 'mock-connector-name',
config: {
metadata: 'mock-metadata',
},
domains: ['foo.com'],
branding: {},
syncProfile: true,
createdAt: Date.now(),
} satisfies SsoConnector;
200 changes: 198 additions & 2 deletions packages/core/src/routes/interaction/utils/single-sign-on.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
/* eslint-disable max-lines */
import { createMockUtils } from '@logto/shared/esm';
import type Provider from 'oidc-provider';

import { mockSsoConnector, wellConfiguredSsoConnector } from '#src/__mocks__/sso.js';
import Sinon from 'sinon';

import {
mockSsoConnector,
wellConfiguredSsoConnector,
mockSamlSsoConnector,
} from '#src/__mocks__/sso.js';
import { EnvSet } from '#src/env-set/index.js';
import RequestError from '#src/errors/RequestError/index.js';
import { type WithLogContext } from '#src/middleware/koa-audit-log.js';
import { type WithInteractionDetailsContext } from '#src/middleware/koa-interaction-details.js';
Expand All @@ -13,6 +20,8 @@ import { createMockProvider } from '#src/test-utils/oidc-provider.js';
import { MockTenant } from '#src/test-utils/tenant.js';
import { createContextWithRouteParameters } from '#src/utils/test-utils.js';

import { idpInitiatedSamlSsoSessionCookieName } from '../../../constants/index.js';
import { SamlSsoConnector } from '../../../sso/SamlSsoConnector/index.js';
import { type WithInteractionHooksContext } from '../middleware/koa-interaction-hooks.js';

const { jest } = import.meta;
Expand All @@ -30,27 +39,43 @@ const insertUserMock = jest.fn().mockResolvedValue([{ id: 'foo' }, { organizatio
const generateUserIdMock = jest.fn().mockResolvedValue('foo');
const getAvailableSsoConnectorsMock = jest.fn();

const findIdpInitiatedSamlSsoSessionMock = jest.fn();
const deleteIdpInitiatedSamlSsoSessionMock = jest.fn();

class MockOidcSsoConnector extends OidcSsoConnector {
override getAuthorizationUrl = getAuthorizationUrlMock;
override getIssuer = getIssuerMock;
override getUserInfo = getUserInfoMock;
}

class MockSamlSsoConnector extends SamlSsoConnector {
override getAuthorizationUrl = getAuthorizationUrlMock;
override getIssuer = getIssuerMock;
override getUserInfo = getUserInfoMock;
}

mockEsm('./interaction.js', () => ({
storeInteractionResult: jest.fn(),
}));

const {
assignSingleSignOnSessionResult: assignSingleSignOnSessionResultMock,
getSingleSignOnSessionResult: getSingleSignOnSessionResultMock,
assignSingleSignOnAuthenticationResult: assignSingleSignOnAuthenticationResultMock,
} = await mockEsmWithActual('./single-sign-on-session.js', () => ({
assignSingleSignOnSessionResult: jest.fn(),
getSingleSignOnSessionResult: jest.fn(),
assignSingleSignOnAuthenticationResult: jest.fn(),
}));

jest
.spyOn(ssoConnectorFactories.OIDC, 'constructor')
.mockImplementation((data: SingleSignOnConnectorData) => new MockOidcSsoConnector(data));
jest
.spyOn(ssoConnectorFactories.SAML, 'constructor')
.mockImplementation(
(data: SingleSignOnConnectorData) => new MockSamlSsoConnector(data, 'tenantId')
);

const {
getSsoAuthorizationUrl,
Expand Down Expand Up @@ -83,6 +108,10 @@ describe('Single sign on util methods tests', () => {
updateUserById: updateUserMock,
findUserByEmail: findUserByEmailMock,
},
ssoConnectors: {
findIdpInitiatedSamlSsoSessionById: findIdpInitiatedSamlSsoSessionMock,
deleteIdpInitiatedSamlSsoSessionById: deleteIdpInitiatedSamlSsoSessionMock,
},
},
undefined,
{
Expand Down Expand Up @@ -327,4 +356,171 @@ describe('Single sign on util methods tests', () => {
});
});
});

describe('getSsoAuthorizationUrl tests with idp initiated sso session', () => {
const stub = Sinon.stub(EnvSet, 'values').value({
...EnvSet.values,
isDevFeaturesEnabled: true,
});

const payload = {
state: 'state',
redirectUri: 'https://example.com',
};

const samlSsoSessionId = 'samlSsoSessionId';
const samlAuthorizationUrl = 'https://saml-connector/callback';

const mockContextWithIdpInitiatedSsoSession = {
...mockContext,
...createContextWithRouteParameters({
cookies: {
[idpInitiatedSamlSsoSessionCookieName]: samlSsoSessionId,
},
}),
};

afterAll(() => {
stub.restore();
});

beforeEach(() => {
getAuthorizationUrlMock.mockResolvedValueOnce(samlAuthorizationUrl);
});

it('should not check idp initiated sso session if the connector is not SAML', async () => {
await expect(
getSsoAuthorizationUrl(
mockContextWithIdpInitiatedSsoSession,
tenant,
wellConfiguredSsoConnector,
payload
)
).resolves.toBe(samlAuthorizationUrl);

expect(findIdpInitiatedSamlSsoSessionMock).not.toHaveBeenCalled();
});

it('should not check idp initiated sso session if the session cookie is not found', async () => {
await expect(
getSsoAuthorizationUrl(mockContext, tenant, mockSamlSsoConnector, payload)
).resolves.toBe(samlAuthorizationUrl);

expect(findIdpInitiatedSamlSsoSessionMock).not.toHaveBeenCalled();
expect(deleteIdpInitiatedSamlSsoSessionMock).not.toHaveBeenCalled();
});

it('should redirect to the connector authorization uri if the idp initiated sso session is not found', async () => {
findIdpInitiatedSamlSsoSessionMock.mockResolvedValueOnce(null);

await expect(
getSsoAuthorizationUrl(
mockContextWithIdpInitiatedSsoSession,
tenant,
mockSamlSsoConnector,
payload
)
).resolves.toBe(samlAuthorizationUrl);

expect(findIdpInitiatedSamlSsoSessionMock).toBeCalledWith(samlSsoSessionId);
});

it('should redirect to the connector authorization uri if the idp initiated sso session connectorId mismatch', async () => {
findIdpInitiatedSamlSsoSessionMock.mockResolvedValueOnce({
id: samlSsoSessionId,
connectorId: 'foo',
});

await expect(
getSsoAuthorizationUrl(
mockContextWithIdpInitiatedSsoSession,
tenant,
mockSamlSsoConnector,
payload
)
).resolves.toBe(samlAuthorizationUrl);

expect(findIdpInitiatedSamlSsoSessionMock).toBeCalledWith(samlSsoSessionId);
expect(deleteIdpInitiatedSamlSsoSessionMock).not.toHaveBeenCalled();
});

it('should redirect to the connector authorization uri if the idp initiated sso session is expired', async () => {
findIdpInitiatedSamlSsoSessionMock.mockResolvedValueOnce({
id: samlSsoSessionId,
connectorId: mockSamlSsoConnector.id,
expiresAt: Date.now() - 1000 * 60 * 11,
});

await expect(
getSsoAuthorizationUrl(
mockContextWithIdpInitiatedSsoSession,
tenant,
mockSamlSsoConnector,
payload
)
).resolves.toBe(samlAuthorizationUrl);

expect(findIdpInitiatedSamlSsoSessionMock).toBeCalledWith(samlSsoSessionId);

// Should delete the idp initiated sso session
expect(mockContextWithIdpInitiatedSsoSession.cookies.set).toBeCalledWith(
idpInitiatedSamlSsoSessionCookieName,
'',
{
httpOnly: true,
expires: new Date(0),
}
);
expect(deleteIdpInitiatedSamlSsoSessionMock).toBeCalledWith(samlSsoSessionId);
});

it('should assign the user info and redirect to the SSO callback uri if the idp initiated sso session is valid', async () => {
findIdpInitiatedSamlSsoSessionMock.mockResolvedValueOnce({
id: samlSsoSessionId,
connectorId: mockSamlSsoConnector.id,
expiresAt: Date.now() + 1000 * 60 * 10,
assertionContent: {
nameID: mockSsoUserInfo.id,
attributes: {
email: mockSsoUserInfo.email,
name: mockSsoUserInfo.name,
avatar: mockSsoUserInfo.avatar,
},
},
});

await expect(
getSsoAuthorizationUrl(
mockContextWithIdpInitiatedSsoSession,
tenant,
mockSamlSsoConnector,
payload
)
).resolves.toBe(`${payload.redirectUri}/?state=${payload.state}`);

expect(findIdpInitiatedSamlSsoSessionMock).toBeCalledWith(samlSsoSessionId);

expect(assignSingleSignOnSessionResultMock).toBeCalledWith(
mockContextWithIdpInitiatedSsoSession,
mockProvider,
{
connectorId: mockSamlSsoConnector.id,
...payload,
userInfo: mockSsoUserInfo,
}
);

// Should delete the idp initiated sso session
expect(mockContextWithIdpInitiatedSsoSession.cookies.set).toBeCalledWith(
idpInitiatedSamlSsoSessionCookieName,
'',
{
httpOnly: true,
expires: new Date(0),
}
);
expect(deleteIdpInitiatedSamlSsoSessionMock).toBeCalledWith(samlSsoSessionId);
});
});
});
/* eslint-enable max-lines */
6 changes: 3 additions & 3 deletions packages/core/src/routes/interaction/utils/single-sign-on.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@ import { generateStandardId } from '@logto/shared';
import { conditional, trySafe } from '@silverhand/essentials';
import { z } from 'zod';

import { idpInitiatedSamlSsoSessionCookieName } from '#src/constants/index.js';
import { EnvSet } from '#src/env-set/index.js';
import RequestError from '#src/errors/RequestError/index.js';
import { type WithLogContext } from '#src/middleware/koa-audit-log.js';
import SamlConnector from '#src/sso/SamlConnector/index.js';
import { ssoConnectorFactories, type SingleSignOnConnectorSession } from '#src/sso/index.js';
import type Queries from '#src/tenants/Queries.js';
import type TenantContext from '#src/tenants/TenantContext.js';
import assertThat from '#src/utils/assert-that.js';

import { idpInitiatedSamlSsoSessionCookieName } from '../../../constants/index.js';
import { EnvSet } from '../../../env-set/index.js';
import SamlConnector from '../../../sso/SamlConnector/index.js';
import { type WithInteractionHooksContext } from '../middleware/koa-interaction-hooks.js';

import {
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/utils/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export const createContextWithRouteParameters = (
set: ctx.set,
path: ctx.path,
URL: ctx.URL,
cookies: ctx.cookies,
params: {},
headers: {},
router: new Router(),
Expand Down

0 comments on commit 4eb15aa

Please sign in to comment.