-
-
Notifications
You must be signed in to change notification settings - Fork 1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(core): Implement configurable session caching
Relates to #394. BREAKING CHANGE: The `RequestContext.session` object is no longer a `Session` entity. Instead it is a new type, `SerializedSession` which contains a subset of data pertaining to the current session. For example, if you have custom code which references `ctx.session.activeOrder` you will now get an error, since `activeOrder` does not exist on `SerializedSession`. Instead you would use `SerializedSession.activeOrderId` and then lookup the order in a separate query. The reason for this change is to enable efficient session caching.
- Loading branch information
1 parent
59dfaad
commit 09a432d
Showing
24 changed files
with
758 additions
and
285 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
/* tslint:disable:no-non-null-assertion */ | ||
import { CachedSession, mergeConfig, SessionCacheStrategy } from '@vendure/core'; | ||
import { createTestEnvironment } from '@vendure/testing'; | ||
import gql from 'graphql-tag'; | ||
import path from 'path'; | ||
|
||
import { initialData } from '../../../e2e-common/e2e-initial-data'; | ||
import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config'; | ||
import { SUPER_ADMIN_USER_IDENTIFIER, SUPER_ADMIN_USER_PASSWORD } from '../../common/src/shared-constants'; | ||
|
||
import { AttemptLogin, Me } from './graphql/generated-e2e-admin-types'; | ||
import { ATTEMPT_LOGIN, ME } from './graphql/shared-definitions'; | ||
|
||
const testSessionCache = new Map<string, CachedSession>(); | ||
const getSpy = jest.fn(); | ||
const setSpy = jest.fn(); | ||
const clearSpy = jest.fn(); | ||
const deleteSpy = jest.fn(); | ||
|
||
class TestingSessionCacheStrategy implements SessionCacheStrategy { | ||
clear() { | ||
clearSpy(); | ||
testSessionCache.clear(); | ||
} | ||
|
||
delete(sessionToken: string) { | ||
deleteSpy(sessionToken); | ||
testSessionCache.delete(sessionToken); | ||
} | ||
|
||
get(sessionToken: string) { | ||
getSpy(sessionToken); | ||
return testSessionCache.get(sessionToken); | ||
} | ||
|
||
set(session: CachedSession) { | ||
setSpy(session); | ||
testSessionCache.set(session.token, session); | ||
} | ||
} | ||
|
||
describe('Session caching', () => { | ||
const { server, adminClient } = createTestEnvironment( | ||
mergeConfig(testConfig, { | ||
authOptions: { | ||
sessionCacheStrategy: new TestingSessionCacheStrategy(), | ||
sessionCacheTTL: 2, | ||
}, | ||
}), | ||
); | ||
|
||
beforeAll(async () => { | ||
await server.init({ | ||
initialData, | ||
productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'), | ||
customerCount: 1, | ||
}); | ||
await adminClient.asSuperAdmin(); | ||
}, TEST_SETUP_TIMEOUT_MS); | ||
|
||
afterAll(async () => { | ||
await server.destroy(); | ||
}); | ||
|
||
it('populates the cache on login', async () => { | ||
setSpy.mockClear(); | ||
expect(setSpy.mock.calls.length).toBe(0); | ||
|
||
await adminClient.query<AttemptLogin.Mutation, AttemptLogin.Variables>(ATTEMPT_LOGIN, { | ||
username: SUPER_ADMIN_USER_IDENTIFIER, | ||
password: SUPER_ADMIN_USER_PASSWORD, | ||
}); | ||
|
||
expect(testSessionCache.size).toBe(1); | ||
expect(setSpy.mock.calls.length).toBe(1); | ||
}); | ||
|
||
it('takes user data from cache on next request', async () => { | ||
getSpy.mockClear(); | ||
const { me } = await adminClient.query<Me.Query>(ME); | ||
|
||
expect(getSpy.mock.calls.length).toBe(1); | ||
}); | ||
|
||
it('sets fresh data after TTL expires', async () => { | ||
setSpy.mockClear(); | ||
|
||
await adminClient.query<Me.Query>(ME); | ||
expect(setSpy.mock.calls.length).toBe(0); | ||
|
||
await adminClient.query<Me.Query>(ME); | ||
expect(setSpy.mock.calls.length).toBe(0); | ||
|
||
await pause(2000); | ||
|
||
await adminClient.query<Me.Query>(ME); | ||
expect(setSpy.mock.calls.length).toBe(1); | ||
}); | ||
|
||
it('clears cache for that user on logout', async () => { | ||
deleteSpy.mockClear(); | ||
|
||
await adminClient.query( | ||
gql` | ||
mutation { | ||
logout | ||
} | ||
`, | ||
); | ||
|
||
expect(testSessionCache.size).toBe(0); | ||
expect(deleteSpy.mock.calls.length).toBe(1); | ||
}); | ||
}); | ||
|
||
describe('Session expiry', () => { | ||
const { server, adminClient } = createTestEnvironment( | ||
mergeConfig(testConfig, { | ||
authOptions: { | ||
sessionDuration: '3s', | ||
sessionCacheTTL: 1, | ||
}, | ||
}), | ||
); | ||
|
||
beforeAll(async () => { | ||
await server.init({ | ||
initialData, | ||
productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'), | ||
customerCount: 1, | ||
}); | ||
await adminClient.asSuperAdmin(); | ||
}, TEST_SETUP_TIMEOUT_MS); | ||
|
||
afterAll(async () => { | ||
await server.destroy(); | ||
}); | ||
|
||
it('session does not expire with continued use', async () => { | ||
await adminClient.asSuperAdmin(); | ||
await pause(1000); | ||
await adminClient.query(ME); | ||
await pause(1000); | ||
await adminClient.query(ME); | ||
await pause(1000); | ||
await adminClient.query(ME); | ||
await pause(1000); | ||
await adminClient.query(ME); | ||
}, 10000); | ||
|
||
it('session expires when not used for longer than sessionDuration', async () => { | ||
await adminClient.asSuperAdmin(); | ||
await pause(3000); | ||
try { | ||
await adminClient.query(ME); | ||
fail('Should have thrown'); | ||
} catch (e) { | ||
expect(e.message).toContain('You are not currently authorized to perform this action'); | ||
} | ||
}, 10000); | ||
}); | ||
|
||
function pause(ms: number): Promise<void> { | ||
return new Promise((resolve) => setTimeout(resolve, ms)); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.