From d4a00b19441cfb61087667a8d38d9b3883ecebf8 Mon Sep 17 00:00:00 2001 From: Julien Date: Sun, 16 Jul 2023 23:24:37 +0200 Subject: [PATCH 001/112] chore: add docker-compose for testing --- docker-compose.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..18b513a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,8 @@ +version: '3.4' + +services: + redis: + image: redis:alpine + ports: + - 6379:6379 + command: redis-server --appendonly yes From 9d12d0700af9cfd3ceb47959f63e880d5cff6057 Mon Sep 17 00:00:00 2001 From: Julien Date: Sun, 16 Jul 2023 23:24:49 +0200 Subject: [PATCH 002/112] chore: add japa/api-client --- package.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 673670c..0b7b621 100644 --- a/package.json +++ b/package.json @@ -25,10 +25,11 @@ "@adonisjs/mrm-preset": "^5.0.3", "@adonisjs/redis": "^7.3.1", "@adonisjs/require-ts": "^2.0.12", + "@japa/api-client": "^1.4.4", "@japa/assert": "^1.3.5", - "@japa/preset-adonis": "^1.1.1", + "@japa/preset-adonis": "^1.2.0", "@japa/run-failed-tests": "^1.0.8", - "@japa/runner": "^2.1.1", + "@japa/runner": "^2.5.1", "@japa/spec-reporter": "^1.2.0", "@poppinss/dev-utils": "^2.0.3", "@types/node": "^18.7.15", @@ -54,6 +55,7 @@ "mrm": "mrm --preset=@adonisjs/mrm-preset", "pretest": "npm run lint", "test": "node -r @adonisjs/require-ts/build/register ./bin/test.ts", + "quick:test": "node -r @adonisjs/require-ts/build/register ./bin/test.ts", "clean": "del build", "compile": "npm run lint && npm run clean && tsc", "copyfiles": "copyfiles \"templates/**/*.txt\" \"instructions.md\" build", From cfdf1b473405d9aeb0799a6bdb850b5c220117e7 Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 21 Jul 2023 11:46:02 +0200 Subject: [PATCH 003/112] refactor: prepare for v6 --- adonis-typings/container.ts | 16 - adonis-typings/context.ts | 16 - adonis-typings/index.ts | 13 - adonis-typings/session.ts | 319 ------------------ bin/japaTypes.ts | 7 - bin/test.ts | 23 +- config.ts | 17 - factories/session_manager_factory.ts | 71 ++++ index.ts | 10 + package.json | 35 +- providers/SessionProvider.ts | 64 ---- providers/session_provider.ts | 49 +++ src/Bindings/Server.ts | 56 --- src/Drivers/Cookie.ts | 24 +- src/Drivers/File.ts | 32 +- src/Drivers/Memory.ts | 6 +- src/Drivers/Redis.ts | 53 +-- src/SessionManager/index.ts | 195 ----------- .../Tests.ts => bindings/api_client.ts} | 24 +- src/bindings/http_context.ts | 24 ++ .../tests.ts => src/bindings/types.ts | 31 +- src/{Client/index.ts => client.ts} | 66 ++-- src/define_config.ts | 17 + src/{Session/index.ts => session.ts} | 287 ++++++++-------- src/session_manager.ts | 187 ++++++++++ src/session_middleware.ts | 27 ++ src/{Store/index.ts => store.ts} | 28 +- src/types.ts | 76 +++++ test-helpers/index.ts | 135 -------- ...e-driver.spec.ts => cookie_driver.spec.ts} | 62 ++-- test/define_config.spec.ts | 18 + ...ile-driver.spec.ts => file_driver.spec.ts} | 45 ++- ...is-driver.spec.ts => redis_driver.spec.ts} | 34 +- test/session-provider.spec.ts | 193 ----------- test/session.spec.ts | 219 ++++++------ ...-client.spec.ts => session_client.spec.ts} | 61 ++-- ...anager.spec.ts => session_manager.spec.ts} | 118 +++---- test/session_middleware.spec.ts | 47 +++ test/session_provider.spec.ts | 144 ++++++++ test/store.spec.ts | 4 +- test_helpers/index.ts | 143 ++++++++ tsconfig.json | 12 +- 42 files changed, 1423 insertions(+), 1585 deletions(-) delete mode 100644 adonis-typings/container.ts delete mode 100644 adonis-typings/context.ts delete mode 100644 adonis-typings/index.ts delete mode 100644 adonis-typings/session.ts delete mode 100644 bin/japaTypes.ts delete mode 100644 config.ts create mode 100644 factories/session_manager_factory.ts create mode 100644 index.ts delete mode 100644 providers/SessionProvider.ts create mode 100644 providers/session_provider.ts delete mode 100644 src/Bindings/Server.ts delete mode 100644 src/SessionManager/index.ts rename src/{Bindings/Tests.ts => bindings/api_client.ts} (86%) create mode 100644 src/bindings/http_context.ts rename adonis-typings/tests.ts => src/bindings/types.ts (72%) rename src/{Client/index.ts => client.ts} (55%) create mode 100644 src/define_config.ts rename src/{Session/index.ts => session.ts} (57%) create mode 100644 src/session_manager.ts create mode 100644 src/session_middleware.ts rename src/{Store/index.ts => store.ts} (81%) create mode 100644 src/types.ts delete mode 100644 test-helpers/index.ts rename test/{cookie-driver.spec.ts => cookie_driver.spec.ts} (56%) create mode 100644 test/define_config.spec.ts rename test/{file-driver.spec.ts => file_driver.spec.ts} (64%) rename test/{redis-driver.spec.ts => redis_driver.spec.ts} (77%) delete mode 100644 test/session-provider.spec.ts rename test/{session-client.spec.ts => session_client.spec.ts} (72%) rename test/{session-manager.spec.ts => session_manager.spec.ts} (56%) create mode 100644 test/session_middleware.spec.ts create mode 100644 test/session_provider.spec.ts create mode 100644 test_helpers/index.ts diff --git a/adonis-typings/container.ts b/adonis-typings/container.ts deleted file mode 100644 index fd32c56..0000000 --- a/adonis-typings/container.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @adonisjs/session - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare module '@ioc:Adonis/Core/Application' { - import { SessionManagerContract } from '@ioc:Adonis/Addons/Session' - - interface ContainerBindings { - 'Adonis/Addons/Session': SessionManagerContract - } -} diff --git a/adonis-typings/context.ts b/adonis-typings/context.ts deleted file mode 100644 index 86a0deb..0000000 --- a/adonis-typings/context.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @adonisjs/session - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare module '@ioc:Adonis/Core/HttpContext' { - import { SessionContract } from '@ioc:Adonis/Addons/Session' - - interface HttpContextContract { - session: SessionContract - } -} diff --git a/adonis-typings/index.ts b/adonis-typings/index.ts deleted file mode 100644 index dffc5ad..0000000 --- a/adonis-typings/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * @adonisjs/session - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -/// -/// -/// -/// diff --git a/adonis-typings/session.ts b/adonis-typings/session.ts deleted file mode 100644 index dfa1e73..0000000 --- a/adonis-typings/session.ts +++ /dev/null @@ -1,319 +0,0 @@ -/* - * @adonisjs/session - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare module '@ioc:Adonis/Addons/Session' { - import { CookieOptions } from '@ioc:Adonis/Core/Response' - import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' - import { ApplicationContract } from '@ioc:Adonis/Core/Application' - - /** - * Shape of session config. - */ - export interface SessionConfig { - /** - * Enable/disable session for the entire application lifecycle - */ - enabled: boolean - - /** - * The driver in play - */ - driver: string - - /** - * Cookie name. - */ - cookieName: string - - /** - * Clear session when browser closes - */ - clearWithBrowser: boolean - - /** - * Age of session cookie - */ - age: string | number - - /** - * Config for the cookie driver and also the session id - * cookie - */ - cookie: Omit, 'maxAge' | 'expires'> - - /** - * Config for the file driver - */ - file?: { - location: string - } - - /** - * The redis connection to use from the `config/redis` file - */ - redisConnection?: string - } - - /** - * Shape of a driver that every session driver must have - */ - export interface SessionDriverContract { - read(sessionId: string): Promise | null> | Record | null - write(sessionId: string, values: Record): Promise | void - destroy(sessionId: string): Promise | void - touch(sessionId: string): Promise | void - } - - /** - * The callback to be passed to the `extend` method. It is invoked - * for each request (if extended driver is in use). - */ - export type ExtendCallback = ( - manager: SessionManagerContract, - config: SessionConfig, - ctx: HttpContextContract - ) => SessionDriverContract - - /** - * The values allowed by the `session.put` method - */ - export type AllowedSessionValues = string | boolean | number | object | Date | Array - - /** - * Store used for storing session values + flash messages - */ - export interface StoreContract { - /** - * A boolean to know if store is empty - */ - isEmpty: boolean - - /** - * Set value for a key - */ - set(key: string, value: AllowedSessionValues): void - - /** - * Increment value for a key. An exception is raised when existing - * value is not a number - */ - increment(key: string, steps?: number): void - - /** - * Decrement value for a key. An exception is raised when existing - * value is not a number - */ - decrement(key: string, steps?: number): void - - /** - * Replace existing values with new ones - */ - update(values: Record): void - - /** - * Merge values with existing ones - */ - merge(values: Record): any - - /** - * Get all values - */ - all(): any - - /** - * Get value for a given key or use the default value - */ - get(key: string, defaultValue?: any): any - - /** - * Find if a value exists. Optionally you can also check arrays - * to have length too - */ - has(key: string, checkForArraysLength?: boolean): boolean - - /** - * Unset value - */ - unset(key: string): void - - /** - * Clear all values - */ - clear(): void - - /** - * Read value and then unset it at the same time - */ - pull(key: string, defaultValue?: any): any - - /** - * Convert store values toObject - */ - toObject(): any - - /** - * Convert store values to toJSON - */ - toJSON(): any - } - - /** - * Shape of the actual session store - */ - export interface SessionContract { - /** - * Has the store being initiated - */ - initiated: boolean - - /** - * Is session store readonly. Will be during Websockets - * request - */ - readonly: boolean - - /** - * Is session just created or we read received the - * session id from the request - */ - fresh: boolean - - /** - * Session id - */ - sessionId: string - - /** - * Previous request flash messages - */ - flashMessages: StoreContract - - /** - * Flash messages that will be sent in the current - * request response - */ - responseFlashMessages: StoreContract - - /** - * Initiate session store - */ - initiate(readonly: boolean): Promise - - /** - * Commit session mutations - */ - commit(): Promise - - /** - * Re-generate session id. This help avoid session - * replay attacks. - */ - regenerate(): void - - /** - * Store API - */ - has(key: string): boolean - put(key: string, value: AllowedSessionValues): void - get(key: string, defaultValue?: any): any - all(): any - forget(key: string): void - pull(key: string, defaultValue?: any): any - increment(key: string, steps?: number): any - decrement(key: string, steps?: number): any - clear(): void - - /** - * Flash a key-value pair - */ - flash(values: { [key: string]: AllowedSessionValues }): void - flash(key: string, value: AllowedSessionValues): void - - /** - * Flash request body - */ - flashAll(): void - - /** - * Flash selected keys from the request body - */ - flashOnly(keys: string[]): void - - /** - * Omit selected keys from the request data and flash - * the rest of values - */ - flashExcept(keys: string[]): void - - /** - * Reflash existing flash messages - */ - reflash(): void - - /** - * Reflash selected keys from the existing flash messages - */ - reflashOnly(keys: string[]): void - - /** - * Omit selected keys from the existing flash messages - * and flash the rest of values - */ - reflashExcept(keys: string[]): void - } - - /** - * SessionClient exposes the API to set session data as a client - */ - export interface SessionClientContract extends StoreContract { - /** - * Find if the sessions are enabled - */ - isEnabled(): boolean - - /** - * Flash messages store to set flash messages - */ - flashMessages: StoreContract - - /** - * Load session data from the driver - */ - load(cookies: Record): Promise<{ - session: Record - flashMessages: Record | null - }> - - /** - * Commits the session data to the session store and returns - * the session id and cookie name for it to be accessible - * by the server - */ - commit(): Promise<{ cookieName: string; sessionId: string; signedSessionId: string }> - - /** - * Forget the session data. - */ - forget(): Promise - } - - /** - * Session manager shape - */ - export interface SessionManagerContract { - isEnabled(): boolean - application: ApplicationContract - client(): SessionClientContract - create(ctx: HttpContextContract): SessionContract - extend(driver: string, callback: ExtendCallback): void - } - - const Session: SessionManagerContract - export default Session -} diff --git a/bin/japaTypes.ts b/bin/japaTypes.ts deleted file mode 100644 index d42cac6..0000000 --- a/bin/japaTypes.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Assert } from '@japa/assert' - -declare module '@japa/runner' { - interface TestContext { - assert: Assert - } -} diff --git a/bin/test.ts b/bin/test.ts index 5aba7ce..156fc01 100644 --- a/bin/test.ts +++ b/bin/test.ts @@ -1,7 +1,7 @@ import { assert } from '@japa/assert' -import { specReporter } from '@japa/spec-reporter' -import { runFailedTests } from '@japa/run-failed-tests' -import { processCliArgs, configure, run } from '@japa/runner' +import { processCLIArgs, configure, run } from '@japa/runner' +import { fileSystem } from '@japa/file-system' +import { BASE_URL } from '../test_helpers/index.js' /* |-------------------------------------------------------------------------- @@ -16,14 +16,17 @@ import { processCliArgs, configure, run } from '@japa/runner' | | Please consult japa.dev/runner-config for the config docs. */ +processCLIArgs(process.argv.slice(2)) configure({ - ...processCliArgs(process.argv.slice(2)), - ...{ - files: ['test/**/*.spec.ts'], - plugins: [assert(), runFailedTests()], - reporters: [specReporter()], - importer: (filePath: string) => import(filePath), - }, + files: ['test/**/*.spec.ts'], + plugins: [ + assert(), + fileSystem({ + autoClean: true, + basePath: BASE_URL, + }), + ], + forceExit: true, }) /* diff --git a/config.ts b/config.ts deleted file mode 100644 index e4e8077..0000000 --- a/config.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @adonisjs/session - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { SessionConfig } from '@ioc:Adonis/Addons/Session' - -/** - * Helper to define session config - */ -export function sessionConfig(config: Config): Config { - return config -} diff --git a/factories/session_manager_factory.ts b/factories/session_manager_factory.ts new file mode 100644 index 0000000..bb50fad --- /dev/null +++ b/factories/session_manager_factory.ts @@ -0,0 +1,71 @@ +/* + * @adonisjs/session + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import type { SessionConfig } from '../src/types.js' +import type { Application } from '@adonisjs/application' +import type { RedisConnectionConfig } from '@adonisjs/redis/types' +import { EncryptionFactory } from '@adonisjs/core/factories/encryption' +import { SessionManager } from '../src/session_manager.js' +import { RedisManagerFactory } from '@adonisjs/redis/factories' + +/** + * Session Manager Factory is used to create an instance of + * session manager for testing + */ +export class SessionManagerFactory { + /** + * Default configuration for the session manager + */ + #options: SessionConfig = { + enabled: true, + driver: 'cookie', + cookieName: 'adonis-session', + clearWithBrowser: false, + age: 3000, + cookie: { path: '/' }, + } + + /** + * Configuration for the redis manager + */ + #redisManagerOptions = { + connection: 'local', + connections: { local: { host: '127.0.0.1', port: 6379, } } + } as const + + /** + * Merge factory parameters + */ + merge(options: SessionConfig) { + this.#options = Object.assign(this.#options, options) + return this + } + + /** + * Merge redis manager parameters + */ + mergeRedisManagerOptions>(options: { + connection: keyof Connections, + connections: Connections, + } + ) { + this.#redisManagerOptions = Object.assign(this.#redisManagerOptions, options) + return this + } + + /** + * Create Session manager instance + */ + create(app: Application) { + return new SessionManager(this.#options, + new EncryptionFactory().create(), + new RedisManagerFactory(this.#redisManagerOptions).create(app), + ) + } +} diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..a9d4008 --- /dev/null +++ b/index.ts @@ -0,0 +1,10 @@ +/** + * @adonisjs/session + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export { defineConfig } from './src/define_config.js' diff --git a/package.json b/package.json index 0b7b621..21d9da9 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "Session provider for AdonisJS", "typings": "./build/adonis-typings/index.d.ts", "main": "build/providers/SessionProvider.js", + "type": "module", "files": [ "build/adonis-typings", "build/providers", @@ -14,25 +15,34 @@ "build/instructions.md" ], "dependencies": { - "@poppinss/utils": "^5.0.0", - "fs-extra": "^10.1.0" + "@poppinss/utils": "6.5.0-3", + "fs-extra": "^11.1.1" }, "peerDependencies": { - "@adonisjs/core": "^5.8.0" + "@adonisjs/core": "6.1.5-8", + "@adonisjs/redis": "8.0.0-1" }, "devDependencies": { - "@adonisjs/core": "^5.8.6", + "@adonisjs/application": "7.1.2-8", + "@adonisjs/core": "6.1.5-8", + "@adonisjs/eslint-config": "^1.1.8", "@adonisjs/mrm-preset": "^5.0.3", - "@adonisjs/redis": "^7.3.1", + "@adonisjs/prettier-config": "^1.1.8", + "@adonisjs/redis": "8.0.0-1", "@adonisjs/require-ts": "^2.0.12", + "@adonisjs/tsconfig": "^1.1.8", + "@adonisjs/view": "7.0.0-4", "@japa/api-client": "^1.4.4", - "@japa/assert": "^1.3.5", - "@japa/preset-adonis": "^1.2.0", - "@japa/run-failed-tests": "^1.0.8", - "@japa/runner": "^2.5.1", - "@japa/spec-reporter": "^1.2.0", + "@japa/assert": "2.0.0-1", + "@japa/file-system": "2.0.0-1", + "@japa/plugin-adonisjs": "2.0.0-1", + "@japa/runner": "3.0.0-6", + "@paralleldrive/cuid2": "^2.2.1", "@poppinss/dev-utils": "^2.0.3", + "@swc/core": "^1.3.69", + "@types/fs-extra": "^11.0.1", "@types/node": "^18.7.15", + "@types/set-cookie-parser": "^2.4.3", "@types/supertest": "^2.0.12", "commitizen": "^4.2.5", "copyfiles": "^2.4.1", @@ -49,13 +59,14 @@ "prettier": "^2.7.1", "set-cookie-parser": "^2.5.1", "supertest": "^6.2.4", + "ts-node": "^10.9.1", "typescript": "^4.8.2" }, "scripts": { "mrm": "mrm --preset=@adonisjs/mrm-preset", "pretest": "npm run lint", - "test": "node -r @adonisjs/require-ts/build/register ./bin/test.ts", - "quick:test": "node -r @adonisjs/require-ts/build/register ./bin/test.ts", + "test": "c8 npm run quick:test", + "quick:test": "node --enable-source-maps --loader=ts-node/esm bin/test.ts", "clean": "del build", "compile": "npm run lint && npm run clean && tsc", "copyfiles": "copyfiles \"templates/**/*.txt\" \"instructions.md\" build", diff --git a/providers/SessionProvider.ts b/providers/SessionProvider.ts deleted file mode 100644 index cf6d601..0000000 --- a/providers/SessionProvider.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * @adonisjs/session - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { ApplicationContract } from '@ioc:Adonis/Core/Application' - -/** - * Session provider for AdonisJS - */ -export default class SessionProvider { - constructor(protected app: ApplicationContract) {} - public static needsApplication = true - - /** - * Register Session Manager - */ - public register(): void { - this.app.container.singleton('Adonis/Addons/Session', () => { - const { SessionManager } = require('../src/SessionManager') - return new SessionManager(this.app, this.app.config.get('session', {})) - }) - } - - /** - * Register bindings for tests - */ - protected registerTestsBindings() { - this.app.container.withBindings( - [ - 'Japa/Preset/ApiRequest', - 'Japa/Preset/ApiResponse', - 'Japa/Preset/ApiClient', - 'Adonis/Addons/Session', - ], - (ApiRequest, ApiResponse, ApiClient, Session) => { - const { defineTestsBindings } = require('../src/Bindings/Tests') - defineTestsBindings(ApiRequest, ApiResponse, ApiClient, Session) - } - ) - } - - /** - * Register server bindings - */ - protected registerServerBindings() { - this.app.container.withBindings( - ['Adonis/Core/Server', 'Adonis/Core/HttpContext', 'Adonis/Addons/Session'], - (Server, HttpContext, Session) => { - const { defineServerBindings } = require('../src/Bindings/Server') - defineServerBindings(HttpContext, Server, Session) - } - ) - } - - public boot(): void { - this.registerServerBindings() - this.registerTestsBindings() - } -} diff --git a/providers/session_provider.ts b/providers/session_provider.ts new file mode 100644 index 0000000..7b1d201 --- /dev/null +++ b/providers/session_provider.ts @@ -0,0 +1,49 @@ +/* + * @adonisjs/session + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { ApplicationService } from '@adonisjs/core/types' +import { extendHttpContext } from '../src/bindings/http_context.js' +import { extendApiClient } from '../src/bindings/api_client.js' + +export default class SessionProvider { + constructor(protected app: ApplicationService) {} + + /** + * Register Session Manager in the container + */ + async register() { + this.app.container.singleton('session', async () => { + const { SessionManager } = await import('../src/session_manager.js') + + const encryption = await this.app.container.make('encryption') + const redis = await this.app.container.make('redis').catch(() => undefined) + const config = this.app.config.get('session', {}) + + return new SessionManager(config, encryption, redis) + }) + } + + /** + * Register bindings + */ + async boot() { + const sessionManager = await this.app.container.make('session') + + /** + * Add `session` getter to the HttpContext class + */ + extendHttpContext(sessionManager) + + /** + * Add some macros and getter to japa/api-client classes for + * easier testing + */ + extendApiClient(sessionManager) + } +} diff --git a/src/Bindings/Server.ts b/src/Bindings/Server.ts deleted file mode 100644 index 56d02ef..0000000 --- a/src/Bindings/Server.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * @adonisjs/session - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -/// - -import { ServerContract } from '@ioc:Adonis/Core/Server' -import { SessionManagerContract } from '@ioc:Adonis/Addons/Session' -import { HttpContextConstructorContract } from '@ioc:Adonis/Core/HttpContext' - -/** - * Share "session" with the HTTP context. Define hooks to initiate and - * commit session when sessions are enabled. - */ -export function defineServerBindings( - HttpContext: HttpContextConstructorContract, - Server: ServerContract, - Session: SessionManagerContract -) { - /** - * Sharing session with the context - */ - HttpContext.getter( - 'session', - function session() { - return Session.create(this) - }, - true - ) - - /** - * Do not register hooks when sessions are disabled - */ - if (!Session.isEnabled()) { - return - } - - /** - * Initiate session store - */ - Server.hooks.before(async (ctx) => { - await ctx.session.initiate(false) - }) - - /** - * Commit store mutations - */ - Server.hooks.after(async (ctx) => { - await ctx.session.commit() - }) -} diff --git a/src/Drivers/Cookie.ts b/src/Drivers/Cookie.ts index 80dfd02..32680d9 100644 --- a/src/Drivers/Cookie.ts +++ b/src/Drivers/Cookie.ts @@ -1,28 +1,32 @@ /* * @adonisjs/session * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -/// - -import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' -import { SessionDriverContract, SessionConfig } from '@ioc:Adonis/Addons/Session' +import type { HttpContext } from '@adonisjs/core/http' +import type { SessionConfig, SessionDriverContract } from '../types.js' /** * Cookie driver utilizes the encrypted HTTP cookies to write session value. */ export class CookieDriver implements SessionDriverContract { - constructor(private config: SessionConfig, private ctx: HttpContextContract) {} + #config: SessionConfig + #ctx: HttpContext + + constructor(config: SessionConfig, ctx: HttpContext) { + this.#config = config + this.#ctx = ctx + } /** * Read session value from the cookie */ public read(sessionId: string): { [key: string]: any } | null { - const cookieValue = this.ctx.request.encryptedCookie(sessionId) + const cookieValue = this.#ctx.request.encryptedCookie(sessionId) if (typeof cookieValue !== 'object') { return null } @@ -37,15 +41,15 @@ export class CookieDriver implements SessionDriverContract { throw new Error('Session cookie driver expects an object of values') } - this.ctx.response.encryptedCookie(sessionId, values, this.config.cookie) + this.#ctx.response.encryptedCookie(sessionId, values, this.#config.cookie) } /** * Removes the session cookie */ public destroy(sessionId: string): void { - if (this.ctx.request.cookiesList()[sessionId]) { - this.ctx.response.clearCookie(sessionId) + if (this.#ctx.request.cookiesList()[sessionId]) { + this.#ctx.response.clearCookie(sessionId) } } diff --git a/src/Drivers/File.ts b/src/Drivers/File.ts index 698ab2d..11ff5ea 100644 --- a/src/Drivers/File.ts +++ b/src/Drivers/File.ts @@ -1,30 +1,32 @@ /** * @adonisjs/session * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -/// - import { join } from 'path' import { Exception } from '@poppinss/utils' -import { MessageBuilder } from '@poppinss/utils/build/helpers' -import { readFile, ensureFile, outputFile, remove } from 'fs-extra' -import { SessionDriverContract, SessionConfig } from '@ioc:Adonis/Addons/Session' +import { MessageBuilder } from '@poppinss/utils' +import { ensureFile, outputFile, remove } from 'fs-extra/esm' +import { readFile } from 'node:fs/promises' +import { SessionConfig, SessionDriverContract } from '../types.js' /** * File driver to read/write session to filesystem */ export class FileDriver implements SessionDriverContract { - constructor(private config: SessionConfig) { - if (!this.config.file || !this.config.file.location) { + #config: SessionConfig + + constructor(config: SessionConfig) { + this.#config = config + + if (!this.#config.file || !this.#config.file.location) { throw new Exception( 'Missing "file.location" for session file driver inside "config/session" file', - 500, - 'E_INVALID_SESSION_DRIVER_CONFIG' + { code: 'E_INVALID_SESSION_DRIVER_CONFIG', status: 500 } ) } } @@ -32,8 +34,8 @@ export class FileDriver implements SessionDriverContract { /** * Returns complete path to the session file */ - private getFilePath(sessionId: string): string { - return join(this.config.file!.location, `${sessionId}.txt`) + #getFilePath(sessionId: string): string { + return join(this.#config.file!.location, `${sessionId}.txt`) } /** @@ -41,7 +43,7 @@ export class FileDriver implements SessionDriverContract { * missing. */ public async read(sessionId: string): Promise<{ [key: string]: any } | null> { - const filePath = this.getFilePath(sessionId) + const filePath = this.#getFilePath(sessionId) await ensureFile(filePath) const contents = await readFile(filePath, 'utf-8') @@ -69,14 +71,14 @@ export class FileDriver implements SessionDriverContract { } const message = new MessageBuilder().build(values, undefined, sessionId) - await outputFile(this.getFilePath(sessionId), message) + await outputFile(this.#getFilePath(sessionId), message) } /** * Cleanup session file by removing it */ public async destroy(sessionId: string): Promise { - await remove(this.getFilePath(sessionId)) + await remove(this.#getFilePath(sessionId)) } /** diff --git a/src/Drivers/Memory.ts b/src/Drivers/Memory.ts index 2b8ee8f..ef04bdf 100644 --- a/src/Drivers/Memory.ts +++ b/src/Drivers/Memory.ts @@ -1,15 +1,13 @@ /** * @adonisjs/session * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -/// - -import { SessionDriverContract } from '@ioc:Adonis/Addons/Session' +import { SessionDriverContract } from '../types.js' /** * Memory driver is meant to be used for writing tests. diff --git a/src/Drivers/Redis.ts b/src/Drivers/Redis.ts index 98d4e57..9e4450f 100644 --- a/src/Drivers/Redis.ts +++ b/src/Drivers/Redis.ts @@ -1,36 +1,43 @@ /** * @adonisjs/session * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -/// - import { Exception } from '@poppinss/utils' -import { MessageBuilder, string } from '@poppinss/utils/build/helpers' -import { SessionDriverContract, SessionConfig } from '@ioc:Adonis/Addons/Session' -import { RedisManagerContract, RedisConnectionContract } from '@ioc:Adonis/Addons/Redis' +import string from '@poppinss/utils/string' +import { MessageBuilder } from '@poppinss/utils' +import type { RedisConnectionContract, RedisManagerContract } from '@adonisjs/redis/types' +import type { SessionDriverContract, SessionConfig } from '../types.js' /** * File driver to read/write session to filesystem */ export class RedisDriver implements SessionDriverContract { - /** - * Convert milliseconds to seconds - */ - private ttl: number = Math.round( - (typeof this.config.age === 'string' ? string.toMs(this.config.age) : this.config.age) / 1000 - ) + #config: SessionConfig + #redis: RedisManagerContract + #ttl: number + + constructor(config: SessionConfig, redis: RedisManagerContract) { + this.#config = config + this.#redis = redis + + /** + * Convert milliseconds to seconds + */ + this.#ttl = Math.round( + (typeof this.#config.age === 'string' + ? string.milliseconds.parse(this.#config.age) + : this.#config.age) / 1000 + ) - constructor(private config: SessionConfig, private redis: RedisManagerContract) { - if (!this.config.redisConnection) { + if (!this.#config.redisConnection) { throw new Exception( 'Missing redisConnection for session redis driver inside "config/session" file', - 500, - 'E_INVALID_SESSION_DRIVER_CONFIG' + { code: 'E_INVALID_SESSION_DRIVER_CONFIG', status: 500 } ) } } @@ -38,8 +45,8 @@ export class RedisDriver implements SessionDriverContract { /** * Returns instance of the redis connection */ - private getRedisConnection(): RedisConnectionContract { - return (this.redis.connection as any)(this.config.redisConnection) + #getRedisConnection(): RedisConnectionContract { + return (this.#redis.connection as any)(this.#config.redisConnection) } /** @@ -47,7 +54,7 @@ export class RedisDriver implements SessionDriverContract { * missing. */ public async read(sessionId: string): Promise<{ [key: string]: any } | null> { - const contents = await this.getRedisConnection().get(sessionId) + const contents = await this.#getRedisConnection().get(sessionId) if (!contents) { return null } @@ -68,9 +75,9 @@ export class RedisDriver implements SessionDriverContract { throw new Error('Session file driver expects an object of values') } - await this.getRedisConnection().setex( + await this.#getRedisConnection().setex( sessionId, - this.ttl, + this.#ttl, new MessageBuilder().build(values, undefined, sessionId) ) } @@ -79,13 +86,13 @@ export class RedisDriver implements SessionDriverContract { * Cleanup session file by removing it */ public async destroy(sessionId: string): Promise { - await this.getRedisConnection().del(sessionId) + await this.#getRedisConnection().del(sessionId) } /** * Updates the value expiry */ public async touch(sessionId: string): Promise { - await this.getRedisConnection().expire(sessionId, this.ttl) + await this.#getRedisConnection().expire(sessionId, this.#ttl) } } diff --git a/src/SessionManager/index.ts b/src/SessionManager/index.ts deleted file mode 100644 index 0dc4285..0000000 --- a/src/SessionManager/index.ts +++ /dev/null @@ -1,195 +0,0 @@ -/** - * @adonisjs/session - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { string } from '@poppinss/utils/build/helpers' -import { ApplicationContract } from '@ioc:Adonis/Core/Application' -import { Exception, ManagerConfigValidator } from '@poppinss/utils' -import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' - -import { - SessionConfig, - ExtendCallback, - SessionDriverContract, - SessionManagerContract, - SessionClientContract, -} from '@ioc:Adonis/Addons/Session' - -import { Session } from '../Session' - -type SessionManagerConfig = SessionConfig & { - cookie: { - expires: undefined - maxAge: number | undefined - } -} - -/** - * Session manager exposes the API to create session instance for a given - * request and also add new drivers. - */ -export class SessionManager implements SessionManagerContract { - /** - * A private map of drivers added from outside in. - */ - private extendedDrivers: Map = new Map() - - /** - * Reference to session config - */ - private config: SessionManagerConfig - - constructor(public application: ApplicationContract, config: SessionConfig) { - this.validateConfig(config) - this.processConfig(config) - } - - /** - * Validates the config - */ - private validateConfig(config: SessionConfig) { - const validator = new ManagerConfigValidator(config, 'session', 'config/session') - validator.validateDefault('driver') - } - - /** - * Processes the config and decides the `expires` option for the cookie - */ - private processConfig(config: SessionConfig): void { - /** - * Explicitly overwriting `cookie.expires` and `cookie.maxAge` from - * the user defined config - */ - const processedConfig: SessionManagerConfig = Object.assign({ enabled: true }, config, { - cookie: { - ...config.cookie, - expires: undefined, - maxAge: undefined, - }, - }) - - /** - * Set the max age when `clearWithBrowser = false`. Otherwise cookie - * is a session cookie - */ - if (!processedConfig.clearWithBrowser) { - const age = - typeof processedConfig.age === 'string' - ? Math.round(string.toMs(processedConfig.age) / 1000) - : processedConfig.age - - processedConfig.cookie.maxAge = age - } - - this.config = processedConfig - } - - /** - * Returns an instance of cookie driver - */ - private createCookieDriver(ctx: HttpContextContract): any { - const { CookieDriver } = require('../Drivers/Cookie') - return new CookieDriver(this.config, ctx) - } - - /** - * Returns an instance of the memory driver - */ - private createMemoryDriver(): any { - const { MemoryDriver } = require('../Drivers/Memory') - return new MemoryDriver() - } - - /** - * Returns an instance of file driver - */ - private createFileDriver(): any { - const { FileDriver } = require('../Drivers/File') - return new FileDriver(this.config) - } - - /** - * Returns an instance of redis driver - */ - private createRedisDriver(): any { - const { RedisDriver } = require('../Drivers/Redis') - - if (!this.application.container.hasBinding('Adonis/Addons/Redis')) { - throw new Error( - 'Install "@adonisjs/redis" in order to use the redis driver for storing sessions' - ) - } - - return new RedisDriver(this.config, this.application.container.use('Adonis/Addons/Redis')) - } - - /** - * Creates an instance of extended driver - */ - private createExtendedDriver(ctx: HttpContextContract): any { - if (!this.extendedDrivers.has(this.config.driver)) { - throw new Exception( - `"${this.config.driver}" is not a valid session driver`, - 500, - 'E_INVALID_SESSION_DRIVER' - ) - } - - return this.extendedDrivers.get(this.config.driver)!(this, this.config, ctx) - } - - /** - * Creates an instance of driver by looking at the config value `driver`. - * An hard exception is raised in case of invalid driver name - */ - private createDriver(ctx: HttpContextContract): SessionDriverContract { - switch (this.config.driver) { - case 'cookie': - return this.createCookieDriver(ctx) - case 'file': - return this.createFileDriver() - case 'redis': - return this.createRedisDriver() - case 'memory': - return this.createMemoryDriver() - default: - return this.createExtendedDriver(ctx) - } - } - - /** - * Find if the sessions are enabled - */ - public isEnabled() { - return this.config.enabled - } - - /** - * Creates an instance of the session client - */ - public client(): SessionClientContract { - const { SessionClient } = require('../Client') - const CookieClient = this.application.container.resolveBinding('Adonis/Core/CookieClient') - - return new SessionClient(this.config, this.createMemoryDriver(), CookieClient, {}) - } - - /** - * Creates a new session instance for a given HTTP request - */ - public create(ctx: HttpContextContract): Session { - return new Session(ctx, this.config, this.createDriver(ctx)) - } - - /** - * Extend the drivers list by adding a new one. - */ - public extend(driver: string, callback: ExtendCallback): void { - this.extendedDrivers.set(driver, callback) - } -} diff --git a/src/Bindings/Tests.ts b/src/bindings/api_client.ts similarity index 86% rename from src/Bindings/Tests.ts rename to src/bindings/api_client.ts index ed9cde2..719b8b4 100644 --- a/src/Bindings/Tests.ts +++ b/src/bindings/api_client.ts @@ -7,28 +7,19 @@ * file that was distributed with this source code. */ -/// +import { ApiRequest, ApiResponse, ApiClient } from '@japa/api-client' +import { InspectOptions, inspect } from 'util' +import { SessionManager } from '../session_manager.js' +import { AllowedSessionValues } from '../types.js' -import { ContainerBindings } from '@ioc:Adonis/Core/Application' -import { SessionManagerContract, AllowedSessionValues } from '@ioc:Adonis/Addons/Session' -import { inspect, InspectOptions } from 'util' - -/** - * Define test bindings - */ -export function defineTestsBindings( - ApiRequest: ContainerBindings['Japa/Preset/ApiRequest'], - ApiResponse: ContainerBindings['Japa/Preset/ApiResponse'], - ApiClient: ContainerBindings['Japa/Preset/ApiClient'], - SessionManager: SessionManagerContract -) { +export function extendApiClient(sessionManager: SessionManager) { /** * Set "sessionClient" on the api request */ ApiRequest.getter( 'sessionClient', function () { - return SessionManager.client() + return sessionManager.client() }, true ) @@ -42,6 +33,7 @@ export function defineTestsBindings( } this.sessionClient.merge(session) + return this }) @@ -141,7 +133,7 @@ export function defineTestsBindings( * cookie */ const { cookieName, sessionId } = await request.sessionClient.commit() - request.cookie(cookieName, sessionId) + request.withCookie(cookieName, sessionId) /** * Cleanup if request has error. Otherwise the teardown diff --git a/src/bindings/http_context.ts b/src/bindings/http_context.ts new file mode 100644 index 0000000..bb59691 --- /dev/null +++ b/src/bindings/http_context.ts @@ -0,0 +1,24 @@ +/* + * @adonisjs/session + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { HttpContext } from '@adonisjs/core/http' +import { SessionManager } from '../session_manager.js' + +/** + * Extends HttpContext class with the ally getter + */ +export function extendHttpContext(session: SessionManager) { + HttpContext.getter( + 'session', + function (this: HttpContext) { + return session.create(this) + }, + true + ) +} diff --git a/adonis-typings/tests.ts b/src/bindings/types.ts similarity index 72% rename from adonis-typings/tests.ts rename to src/bindings/types.ts index 33e5697..3d84e7e 100644 --- a/adonis-typings/tests.ts +++ b/src/bindings/types.ts @@ -7,13 +7,36 @@ * file that was distributed with this source code. */ -import '@japa/api-client' -import { InspectOptions } from 'util' -import { AllowedSessionValues, SessionClientContract } from '@ioc:Adonis/Addons/Session' +import { InspectOptions } from 'node:util' +import type { SessionClient } from '../client.js' +import type { Session } from '../session.js' +import type { SessionManager } from '../session_manager.js' +import type { AllowedSessionValues } from '../types.js' +/** + * HttpContext augmentations + */ +declare module '@adonisjs/core/http' { + interface HttpContext { + session: Session + } +} + +/** + * Container augmentations + */ +declare module '@adonisjs/core/types' { + interface ContainerBindings { + session: SessionManager + } +} + +/** + * Japa api client augmentations + */ declare module '@japa/api-client' { export interface ApiRequest { - sessionClient: SessionClientContract + sessionClient: SessionClient /** * Send session values in the request diff --git a/src/Client/index.ts b/src/client.ts similarity index 55% rename from src/Client/index.ts rename to src/client.ts index db90c4c..b0cfb97 100644 --- a/src/Client/index.ts +++ b/src/client.ts @@ -1,38 +1,46 @@ /* * @adonisjs/session * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -/// - -import { cuid } from '@poppinss/utils/build/helpers' -import { - SessionConfig, - SessionDriverContract, - SessionClientContract, -} from '@ioc:Adonis/Addons/Session' - -import { CookieClientContract } from '@ioc:Adonis/Core/CookieClient' -import { Store } from '../Store' +import { cuid } from '@adonisjs/core/helpers' +import { Store } from './store.js' +import type { SessionConfig, SessionDriverContract } from './types.js' +import type { CookieClient } from '@adonisjs/core/http' /** * SessionClient exposes the API to set session data as a client */ -export class SessionClient extends Store implements SessionClientContract { +export class SessionClient extends Store { + /** + * Session configuration + */ + #config: SessionConfig + + /** + * The session driver used to read and write session data + */ + #driver: SessionDriverContract + + /** + * Cookie client contract to sign and unsign cookies + */ + #cookieClient: CookieClient + /** * Each instance of client works on a single session id. Generate * multiple client instances for a different session id */ - private sessionId = cuid() + sessionId = cuid() /** * Session key for setting flash messages */ - private flashMessagesKey = '__flash__' + #flashMessagesKey = '__flash__' /** * Flash messages store. They are merged with the session data during @@ -41,31 +49,35 @@ export class SessionClient extends Store implements SessionClientContract { public flashMessages = new Store({}) constructor( - private config: SessionConfig, - private driver: SessionDriverContract, - private cookieClient: CookieClientContract, + config: SessionConfig, + driver: SessionDriverContract, + cookieClient: CookieClient, values: { [key: string]: any } | null ) { super(values) + + this.#config = config + this.#driver = driver + this.#cookieClient = cookieClient } /** * Find if the sessions are enabled */ public isEnabled() { - return this.config.enabled + return this.#config.enabled } /** * Load session from the driver */ public async load(cookies: Record) { - const sessionIdCookie = cookies[this.config.cookieName] + const sessionIdCookie = cookies[this.#config.cookieName] const sessionId = sessionIdCookie ? sessionIdCookie.value : this.sessionId - const contents = await this.driver.read(sessionId) + const contents = await this.#driver.read(sessionId) const store = new Store(contents) - const flashMessages = store.pull(this.flashMessagesKey, null) + const flashMessages = store.pull(this.#flashMessagesKey, null) return { session: store.all(), @@ -79,8 +91,8 @@ export class SessionClient extends Store implements SessionClientContract { * by the server */ public async commit() { - this.set(this.flashMessagesKey, this.flashMessages.all()) - await this.driver.write(this.sessionId, this.toJSON()) + this.set(this.#flashMessagesKey, this.flashMessages.all()) + await this.#driver.write(this.sessionId, this.toJSON()) /** * Clear from the session client memory @@ -90,8 +102,8 @@ export class SessionClient extends Store implements SessionClientContract { return { sessionId: this.sessionId!, - signedSessionId: this.cookieClient.sign(this.config.cookieName, this.sessionId)!, - cookieName: this.config.cookieName, + signedSessionId: this.#cookieClient.sign(this.#config.cookieName, this.sessionId)!, + cookieName: this.#config.cookieName, } } @@ -108,6 +120,6 @@ export class SessionClient extends Store implements SessionClientContract { /** * Clear with the driver */ - await this.driver.destroy(this.sessionId) + await this.#driver.destroy(this.sessionId) } } diff --git a/src/define_config.ts b/src/define_config.ts new file mode 100644 index 0000000..ed0cdde --- /dev/null +++ b/src/define_config.ts @@ -0,0 +1,17 @@ +import { InvalidArgumentsException } from '@poppinss/utils' +import { SessionConfig } from './types.js' + +/** + * Helper to define session config + */ +export function defineConfig(config: SessionConfig) { + if (!config.cookieName) { + throw new InvalidArgumentsException('Missing "cookieName" property inside the session config') + } + + if (!config.driver) { + throw new InvalidArgumentsException('Missing "driver" property inside the session config') + } + + return config +} diff --git a/src/Session/index.ts b/src/session.ts similarity index 57% rename from src/Session/index.ts rename to src/session.ts index b3a4224..948c5b4 100644 --- a/src/Session/index.ts +++ b/src/session.ts @@ -1,32 +1,63 @@ /* * @adonisjs/session * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -/// +import type { SessionConfig, SessionDriverContract, AllowedSessionValues } from './types.js' +import type { HttpContext } from '@adonisjs/core/http' +import { Exception } from '@poppinss/utils' +import lodash from '@poppinss/utils/lodash' +import { cuid } from '@adonisjs/core/helpers' -import { Exception, lodash } from '@poppinss/utils' -import { cuid } from '@poppinss/utils/build/helpers' -import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' - -import { - SessionConfig, - SessionContract, - AllowedSessionValues, - SessionDriverContract, -} from '@ioc:Adonis/Addons/Session' - -import { Store } from '../Store' +import { Store } from './store.js' /** * Session class exposes the API to read/write values to the session for * a given request. */ -export class Session implements SessionContract { +export class Session { + /** + * Session id for the current request. It will be different + * from the "this.sessionId" when regenerate is called. + */ + #currentSessionId: string + + /** + * A instance of store with values read from the driver. The store + * in initiated inside the [[initiate]] method + */ + #store!: Store + + /** + * Whether or not to re-generate the session id before committing + * session values. + */ + #regeneratedSessionId = false + + /** + * Session key for setting flash messages + */ + #flashMessagesKey = '__flash__' + + /** + * The HTTP context for the current request. + */ + #ctx: HttpContext + + /** + * Configuration for the session + */ + #config: SessionConfig + + /** + * The session driver instance used to read and write session data. + */ + #driver: SessionDriverContract + /** * Set to true inside the `initiate` method */ @@ -49,31 +80,13 @@ export class Session implements SessionContract { * Session id for the given request. A new session id is only * generated when the cookie for the session id is missing */ - public sessionId = this.getSessionId() + public sessionId: string /** * A copy of previously set flash messages */ public flashMessages = new Store({}) - /** - * Session id for the current request. It will be different - * from the "this.sessionId" when regenerate is called. - */ - private currentSessionId = this.sessionId - - /** - * A instance of store with values read from the driver. The store - * in initiated inside the [[initiate]] method - */ - private store: Store - - /** - * Whether or not to re-generate the session id before comitting - * session values. - */ - private regeneratedSessionId = false - /** * A copy of flash messages. The `input` messages * are overwritten when any of the input related @@ -83,54 +96,49 @@ export class Session implements SessionContract { */ public responseFlashMessages = new Store({}) - /** - * Session key for setting flash messages - */ - private flashMessagesKey = '__flash__' + constructor(ctx: HttpContext, config: SessionConfig, driver: SessionDriverContract) { + this.#ctx = ctx + this.#config = config + this.#driver = driver - constructor( - private ctx: HttpContextContract, - private config: SessionConfig, - private driver: SessionDriverContract - ) {} + this.sessionId = this.#getSessionId() + this.#currentSessionId = this.sessionId + } /** * Returns a merged copy of flash messages or null * when nothing is set */ - private setFlashMessages(): void { + #setFlashMessages(): void { if (this.responseFlashMessages.isEmpty) { return } const { input, ...others } = this.responseFlashMessages.all() - this.put(this.flashMessagesKey, { ...input, ...others }) + this.put(this.#flashMessagesKey, { ...input, ...others }) } /** * Returns the existing session id or creates one. */ - private getSessionId(): string { - const sessionId = this.ctx.request.cookie(this.config.cookieName) + #getSessionId(): string { + const sessionId = this.#ctx.request.cookie(this.#config.cookieName) if (sessionId) { - this.ctx.logger.trace('existing session found') return sessionId } this.fresh = true - this.ctx.logger.trace('generating new session id') return cuid() } /** * Ensures the session store is initialized */ - private ensureIsReady(): void { + #ensureIsReady(): void { if (!this.initiated) { throw new Exception( 'Session store is not initiated yet. Make sure you are using the session hook', - 500, - 'E_RUNTIME_EXCEPTION' + { code: 'E_RUNTIME_EXCEPTION', status: 500 } ) } } @@ -138,62 +146,61 @@ export class Session implements SessionContract { /** * Raises exception when session store is in readonly mode */ - private ensureIsMutable() { + #ensureIsMutable() { if (this.readonly) { - throw new Exception( - 'Session store is in readonly mode and cannot be mutated', - 500, - 'E_RUNTIME_EXCEPTION' - ) + throw new Exception('Session store is in readonly mode and cannot be mutated', { + status: 500, + code: 'E_RUNTIME_EXCEPTION', + }) } } /** * Touches the session cookie */ - private touchSessionCookie(): void { - this.ctx.logger.trace('touching session cookie') - this.ctx.response.cookie(this.config.cookieName, this.sessionId, this.config.cookie!) + #touchSessionCookie(): void { + this.#ctx.logger.trace('touching session cookie') + this.#ctx.response.cookie(this.#config.cookieName, this.sessionId, this.#config.cookie!) } /** * Commits the session value to the store */ - private async commitValuesToStore(): Promise { - this.ctx.logger.trace('persist session store with driver') - await this.driver.write(this.sessionId, this.store.toJSON()) + async #commitValuesToStore(): Promise { + this.#ctx.logger.trace('persist session store with driver') + await this.#driver.write(this.sessionId, this.#store.toJSON()) } /** * Touches the driver to make sure the session values doesn't expire */ - private async touchDriver(): Promise { - this.ctx.logger.trace('touch driver for liveliness') - await this.driver.touch(this.sessionId) + async #touchDriver(): Promise { + this.#ctx.logger.trace('touch driver for liveliness') + await this.#driver.touch(this.sessionId) } /** * Reading flash messages from the last HTTP request and * updating the flash messages bag */ - private readLastRequestFlashMessage() { + #readLastRequestFlashMessage() { if (this.readonly) { return } - this.flashMessages.update(this.pull(this.flashMessagesKey, null)) + this.flashMessages.update(this.pull(this.#flashMessagesKey, null)) } /** * Share flash messages & read only session's functions with views * (only when view property exists) */ - private shareLocalsWithView() { - if (!this.ctx['view'] || typeof this.ctx['view'].share !== 'function') { + #shareLocalsWithView() { + if (!this.#ctx['view'] || typeof this.#ctx['view'].share !== 'function') { return } - this.ctx['view'].share({ + this.#ctx['view'].share({ flashMessages: this.flashMessages, session: { get: this.get.bind(this), @@ -216,21 +223,12 @@ export class Session implements SessionContract { this.readonly = readonly - /** - * Profiling the driver read method - */ - await this.ctx.profiler.profileAsync( - 'session:initiate', - { driver: this.config.driver }, - async () => { - const contents = await this.driver.read(this.sessionId) - this.store = new Store(contents) - } - ) + const contents = await this.#driver.read(this.sessionId) + this.#store = new Store(contents) this.initiated = true - this.readLastRequestFlashMessage() - this.shareLocalsWithView() + this.#readLastRequestFlashMessage() + this.#shareLocalsWithView() } /** @@ -238,26 +236,26 @@ export class Session implements SessionContract { * session fixation attacks. */ public regenerate(): void { - this.ctx.logger.trace('explicitly re-generating session id') + this.#ctx.logger.trace('explicitly re-generating session id') this.sessionId = cuid() - this.regeneratedSessionId = true + this.#regeneratedSessionId = true } /** * Set/update session value */ public put(key: string, value: AllowedSessionValues): void { - this.ensureIsReady() - this.ensureIsMutable() - this.store.set(key, value) + this.#ensureIsReady() + this.#ensureIsMutable() + this.#store.set(key, value) } /** * Find if the value exists in the session */ public has(key: string): boolean { - this.ensureIsReady() - return this.store.has(key) + this.#ensureIsReady() + return this.#store.has(key) } /** @@ -265,25 +263,25 @@ export class Session implements SessionContract { * when actual value is `undefined` */ public get(key: string, defaultValue?: any): any { - this.ensureIsReady() - return this.store.get(key, defaultValue) + this.#ensureIsReady() + return this.#store.get(key, defaultValue) } /** * Returns everything from the session */ public all(): any { - this.ensureIsReady() - return this.store.all() + this.#ensureIsReady() + return this.#store.all() } /** * Remove value for a given key from the session */ public forget(key: string): void { - this.ensureIsReady() - this.ensureIsMutable() - this.store.unset(key) + this.#ensureIsReady() + this.#ensureIsMutable() + this.#store.unset(key) } /** @@ -291,9 +289,9 @@ export class Session implements SessionContract { * by `session.forget` */ public pull(key: string, defaultValue?: any): any { - this.ensureIsReady() - this.ensureIsMutable() - return this.store.pull(key, defaultValue) + this.#ensureIsReady() + this.#ensureIsMutable() + return this.#store.pull(key, defaultValue) } /** @@ -302,9 +300,9 @@ export class Session implements SessionContract { * a number */ public increment(key: string, steps: number = 1): void { - this.ensureIsReady() - this.ensureIsMutable() - this.store.increment(key, steps) + this.#ensureIsReady() + this.#ensureIsMutable() + this.#store.increment(key, steps) } /** @@ -313,18 +311,18 @@ export class Session implements SessionContract { * a number */ public decrement(key: string, steps: number = 1): void { - this.ensureIsReady() - this.ensureIsMutable() - this.store.decrement(key, steps) + this.#ensureIsReady() + this.#ensureIsMutable() + this.#store.decrement(key, steps) } /** * Remove everything from the session */ public clear(): void { - this.ensureIsReady() - this.ensureIsMutable() - this.store.clear() + this.#ensureIsReady() + this.#ensureIsMutable() + this.#store.clear() } /** @@ -334,8 +332,8 @@ export class Session implements SessionContract { key: string | { [key: string]: AllowedSessionValues }, value?: AllowedSessionValues ): void { - this.ensureIsReady() - this.ensureIsMutable() + this.#ensureIsReady() + this.#ensureIsMutable() /** * Update value @@ -353,27 +351,27 @@ export class Session implements SessionContract { * Flash all form values */ public flashAll(): void { - this.ensureIsReady() - this.ensureIsMutable() - this.responseFlashMessages.set('input', this.ctx.request.original()) + this.#ensureIsReady() + this.#ensureIsMutable() + this.responseFlashMessages.set('input', this.#ctx.request.original()) } /** * Flash all form values except mentioned keys */ public flashExcept(keys: string[]): void { - this.ensureIsReady() - this.ensureIsMutable() - this.responseFlashMessages.set('input', lodash.omit(this.ctx.request.original(), keys)) + this.#ensureIsReady() + this.#ensureIsMutable() + this.responseFlashMessages.set('input', lodash.omit(this.#ctx.request.original(), keys)) } /** * Flash only defined keys from the form values */ public flashOnly(keys: string[]): void { - this.ensureIsReady() - this.ensureIsMutable() - this.responseFlashMessages.set('input', lodash.pick(this.ctx.request.original(), keys)) + this.#ensureIsReady() + this.#ensureIsMutable() + this.responseFlashMessages.set('input', lodash.pick(this.#ctx.request.original(), keys)) } /** @@ -402,30 +400,25 @@ export class Session implements SessionContract { * Writes value to the underlying session driver. */ public async commit(): Promise { - await this.ctx.profiler.profileAsync( - 'session:commit', - { driver: this.config.driver }, - async () => { - if (!this.initiated) { - this.touchSessionCookie() - await this.touchDriver() - return - } - - /** - * Cleanup old session and re-generate new session - */ - if (this.regeneratedSessionId) { - await this.driver.destroy(this.currentSessionId) - } - - /** - * Touch the session cookie to keep it alive. - */ - this.touchSessionCookie() - this.setFlashMessages() - await this.commitValuesToStore() - } - ) + if (!this.initiated) { + console.log('session not initiated') + this.#touchSessionCookie() + await this.#touchDriver() + return + } + + /** + * Cleanup old session and re-generate new session + */ + if (this.#regeneratedSessionId) { + await this.#driver.destroy(this.#currentSessionId) + } + + /** + * Touch the session cookie to keep it alive. + */ + this.#touchSessionCookie() + this.#setFlashMessages() + await this.#commitValuesToStore() } } diff --git a/src/session_manager.ts b/src/session_manager.ts new file mode 100644 index 0000000..3cdc549 --- /dev/null +++ b/src/session_manager.ts @@ -0,0 +1,187 @@ +/** + * @adonisjs/session + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import string from '@poppinss/utils/string' +import { Exception } from '@poppinss/utils' + +import { Session } from './session.js' +import { CookieClient, HttpContext } from '@adonisjs/core/http' +import { CookieDriver } from './drivers/cookie.js' +import { MemoryDriver } from './drivers/memory.js' +import { FileDriver } from './drivers/file.js' +import { RedisDriver } from './drivers/redis.js' +import { ExtendCallback, SessionConfig, SessionDriverContract } from './types.js' +import { RedisManagerContract } from '@adonisjs/redis/types' +import { Encryption } from '@adonisjs/core/encryption' +import { SessionClient } from './client.js' + +type SessionManagerConfig = SessionConfig & { + cookie: { + expires: undefined + maxAge: number | undefined + } +} + +/** + * Session manager exposes the API to create session instance for a given + * request and also add new drivers. + */ +export class SessionManager { + /** + * A private map of drivers added from outside in. + */ + #extendedDrivers: Map = new Map() + + /** + * Reference to session config + */ + #config!: SessionManagerConfig + + /** + * Reference to the encryption instance + */ + #encryption: Encryption + + /** + * Reference to the redis manager + */ + #redis?: RedisManagerContract + + constructor(config: SessionConfig, encryption: Encryption, redis?: RedisManagerContract) { + this.#encryption = encryption + this.#redis = redis + + this.#processConfig(config) + } + + /** + * Processes the config and decides the `expires` option for the cookie + */ + #processConfig(config: SessionConfig): void { + /** + * Explicitly overwriting `cookie.expires` and `cookie.maxAge` from + * the user defined config + */ + const processedConfig: SessionManagerConfig = Object.assign({ enabled: true }, config, { + cookie: { + ...config.cookie, + expires: undefined, + maxAge: undefined, + }, + }) + + /** + * Set the max age when `clearWithBrowser = false`. Otherwise cookie + * is a session cookie + */ + if (!processedConfig.clearWithBrowser) { + const age = + typeof processedConfig.age === 'string' + ? Math.round(string.milliseconds.parse(processedConfig.age) / 1000) + : processedConfig.age + + processedConfig.cookie.maxAge = age + } + + this.#config = processedConfig + } + + /** + * Returns an instance of cookie driver + */ + #createCookieDriver(ctx: HttpContext) { + return new CookieDriver(this.#config, ctx) + } + + /** + * Returns an instance of the memory driver + */ + #createMemoryDriver() { + return new MemoryDriver() + } + + /** + * Returns an instance of file driver + */ + #createFileDriver() { + return new FileDriver(this.#config) + } + + /** + * Returns an instance of redis driver + */ + #createRedisDriver() { + if (!this.#redis) { + throw new Error( + 'Install "@adonisjs/redis" in order to use the redis driver for storing sessions' + ) + } + + return new RedisDriver(this.#config, this.#redis) + } + + /** + * Creates an instance of extended driver + */ + #createExtendedDriver(ctx: HttpContext): any { + if (!this.#extendedDrivers.has(this.#config.driver)) { + throw new Exception(`"${this.#config.driver}" is not a valid session driver`, { + code: 'E_INVALID_SESSION_DRIVER', + status: 500, + }) + } + + return this.#extendedDrivers.get(this.#config.driver)!(this, this.#config, ctx) + } + + #createDriver(ctx: HttpContext): SessionDriverContract { + switch (this.#config.driver) { + case 'cookie': + return this.#createCookieDriver(ctx) + case 'file': + return this.#createFileDriver() + case 'redis': + return this.#createRedisDriver() + case 'memory': + return this.#createMemoryDriver() + default: + return this.#createExtendedDriver(ctx) + } + } + + /** + * Find if the sessions are enabled + */ + public isEnabled() { + return this.#config.enabled + } + + /** + * Creates an instance of the session client + */ + public client() { + const cookieClient = new CookieClient(this.#encryption) + + return new SessionClient(this.#config, this.#createMemoryDriver(), cookieClient, {}) + } + + /** + * Creates a new session instance for a given HTTP request + */ + public create(ctx: HttpContext) { + return new Session(ctx, this.#config, this.#createDriver(ctx)) + } + + /** + * Extend the drivers list by adding a new one. + */ + public extend(driver: string, callback: ExtendCallback): void { + this.#extendedDrivers.set(driver, callback) + } +} diff --git a/src/session_middleware.ts b/src/session_middleware.ts new file mode 100644 index 0000000..2d5abed --- /dev/null +++ b/src/session_middleware.ts @@ -0,0 +1,27 @@ +import type { HttpContext } from "@adonisjs/core/http"; +import type { NextFn } from "@adonisjs/core/types/http"; +import { SessionManager } from "./session_manager.js"; + +export default class SessionMiddleware { + async handle(ctx: HttpContext, next: NextFn) { + const sessionManager = await ctx.containerResolver.make('session') as SessionManager + if (!sessionManager.isEnabled()) { + return + } + + /** + * Initiate session store + */ + await ctx.session.initiate(false) + + /** + * Call next middlewares or route handler + */ + await next() + + /** + * Commit store mutations + */ + await ctx.session.commit() + } +} diff --git a/src/Store/index.ts b/src/store.ts similarity index 81% rename from src/Store/index.ts rename to src/store.ts index 8c9941a..8d4c6ff 100644 --- a/src/Store/index.ts +++ b/src/store.ts @@ -1,56 +1,56 @@ /* * @adonisjs/redis * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -/// +import { Exception } from '@poppinss/utils' +import lodash from '@poppinss/utils/lodash' -import { Exception, lodash } from '@poppinss/utils' -import { AllowedSessionValues, StoreContract } from '@ioc:Adonis/Addons/Session' +import type { AllowedSessionValues } from './types.js' /** * Session store to mutate and access values from the session object */ -export class Store implements StoreContract { +export class Store { /** * Underlying store values */ - private values: { [key: string]: any } + #values: { [key: string]: any } constructor(values: { [key: string]: any } | null) { - this.values = values || {} + this.#values = values || {} } /** * Find if store is empty or not */ public get isEmpty() { - return !this.values || Object.keys(this.values).length === 0 + return !this.#values || Object.keys(this.#values).length === 0 } /** * Set key/value pair */ public set(key: string, value: AllowedSessionValues): void { - lodash.set(this.values, key, value) + lodash.set(this.#values, key, value) } /** * Get value for a given key */ public get(key: string, defaultValue?: any): any { - return lodash.get(this.values, key, defaultValue) + return lodash.get(this.#values, key, defaultValue) } /** * Remove key */ public unset(key: string): void { - lodash.unset(this.values, key) + lodash.unset(this.#values, key) } /** @@ -101,14 +101,14 @@ export class Store implements StoreContract { * Overwrite the underlying values object */ public update(values: { [key: string]: any }): void { - this.values = values + this.#values = values } /** * Update to merge values */ public merge(values: { [key: string]: any }): any { - lodash.merge(this.values, values) + lodash.merge(this.#values, values) } /** @@ -128,7 +128,7 @@ export class Store implements StoreContract { * Get all values */ public all(): any { - return this.values + return this.#values } /** diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..2b978ba --- /dev/null +++ b/src/types.ts @@ -0,0 +1,76 @@ +import type { CookieOptions } from '@adonisjs/core/types/http' +import type { SessionManager } from './session_manager.js' +import type { HttpContext } from '@adonisjs/core/http' + +/** + * The callback to be passed to the `extend` method. It is invoked + * for each request (if extended driver is in use). + */ +export type ExtendCallback = ( + manager: SessionManager, + config: SessionConfig, + ctx: HttpContext +) => SessionDriverContract + +/** + * Shape of a driver that every session driver must have + */ +export interface SessionDriverContract { + read(sessionId: string): Promise | null> | Record | null + write(sessionId: string, values: Record): Promise | void + destroy(sessionId: string): Promise | void + touch(sessionId: string): Promise | void +} + +/** + * Shape of session config. + */ +export interface SessionConfig { + /** + * Enable/disable session for the entire application lifecycle + */ + enabled: boolean + + /** + * The driver in play + */ + driver: string + + /** + * Cookie name. + */ + cookieName: string + + /** + * Clear session when browser closes + */ + clearWithBrowser: boolean + + /** + * Age of session cookie + */ + age: string | number + + /** + * Config for the cookie driver and also the session id + * cookie + */ + cookie: Omit, 'maxAge' | 'expires'> + + /** + * Config for the file driver + */ + file?: { + location: string + } + + /** + * The redis connection to use from the `config/redis` file + */ + redisConnection?: string +} + +/** + * The values allowed by the `session.put` method + */ +export type AllowedSessionValues = string | boolean | number | object | Date | Array diff --git a/test-helpers/index.ts b/test-helpers/index.ts deleted file mode 100644 index 6cdb1c1..0000000 --- a/test-helpers/index.ts +++ /dev/null @@ -1,135 +0,0 @@ -/* - * @adonisjs/session - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { join } from 'path' -import { Filesystem } from '@poppinss/dev-utils' -import { SessionConfig } from '@ioc:Adonis/Addons/Session' -import { Application } from '@adonisjs/core/build/standalone' -import { RedisManagerContract } from '@ioc:Adonis/Addons/Redis' -import { ApplicationContract } from '@ioc:Adonis/Core/Application' -import { RedisManager } from '@adonisjs/redis/build/src/RedisManager/index.js' - -export const fs = new Filesystem(join(__dirname, 'app')) - -/** - * Session default config - */ -export const sessionConfig: SessionConfig = { - enabled: true, - driver: 'cookie', - cookieName: 'adonis-session', - clearWithBrowser: false, - age: 3000, - cookie: { - path: '/', - }, -} - -export async function setup(config?: any) { - await fs.add('.env', '') - await fs.add( - 'config/app.ts', - ` - export const appKey = '${Math.random().toFixed(36).substring(2, 38)}', - export const http = { - cookie: {}, - trustProxy: () => true, - } - ` - ) - - await fs.add( - 'config/session.ts', - ` - const sessionConfig = ${JSON.stringify(config || sessionConfig, null, 2)} - export default sessionConfig - ` - ) - - const app = new Application(fs.basePath, 'web', { - providers: [ - '@adonisjs/core', - '../../providers/SessionProvider', - '@japa/preset-adonis/TestsProvider', - ], - }) - - await app.setup() - await app.registerProviders() - await app.bootProviders() - - return app -} - -/** - * Sleep for a while - */ -export function sleep(time: number): Promise { - return new Promise((resolve) => setTimeout(resolve, time)) -} - -/** - * Signs value to be set as cookie header - */ -export function signCookie(app: ApplicationContract, value: any, name: string) { - const encryption = app.container.use('Adonis/Core/Encryption') - return `${name}=s:${encryption.verifier.sign(value, undefined, name)}` -} - -/** - * Encrypt value to be set as cookie header - */ -export function encryptCookie(app: ApplicationContract, value: any, name: string) { - const encryption = app.container.use('Adonis/Core/Encryption') - return `${name}=e:${encryption.encrypt(value, undefined, name)}` -} - -/** - * Decrypt cookie - */ -export function decryptCookie(app: ApplicationContract, header: any, name: string) { - const encryption = app.container.use('Adonis/Core/Encryption') - const cookieValue = decodeURIComponent(header['set-cookie'][0].split(';')[0]) - .replace(`${name}=`, '') - .slice(2) - - return encryption.decrypt(cookieValue, name) -} - -/** - * Unsign cookie - */ -export function unsignCookie(app: ApplicationContract, header: any, name: string) { - const encryption = app.container.use('Adonis/Core/Encryption') - - const cookieValue = decodeURIComponent(header['set-cookie'][0].split(';')[0]) - .replace(`${name}=`, '') - .slice(2) - - return encryption.verifier.unsign(cookieValue, name) -} - -/** - * Reference to the redis manager - */ -export function getRedisManager(application: ApplicationContract) { - return new RedisManager( - application, - { - connection: 'session', - connections: { - session: { - host: process.env.REDIS_HOST || '0.0.0.0', - port: process.env.REDIS_PORT || 6379, - }, - }, - } as any, - application.container.use('Adonis/Core/Event') - ) as unknown as RedisManagerContract -} diff --git a/test/cookie-driver.spec.ts b/test/cookie_driver.spec.ts similarity index 56% rename from test/cookie-driver.spec.ts rename to test/cookie_driver.spec.ts index 10c0b28..4989664 100644 --- a/test/cookie-driver.spec.ts +++ b/test/cookie_driver.spec.ts @@ -1,34 +1,35 @@ /* * @adonisjs/session * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -/// - import { test } from '@japa/runner' import supertest from 'supertest' import { createServer } from 'http' -import { CookieDriver } from '../src/Drivers/Cookie' -import { setup, fs, encryptCookie, decryptCookie, sessionConfig } from '../test-helpers' - -test.group('Cookie driver', (group) => { - group.each.teardown(async () => { - await fs.cleanup() - }) - - test('return null object when cookie is missing', async ({ assert }) => { +import { CookieDriver } from '../src/drivers/cookie.js' +import { + setup, + sessionConfig, + encryptCookie, + decryptCookie, + createHttpContext, +} from '../test_helpers/index.js' + +test.group('Cookie driver', () => { + test('return null object when cookie is missing', async ({ fs, assert }) => { assert.plan(1) - const app = await setup() + const { app } = await setup(fs) const sessionId = '1234' const server = createServer(async (req, res) => { - const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res) + const ctx = await createHttpContext(app, req, res) + const session = new CookieDriver(sessionConfig, ctx) const value = session.read(sessionId) assert.isNull(value) @@ -38,14 +39,15 @@ test.group('Cookie driver', (group) => { await supertest(server).get('/') }) - test('return empty object when cookie value is invalid', async ({ assert }) => { + test('return empty object when cookie value is invalid', async ({ fs, assert }) => { assert.plan(1) - const app = await setup() + const { app } = await setup(fs) const sessionId = '1234' const server = createServer(async (req, res) => { - const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res) + const ctx = await createHttpContext(app, req, res) + const session = new CookieDriver(sessionConfig, ctx) const value = session.read(sessionId) assert.isNull(value) @@ -55,12 +57,12 @@ test.group('Cookie driver', (group) => { await supertest(server).get('/').set('cookie', '1234=hello-world') }) - test('return cookie values as an object', async ({ assert }) => { - const app = await setup() + test('return cookie values as an object', async ({ fs, assert }) => { + const { app } = await setup(fs) const sessionId = '1234' const server = createServer(async (req, res) => { - const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res) + const ctx = await createHttpContext(app, req, res) const session = new CookieDriver(sessionConfig, ctx) const value = session.read(sessionId) @@ -72,17 +74,17 @@ test.group('Cookie driver', (group) => { const { body } = await supertest(server) .get('/') - .set('cookie', encryptCookie(app, { message: 'hello-world' }, sessionId)) + .set('cookie', await encryptCookie(app, { message: 'hello-world' }, sessionId)) assert.deepEqual(body, { message: 'hello-world' }) }) - test('write cookie value', async ({ assert }) => { - const app = await setup() + test('write cookie value', async ({ fs, assert }) => { + const { app } = await setup(fs) const sessionId = '1234' const server = createServer(async (req, res) => { - const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res) + const ctx = await createHttpContext(app, req, res) const session = new CookieDriver(sessionConfig, ctx) session.write(sessionId, { message: 'hello-world' }) @@ -92,15 +94,15 @@ test.group('Cookie driver', (group) => { }) const { header } = await supertest(server).get('/') - assert.deepEqual(decryptCookie(app, header, sessionId), { message: 'hello-world' }) + assert.deepEqual(await decryptCookie(app, header, sessionId), { message: 'hello-world' }) }) - test('update cookie with existing value', async ({ assert }) => { - const app = await setup() + test('update cookie with existing value', async ({ fs, assert }) => { + const { app } = await setup(fs) const sessionId = '1234' const server = createServer(async (req, res) => { - const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res) + const ctx = await createHttpContext(app, req, res) const session = new CookieDriver(sessionConfig, ctx) session.touch(sessionId) @@ -111,8 +113,8 @@ test.group('Cookie driver', (group) => { const { header } = await supertest(server) .get('/') - .set('cookie', encryptCookie(app, { message: 'hello-world' }, sessionId)) + .set('cookie', await encryptCookie(app, { message: 'hello-world' }, sessionId)) - assert.deepEqual(decryptCookie(app, header, sessionId), { message: 'hello-world' }) + assert.deepEqual(await decryptCookie(app, header, sessionId), { message: 'hello-world' }) }) }) diff --git a/test/define_config.spec.ts b/test/define_config.spec.ts new file mode 100644 index 0000000..d717f0b --- /dev/null +++ b/test/define_config.spec.ts @@ -0,0 +1,18 @@ +import { test } from '@japa/runner' +import { defineConfig } from '../src/define_config.js' + +test.group('Define config', () => { + test('should throws if no driver defined', ({ assert }) => { + assert.throws( + () => defineConfig({ cookieName: 'hey' } as any), + 'Missing "driver" property inside the session config' + ) + }) + + test('should throws if no cookieName defined', ({ assert }) => { + assert.throws( + () => defineConfig({ driver: 'cookie' } as any), + 'Missing "cookieName" property inside the session config' + ) + }) +}) diff --git a/test/file-driver.spec.ts b/test/file_driver.spec.ts similarity index 64% rename from test/file-driver.spec.ts rename to test/file_driver.spec.ts index 9804b89..0a9b86a 100644 --- a/test/file-driver.spec.ts +++ b/test/file_driver.spec.ts @@ -1,32 +1,31 @@ /* * @adonisjs/session * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -/// - import { test } from '@japa/runner' import { join } from 'path' -import { Filesystem } from '@poppinss/dev-utils' - -import { FileDriver } from '../src/Drivers/File' -import { sleep, sessionConfig } from '../test-helpers' +import { FileDriver } from '../src/drivers/file.js' +import { sleep, sessionConfig, BASE_URL } from '../test_helpers/index.js' +import { fileURLToPath } from 'url' -const fs = new Filesystem() const config = Object.assign({}, sessionConfig, { driver: 'file', - file: { - location: fs.basePath, - }, + file: { location: fileURLToPath(BASE_URL) }, }) -test.group('File driver', (group) => { - group.each.teardown(async () => { - await fs.cleanup() +test.group('File driver', () => { + test('throws if location is missing', ({ assert }) => { + // @ts-ignore + const session = () => new FileDriver({ driver: 'file', file: {} }) + assert.throws( + session, + 'Missing "file.location" for session file driver inside "config/session" file' + ) }) test('return null when file is missing', async ({ assert }) => { @@ -41,11 +40,10 @@ test.group('File driver', (group) => { const session = new FileDriver(config) await session.write(sessionId, { message: 'hello-world' }) - const contents = await fs.get('1234.txt') - assert.deepEqual(JSON.parse(contents), { - message: { message: 'hello-world' }, - purpose: '1234', - }) + await assert.fileEquals( + '1234.txt', + JSON.stringify({ message: { message: 'hello-world' }, purpose: '1234' }) + ) }) test('get session existing value', async ({ assert }) => { @@ -62,22 +60,21 @@ test.group('File driver', (group) => { await session.write(sessionId, { message: 'hello-world' }) await session.destroy(sessionId) - const exists = await fs.fsExtra.pathExists(join(fs.basePath, '1234.txt')) - assert.isFalse(exists) + await assert.fileNotExists('1234.txt') }) - test('update session expiry', async ({ assert }) => { + test('update session expiry', async ({ assert, fs }) => { const sessionId = '1234' const session = new FileDriver(config) await session.write(sessionId, { message: 'hello-world' }) await sleep(1000) - const { mtimeMs } = await fs.fsExtra.stat(join(fs.basePath, '1234.txt')) + const { mtimeMs } = await fs.adapter.stat(join(fs.basePath, '1234.txt')) assert.isBelow(mtimeMs, Date.now()) await session.touch(sessionId) - let { mtimeMs: newMtimeMs } = await fs.fsExtra.stat(join(fs.basePath, '1234.txt')) + let { mtimeMs: newMtimeMs } = await fs.adapter.stat(join(fs.basePath, '1234.txt')) assert.isAbove(newMtimeMs, mtimeMs) }).timeout(0) }) diff --git a/test/redis-driver.spec.ts b/test/redis_driver.spec.ts similarity index 77% rename from test/redis-driver.spec.ts rename to test/redis_driver.spec.ts index fbcaeb6..33c8e01 100644 --- a/test/redis-driver.spec.ts +++ b/test/redis_driver.spec.ts @@ -1,27 +1,21 @@ /* * @adonisjs/session * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -/// - import { test } from '@japa/runner' -import { RedisDriver } from '../src/Drivers/Redis' -import { fs, setup, sleep, sessionConfig, getRedisManager } from '../test-helpers' +import { RedisDriver } from '../src/drivers/redis.js' +import { setup, sleep, sessionConfig, getRedisManager } from '../test_helpers/index.js' const config = Object.assign({}, sessionConfig, { driver: 'redis', redisConnection: 'session' }) -test.group('Redis driver', (group) => { - group.each.teardown(async () => { - await fs.cleanup() - }) - - test('return null when value is missing', async ({ assert }) => { - const app = await setup() +test.group('Redis driver', () => { + test('return null when value is missing', async ({ fs, assert }) => { + const { app } = await setup(fs) const sessionId = '1234' const redis = getRedisManager(app) @@ -32,8 +26,8 @@ test.group('Redis driver', (group) => { assert.isNull(value) }) - test('write session value to the redis store', async ({ assert }) => { - const app = await setup() + test('write session value to the redis store', async ({ fs, assert }) => { + const { app } = await setup(fs) const sessionId = '1234' const redis = getRedisManager(app) @@ -51,8 +45,8 @@ test.group('Redis driver', (group) => { }) }) - test('get session existing value', async ({ assert }) => { - const app = await setup() + test('get session existing value', async ({ fs, assert }) => { + const { app } = await setup(fs) const sessionId = '1234' const redis = getRedisManager(app) @@ -73,8 +67,8 @@ test.group('Redis driver', (group) => { assert.deepEqual(contents, { message: 'hello-world' }) }) - test('remove session', async ({ assert }) => { - const app = await setup() + test('remove session', async ({ assert, fs }) => { + const { app } = await setup(fs) const sessionId = '1234' const redis = getRedisManager(app) @@ -98,8 +92,8 @@ test.group('Redis driver', (group) => { assert.isNull(contents) }) - test('update session expiry', async ({ assert }) => { - const app = await setup() + test('update session expiry', async ({ fs, assert }) => { + const { app } = await setup(fs) const sessionId = '1234' const redis = getRedisManager(app) diff --git a/test/session-provider.spec.ts b/test/session-provider.spec.ts deleted file mode 100644 index d3a2069..0000000 --- a/test/session-provider.spec.ts +++ /dev/null @@ -1,193 +0,0 @@ -/* - * @adonisjs/session - * - * (c) Harminder Virk - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' -import { createServer } from 'http' -import { ApiClient } from '@japa/api-client' -import { fs, setup } from '../test-helpers' -import { MemoryDriver } from '../src/Drivers/Memory' -import { SessionManager } from '../src/SessionManager' - -test.group('Session Provider', (group) => { - group.each.teardown(async () => { - ApiClient.clearSetupHooks() - ApiClient.clearTeardownHooks() - ApiClient.clearRequestHandlers() - await fs.cleanup() - }) - - test('register session provider', async ({ assert }) => { - const app = await setup({ - driver: 'cookie', - }) - - assert.instanceOf(app.container.use('Adonis/Addons/Session'), SessionManager) - assert.deepEqual( - app.container.use('Adonis/Addons/Session'), - app.container.use('Adonis/Addons/Session') - ) - assert.deepEqual(app.container.use('Adonis/Addons/Session')['application'], app) - assert.equal(app.container.use('Adonis/Core/Server').hooks['hooks'].before.length, 1) - assert.equal(app.container.use('Adonis/Core/Server').hooks['hooks'].after.length, 1) - }) - - test('raise error when config is missing', async ({ assert }) => { - assert.plan(1) - - try { - await setup({}) - } catch (error) { - assert.equal( - error.message, - 'Invalid "session" config. Missing value for "driver". Make sure to set it inside the "config/session" file' - ) - } - }) - - test('do not register hooks when session is disabled', async ({ assert }) => { - const app = await setup({ - enabled: false, - driver: 'cookie', - }) - - assert.instanceOf(app.container.use('Adonis/Addons/Session'), SessionManager) - assert.deepEqual( - app.container.use('Adonis/Addons/Session'), - app.container.use('Adonis/Addons/Session') - ) - assert.deepEqual(app.container.use('Adonis/Addons/Session')['application'], app) - assert.equal(app.container.use('Adonis/Core/Server').hooks['hooks'].before.length, 0) - assert.equal(app.container.use('Adonis/Core/Server').hooks['hooks'].after.length, 0) - }) - - test('register test api request methods', async ({ assert }) => { - const app = await setup({ - driver: 'cookie', - }) - - assert.instanceOf(app.container.use('Adonis/Addons/Session'), SessionManager) - assert.deepEqual( - app.container.use('Adonis/Addons/Session'), - app.container.use('Adonis/Addons/Session') - ) - - assert.isTrue(app.container.use('Japa/Preset/ApiRequest').hasMacro('session')) - assert.isTrue(app.container.use('Japa/Preset/ApiRequest').hasMacro('flashMessages')) - assert.isTrue(app.container.use('Japa/Preset/ApiRequest').hasGetter('sessionClient')) - }) - - test('set session before making the api request', async ({ assert }) => { - const app = await setup({ - driver: 'memory', - cookieName: 'adonis-session', - }) - - const server = createServer(async (req, res) => { - const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res) - await ctx.session.initiate(false) - - try { - ctx.response.send(ctx.session.all()) - } catch (error) { - ctx.response.status(500).send(error.stack) - } - - ctx.response.finish() - }) - server.listen(3333) - - const client = new (app.container.use('Japa/Preset/ApiClient'))('http://localhost:3333') - const response = await client.get('/').session({ username: 'virk' }) - server.close() - - assert.deepEqual(response.status(), 200) - assert.deepEqual(response.body(), { username: 'virk' }) - }) - - test('get session data from the response', async ({ assert }) => { - const app = await setup({ - driver: 'memory', - cookieName: 'adonis-session', - }) - - const server = createServer(async (req, res) => { - const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res) - - await ctx.session.initiate(false) - ctx.session.put('username', 'virk') - await ctx.session.commit() - - ctx.response.finish() - }) - server.listen(3333) - - const client = new (app.container.use('Japa/Preset/ApiClient'))('http://localhost:3333', assert) - const response = await client.get('/') - server.close() - - assert.equal(MemoryDriver.sessions.size, 0) - assert.deepEqual(response.status(), 200) - - response.assertSession('username', 'virk') - response.assertSessionMissing('age') - }) - - test('get flash messages from the response', async ({ assert }) => { - const app = await setup({ - driver: 'memory', - cookieName: 'adonis-session', - }) - - const server = createServer(async (req, res) => { - const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res) - - await ctx.session.initiate(false) - ctx.session.flash({ username: 'virk' }) - await ctx.session.commit() - - ctx.response.finish() - }) - server.listen(3333) - - const client = new (app.container.use('Japa/Preset/ApiClient'))('http://localhost:3333', assert) - const response = await client.get('/') - server.close() - - assert.equal(MemoryDriver.sessions.size, 0) - assert.deepEqual(response.status(), 200) - - response.assertFlashMessage('username', 'virk') - response.assertFlashMissing('age') - }) - - test('destroy session when request fails', async ({ assert }) => { - const app = await setup({ - driver: 'memory', - cookieName: 'adonis-session', - }) - - const server = createServer(async (req, res) => { - const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res) - - await ctx.session.initiate(false) - ctx.session.put('username', 'virk') - await ctx.session.commit() - - ctx.response.status(500).send('Server error') - ctx.response.finish() - }) - server.listen(3333) - - const client = new (app.container.use('Japa/Preset/ApiClient'))('http://localhost:3333', assert) - await assert.rejects(() => client.get('/')) - server.close() - - assert.equal(MemoryDriver.sessions.size, 0) - }) -}) diff --git a/test/session.spec.ts b/test/session.spec.ts index 05b9785..4c3cd44 100644 --- a/test/session.spec.ts +++ b/test/session.spec.ts @@ -1,36 +1,40 @@ /** * @adonisjs/session * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -/// - import { test } from '@japa/runner' import supertest from 'supertest' import { createServer } from 'http' -import { Store } from '../src/Store' -import { Session } from '../src/Session' -import { MemoryDriver } from '../src/Drivers/Memory' -import { setup, fs, sessionConfig, unsignCookie, signCookie } from '../test-helpers/index' +import { Store } from '../src/store.js' +import { Session } from '../src/session.js' +import { MemoryDriver } from '../src/drivers/memory.js' +import { + setup, + sessionConfig, + unsignCookie, + signCookie, + createHttpContext, +} from '../test_helpers/index.js' test.group('Session', (group) => { group.each.teardown(async () => { MemoryDriver.sessions.clear() - await fs.cleanup() }) test("initiate session with fresh session id when there isn't any session", async ({ assert, + fs, }) => { - const app = await setup() + const { app } = await setup(fs) const server = createServer(async (req, res) => { - const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res) + const ctx = await createHttpContext(app, req, res) const driver = new MemoryDriver() const session = new Session(ctx, sessionConfig, driver) @@ -44,11 +48,11 @@ test.group('Session', (group) => { await supertest(server).get('/') }) - test('initiate session with empty store when session id exists', async ({ assert }) => { - const app = await setup() + test('initiate session with empty store when session id exists', async ({ fs, assert }) => { + const { app } = await setup(fs) const server = createServer(async (req, res) => { - const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res) + const ctx = await createHttpContext(app, req, res) const driver = new MemoryDriver() const session = new Session(ctx, sessionConfig, driver) @@ -62,14 +66,14 @@ test.group('Session', (group) => { await supertest(server) .get('/') - .set('cookie', signCookie(app, '1234', sessionConfig.cookieName)) + .set('cookie', await signCookie(app, '1234', sessionConfig.cookieName)) }) - test('write session values with driver on commit', async ({ assert }) => { - const app = await setup() + test('write session values with driver on commit', async ({ assert, fs }) => { + const { app } = await setup(fs) const server = createServer(async (req, res) => { - const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res) + const ctx = await createHttpContext(app, req, res) const driver = new MemoryDriver() const session = new Session(ctx, sessionConfig, driver) @@ -83,17 +87,17 @@ test.group('Session', (group) => { const { header } = await supertest(server).get('/') - const sessionId = unsignCookie(app, header, sessionConfig.cookieName) + const sessionId = await unsignCookie(app, header, sessionConfig.cookieName) assert.exists(sessionId) const session = MemoryDriver.sessions.get(sessionId)! assert.deepEqual(new Store(session).all(), { user: { username: 'virk' } }) }) - test('re-use existing session id', async ({ assert }) => { - const app = await setup() + test('re-use existing session id', async ({ assert, fs }) => { + const { app } = await setup(fs) const server = createServer(async (req, res) => { - const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res) + const ctx = await createHttpContext(app, req, res) const driver = new MemoryDriver() const session = new Session(ctx, sessionConfig, driver) @@ -107,20 +111,20 @@ test.group('Session', (group) => { const { header } = await supertest(server) .get('/') - .set('cookie', signCookie(app, '1234', sessionConfig.cookieName)) + .set('cookie', await signCookie(app, '1234', sessionConfig.cookieName)) - const sessionId = unsignCookie(app, header, sessionConfig.cookieName) + const sessionId = await unsignCookie(app, header, sessionConfig.cookieName) assert.equal(sessionId, '1234') const session = MemoryDriver.sessions.get('1234')! assert.deepEqual(new Store(session).all(), { user: { username: 'virk' } }) }) - test('retain driver existing values', async ({ assert }) => { - const app = await setup() + test('retain driver existing values', async ({ fs, assert }) => { + const { app } = await setup(fs) const server = createServer(async (req, res) => { - const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res) + const ctx = await createHttpContext(app, req, res) const driver = new MemoryDriver() const session = new Session(ctx, sessionConfig, driver) @@ -144,9 +148,9 @@ test.group('Session', (group) => { */ const { header } = await supertest(server) .get('/') - .set('cookie', signCookie(app, '1234', sessionConfig.cookieName)) + .set('cookie', await signCookie(app, '1234', sessionConfig.cookieName)) - const sessionId = unsignCookie(app, header, sessionConfig.cookieName) + const sessionId = await unsignCookie(app, header, sessionConfig.cookieName) assert.equal(sessionId, '1234') /** @@ -156,11 +160,11 @@ test.group('Session', (group) => { assert.deepEqual(new Store(session).all(), { user: { username: 'virk', age: 22 } }) }) - test('regenerate session id when regenerate method is called', async ({ assert }) => { - const app = await setup() + test('regenerate session id when regenerate method is called', async ({ fs, assert }) => { + const { app } = await setup(fs) const server = createServer(async (req, res) => { - const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res) + const ctx = await createHttpContext(app, req, res) const driver = new MemoryDriver() const session = new Session(ctx, sessionConfig, driver) @@ -183,9 +187,9 @@ test.group('Session', (group) => { const { header } = await supertest(server) .get('/') - .set('cookie', signCookie(app, '1234', sessionConfig.cookieName)) + .set('cookie', await signCookie(app, '1234', sessionConfig.cookieName)) - const sessionId = unsignCookie(app, header, sessionConfig.cookieName) + const sessionId = await unsignCookie(app, header, sessionConfig.cookieName) assert.exists(sessionId) assert.notEqual(sessionId, '1234') @@ -199,11 +203,11 @@ test.group('Session', (group) => { assert.isUndefined(MemoryDriver.sessions.get('1234')) }) - test('get session value', async ({ assert }) => { - const app = await setup() + test('get session value', async ({ assert, fs }) => { + const { app } = await setup(fs) const server = createServer(async (req, res) => { - const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res) + const ctx = await createHttpContext(app, req, res) const driver = new MemoryDriver() const session = new Session(ctx, sessionConfig, driver) @@ -225,16 +229,16 @@ test.group('Session', (group) => { */ const { text } = await supertest(server) .get('/') - .set('cookie', signCookie(app, '1234', sessionConfig.cookieName)) + .set('cookie', await signCookie(app, '1234', sessionConfig.cookieName)) assert.equal(text, '22') }) - test('get nested value using form input syntax', async ({ assert }) => { - const app = await setup() + test('get nested value using form input syntax', async ({ assert, fs }) => { + const { app } = await setup(fs) const server = createServer(async (req, res) => { - const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res) + const ctx = await createHttpContext(app, req, res) const driver = new MemoryDriver() const session = new Session(ctx, sessionConfig, driver) @@ -256,7 +260,7 @@ test.group('Session', (group) => { */ const { text } = await supertest(server) .get('/') - .set('cookie', signCookie(app, '1234', sessionConfig.cookieName)) + .set('cookie', await signCookie(app, '1234', sessionConfig.cookieName)) assert.equal(text, '22') }) @@ -265,14 +269,13 @@ test.group('Session', (group) => { test.group('Session | Flash', (group) => { group.each.teardown(async () => { MemoryDriver.sessions.clear() - await fs.cleanup() }) - test('set custom flash messages', async ({ assert }) => { - const app = await setup() + test('set custom flash messages', async ({ assert, fs }) => { + const { app } = await setup(fs) const server = createServer(async (req, res) => { - const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res) + const ctx = await createHttpContext(app, req, res) const driver = new MemoryDriver() const session = new Session(ctx, sessionConfig, driver) @@ -289,7 +292,7 @@ test.group('Session | Flash', (group) => { /** * Ensure session id is changed */ - const sessionId = unsignCookie(app, header, sessionConfig.cookieName) + const sessionId = await unsignCookie(app, header, sessionConfig.cookieName) assert.exists(sessionId) const session = MemoryDriver.sessions.get(sessionId)! @@ -300,11 +303,11 @@ test.group('Session | Flash', (group) => { }) }) - test('flash input values', async ({ assert }) => { - const app = await setup() + test('flash input values', async ({ assert, fs }) => { + const { app } = await setup(fs) const server = createServer(async (req, res) => { - const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res) + const ctx = await createHttpContext(app, req, res) ctx.request.setInitialBody({ username: 'virk', age: 28 }) const driver = new MemoryDriver() @@ -319,7 +322,7 @@ test.group('Session | Flash', (group) => { const { header } = await supertest(server).get('/') - const sessionId = unsignCookie(app, header, sessionConfig.cookieName) + const sessionId = await unsignCookie(app, header, sessionConfig.cookieName) assert.exists(sessionId) const session = MemoryDriver.sessions.get(sessionId)! @@ -331,11 +334,11 @@ test.group('Session | Flash', (group) => { }) }) - test('flash selected input values', async ({ assert }) => { - const app = await setup() + test('flash selected input values', async ({ assert, fs }) => { + const { app } = await setup(fs) const server = createServer(async (req, res) => { - const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res) + const ctx = await createHttpContext(app, req, res) ctx.request.setInitialBody({ username: 'virk', @@ -357,7 +360,7 @@ test.group('Session | Flash', (group) => { const { header } = await supertest(server).get('/') - const sessionId = unsignCookie(app, header, sessionConfig.cookieName) + const sessionId = await unsignCookie(app, header, sessionConfig.cookieName) assert.exists(sessionId) const session = MemoryDriver.sessions.get(sessionId)! @@ -371,11 +374,11 @@ test.group('Session | Flash', (group) => { }) }) - test("flash all input values except the defined one's", async ({ assert }) => { - const app = await setup() + test("flash all input values except the defined one's", async ({ assert, fs }) => { + const { app } = await setup(fs) const server = createServer(async (req, res) => { - const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res) + const ctx = await createHttpContext(app, req, res) ctx.request.setInitialBody({ username: 'virk', @@ -397,7 +400,7 @@ test.group('Session | Flash', (group) => { const { header } = await supertest(server).get('/') - const sessionId = unsignCookie(app, header, sessionConfig.cookieName) + const sessionId = await unsignCookie(app, header, sessionConfig.cookieName) assert.exists(sessionId) const session = MemoryDriver.sessions.get(sessionId)! @@ -411,11 +414,11 @@ test.group('Session | Flash', (group) => { }) }) - test('flash input along with custom messages', async ({ assert }) => { - const app = await setup() + test('flash input along with custom messages', async ({ assert, fs }) => { + const { app } = await setup(fs) const server = createServer(async (req, res) => { - const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res) + const ctx = await createHttpContext(app, req, res) ctx.request.setInitialBody({ username: 'virk', @@ -435,7 +438,7 @@ test.group('Session | Flash', (group) => { const { header } = await supertest(server).get('/') - const sessionId = unsignCookie(app, header, sessionConfig.cookieName) + const sessionId = await unsignCookie(app, header, sessionConfig.cookieName) assert.exists(sessionId) const session = MemoryDriver.sessions.get(sessionId)! @@ -447,11 +450,11 @@ test.group('Session | Flash', (group) => { }) }) - test('read old flash values', async ({ assert }) => { - const app = await setup() + test('read old flash values', async ({ assert, fs }) => { + const { app } = await setup(fs) const server = createServer(async (req, res) => { - const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res) + const ctx = await createHttpContext(app, req, res) const driver = new MemoryDriver() const session = new Session(ctx, sessionConfig, driver) @@ -480,19 +483,19 @@ test.group('Session | Flash', (group) => { const { header } = await supertest(server) .get('/') - .set('cookie', signCookie(app, '1234', sessionConfig.cookieName)) + .set('cookie', await signCookie(app, '1234', sessionConfig.cookieName)) - const sessionId = unsignCookie(app, header, sessionConfig.cookieName) + const sessionId = await unsignCookie(app, header, sessionConfig.cookieName) assert.exists(sessionId) const session = MemoryDriver.sessions.get(sessionId)! assert.deepEqual(new Store(session).all(), {}) }) - test('read selected old values', async ({ assert }) => { - const app = await setup() + test('read selected old values', async ({ assert, fs }) => { + const { app } = await setup(fs) const server = createServer(async (req, res) => { - const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res) + const ctx = await createHttpContext(app, req, res) const driver = new MemoryDriver() const session = new Session(ctx, sessionConfig, driver) @@ -518,19 +521,19 @@ test.group('Session | Flash', (group) => { const { header } = await supertest(server) .get('/') - .set('cookie', signCookie(app, '1234', sessionConfig.cookieName)) + .set('cookie', await signCookie(app, '1234', sessionConfig.cookieName)) - const sessionId = unsignCookie(app, header, sessionConfig.cookieName) + const sessionId = await unsignCookie(app, header, sessionConfig.cookieName) assert.exists(sessionId) const session = MemoryDriver.sessions.get(sessionId)! assert.deepEqual(new Store(session).all(), {}) }) - test('flash custom messages as an object', async ({ assert }) => { - const app = await setup() + test('flash custom messages as an object', async ({ assert, fs }) => { + const { app } = await setup(fs) const server = createServer(async (req, res) => { - const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res) + const ctx = await createHttpContext(app, req, res) const driver = new MemoryDriver() const session = new Session(ctx, sessionConfig, driver) @@ -545,7 +548,7 @@ test.group('Session | Flash', (group) => { const { header } = await supertest(server).get('/') - const sessionId = unsignCookie(app, header, sessionConfig.cookieName) + const sessionId = await unsignCookie(app, header, sessionConfig.cookieName) assert.exists(sessionId) const session = MemoryDriver.sessions.get(sessionId)! @@ -557,11 +560,11 @@ test.group('Session | Flash', (group) => { }) }) - test('always flash original input values', async ({ assert }) => { - const app = await setup() + test('always flash original input values', async ({ assert, fs }) => { + const { app } = await setup(fs) const server = createServer(async (req, res) => { - const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res) + const ctx = await createHttpContext(app, req, res) ctx.request.setInitialBody({ username: 'virk', age: 28 }) ctx.request.updateBody({ username: 'nikk', age: 22 }) @@ -578,7 +581,7 @@ test.group('Session | Flash', (group) => { const { header } = await supertest(server).get('/') - const sessionId = unsignCookie(app, header, sessionConfig.cookieName) + const sessionId = await unsignCookie(app, header, sessionConfig.cookieName) assert.exists(sessionId) const session = MemoryDriver.sessions.get(sessionId)! @@ -590,12 +593,12 @@ test.group('Session | Flash', (group) => { }) }) - test('do not attempt to commit when initiate raises an exception', async ({ assert }) => { + test('do not attempt to commit when initiate raises an exception', async ({ assert, fs }) => { assert.plan(3) - const app = await setup() + const { app } = await setup(fs) const server = createServer(async (req, res) => { - const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res) + const ctx = await createHttpContext(app, req, res) ctx.request.setInitialBody({ username: 'virk', age: 28 }) ctx.request.updateBody({ username: 'nikk', age: 22 }) @@ -620,18 +623,18 @@ test.group('Session | Flash', (group) => { const { header } = await supertest(server).get('/') - const sessionId = unsignCookie(app, header, sessionConfig.cookieName) + const sessionId = await unsignCookie(app, header, sessionConfig.cookieName) assert.exists(sessionId) const session = MemoryDriver.sessions.get(sessionId)! assert.deepEqual(new Store(session).all(), {}) }) - test('reflash existing flash values', async ({ assert }) => { - const app = await setup() + test('reflash existing flash values', async ({ assert, fs }) => { + const { app } = await setup(fs) const server = createServer(async (req, res) => { - const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res) + const ctx = await createHttpContext(app, req, res) const driver = new MemoryDriver() const session = new Session(ctx, sessionConfig, driver) @@ -655,9 +658,9 @@ test.group('Session | Flash', (group) => { const { header } = await supertest(server) .get('/') - .set('cookie', signCookie(app, '1234', sessionConfig.cookieName)) + .set('cookie', await signCookie(app, '1234', sessionConfig.cookieName)) - const sessionId = unsignCookie(app, header, sessionConfig.cookieName) + const sessionId = await unsignCookie(app, header, sessionConfig.cookieName) assert.exists(sessionId) const session = MemoryDriver.sessions.get(sessionId)! assert.deepEqual(new Store(session).all(), { @@ -668,11 +671,11 @@ test.group('Session | Flash', (group) => { }) }) - test('cherry pick keys during reflash', async ({ assert }) => { - const app = await setup() + test('cherry pick keys during reflash', async ({ assert, fs }) => { + const { app } = await setup(fs) const server = createServer(async (req, res) => { - const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res) + const ctx = await createHttpContext(app, req, res) const driver = new MemoryDriver() const session = new Session(ctx, sessionConfig, driver) @@ -698,9 +701,9 @@ test.group('Session | Flash', (group) => { const { header } = await supertest(server) .get('/') - .set('cookie', signCookie(app, '1234', sessionConfig.cookieName)) + .set('cookie', await signCookie(app, '1234', sessionConfig.cookieName)) - const sessionId = unsignCookie(app, header, sessionConfig.cookieName) + const sessionId = await unsignCookie(app, header, sessionConfig.cookieName) assert.exists(sessionId) const session = MemoryDriver.sessions.get(sessionId)! assert.deepEqual(new Store(session).all(), { @@ -710,11 +713,11 @@ test.group('Session | Flash', (group) => { }) }) - test('ignore keys during reflash', async ({ assert }) => { - const app = await setup() + test('ignore keys during reflash', async ({ assert, fs }) => { + const { app } = await setup(fs) const server = createServer(async (req, res) => { - const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res) + const ctx = await createHttpContext(app, req, res) const driver = new MemoryDriver() const session = new Session(ctx, sessionConfig, driver) @@ -740,9 +743,9 @@ test.group('Session | Flash', (group) => { const { header } = await supertest(server) .get('/') - .set('cookie', signCookie(app, '1234', sessionConfig.cookieName)) + .set('cookie', await signCookie(app, '1234', sessionConfig.cookieName)) - const sessionId = unsignCookie(app, header, sessionConfig.cookieName) + const sessionId = await unsignCookie(app, header, sessionConfig.cookieName) assert.exists(sessionId) const session = MemoryDriver.sessions.get(sessionId)! assert.deepEqual(new Store(session).all(), { @@ -751,4 +754,22 @@ test.group('Session | Flash', (group) => { }, }) }) + + test('should not store session if empty', async ({ assert, fs }) => { + const { app } = await setup(fs) + + const server = createServer(async (req, res) => { + const ctx = await createHttpContext(app, req, res) + + const driver = new MemoryDriver() + const session = new Session(ctx, sessionConfig, driver) + await session.initiate(false) + + assert.isTrue(session.fresh) + assert.isTrue(session.initiated) + res.end() + }) + + await supertest(server).get('/') + }) }) diff --git a/test/session-client.spec.ts b/test/session_client.spec.ts similarity index 72% rename from test/session-client.spec.ts rename to test/session_client.spec.ts index 9791746..0f52765 100644 --- a/test/session-client.spec.ts +++ b/test/session_client.spec.ts @@ -1,42 +1,40 @@ /** * @adonisjs/session * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -/// - import { test } from '@japa/runner' import supertest from 'supertest' import { createServer } from 'http' import setCookieParser from 'set-cookie-parser' -import { MemoryDriver } from '../src/Drivers/Memory' -import { SessionManager } from '../src/SessionManager' -import { setup, fs, sessionConfig } from '../test-helpers' +import { MemoryDriver } from '../src/drivers/memory.js' +import { setup, sessionConfig, createHttpContext } from '../test_helpers/index.js' +import { SessionManagerFactory } from '../factories/session_manager_factory.js' +import { CookieClient } from '@adonisjs/core/http' test.group('Session Client', (group) => { group.each.teardown(async () => { MemoryDriver.sessions = new Map() - await fs.cleanup() }) - test('set session using the session client', async ({ assert }) => { + test('set session using the session client', async ({ fs, assert }) => { assert.plan(1) - const app = await setup() + const { app } = await setup(fs) const config = Object.assign({}, sessionConfig, { driver: 'memory', clearWithBrowser: true }) - const manager = new SessionManager(app, config) + const manager = new SessionManagerFactory().merge(config).create(app) const client = manager.client() client.set('username', 'virk') const { signedSessionId, cookieName } = await client.commit() const server = createServer(async (req, res) => { - const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res) + const ctx = await createHttpContext(app, req, res) const session = manager.create(ctx) await session.initiate(false) @@ -47,19 +45,20 @@ test.group('Session Client', (group) => { await supertest(server).get('/').set('Cookie', `${cookieName}=${signedSessionId}`) }) - test('set flash messages', async ({ assert }) => { + test('set flash messages', async ({ fs, assert }) => { assert.plan(1) - const app = await setup() + const { app } = await setup(fs) const config = Object.assign({}, sessionConfig, { driver: 'memory', clearWithBrowser: true }) - const manager = new SessionManager(app, config) + const manager = new SessionManagerFactory().merge(config).create(app) const client = manager.client() client.flashMessages.merge({ foo: 'bar' }) const { signedSessionId, cookieName } = await client.commit() const server = createServer(async (req, res) => { - const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res) + const ctx = await createHttpContext(app, req, res) + const session = manager.create(ctx) await session.initiate(false) @@ -70,19 +69,19 @@ test.group('Session Client', (group) => { await supertest(server).get('/').set('Cookie', `${cookieName}=${signedSessionId}`) }) - test('clear session store', async ({ assert }) => { + test('clear session store', async ({ fs, assert }) => { assert.plan(1) - const app = await setup() + const { app } = await setup(fs) const config = Object.assign({}, sessionConfig, { driver: 'memory', clearWithBrowser: true }) - const manager = new SessionManager(app, config) + const manager = new SessionManagerFactory().merge(config).create(app) const client = manager.client() client.set('username', 'virk') const { signedSessionId, cookieName } = await client.commit() const server = createServer(async (req, res) => { - const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res) + const ctx = await createHttpContext(app, req, res) const session = manager.create(ctx) await session.initiate(false) @@ -94,19 +93,18 @@ test.group('Session Client', (group) => { await supertest(server).get('/').set('Cookie', `${cookieName}=${signedSessionId}`) }) - test('get session data from the driver', async ({ assert }) => { - const app = await setup() + test('get session data from the driver', async ({ fs, assert }) => { + const { app } = await setup(fs) const config = Object.assign({}, sessionConfig, { driver: 'memory', clearWithBrowser: true }) - const manager = new SessionManager(app, config) - const cookieClient = app.container.resolveBinding('Adonis/Core/CookieClient') + const manager = new SessionManagerFactory().merge(config).create(app) const client = manager.client() client.set('username', 'virk') const { signedSessionId, cookieName } = await client.commit() const server = createServer(async (req, res) => { - const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res) + const ctx = await createHttpContext(app, req, res) const session = manager.create(ctx) await session.initiate(false) session.put('age', 22) @@ -120,13 +118,14 @@ test.group('Session Client', (group) => { .get('/') .set('Cookie', `${cookieName}=${signedSessionId}`) + const cookieClient = new CookieClient(await app.container.make('encryption')) const cookies = setCookieParser.parse(response.header['set-cookie'], { map: true }) const parsedCookies = Object.keys(cookies).reduce((result, key) => { const value = cookies[key] value.value = cookieClient.parse(value.name, value.value) result[key] = value return result - }, {}) + }, {} as Record) const { session, flashMessages } = await client.load(parsedCookies) @@ -134,18 +133,17 @@ test.group('Session Client', (group) => { assert.isNull(flashMessages) }) - test('get flash messages from the driver', async ({ assert }) => { - const app = await setup() + test('get flash messages from the driver', async ({ fs, assert }) => { + const { app } = await setup(fs) const config = Object.assign({}, sessionConfig, { driver: 'memory', clearWithBrowser: true }) - const manager = new SessionManager(app, config) - const cookieClient = app.container.resolveBinding('Adonis/Core/CookieClient') + const manager = new SessionManagerFactory().merge(config).create(app) const client = manager.client() const { signedSessionId, cookieName } = await client.commit() const server = createServer(async (req, res) => { - const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res) + const ctx = await createHttpContext(app, req, res) const session = manager.create(ctx) await session.initiate(false) @@ -160,13 +158,14 @@ test.group('Session Client', (group) => { .get('/') .set('Cookie', `${cookieName}=${signedSessionId}`) + const cookieClient = new CookieClient(await app.container.make('encryption')) const cookies = setCookieParser.parse(response.header['set-cookie'], { map: true }) const parsedCookies = Object.keys(cookies).reduce((result, key) => { const value = cookies[key] value.value = cookieClient.parse(value.name, value.value) result[key] = value return result - }, {}) + }, {} as Record) const { session, flashMessages } = await client.load(parsedCookies) diff --git a/test/session-manager.spec.ts b/test/session_manager.spec.ts similarity index 56% rename from test/session-manager.spec.ts rename to test/session_manager.spec.ts index 42bd858..b392ae8 100644 --- a/test/session-manager.spec.ts +++ b/test/session_manager.spec.ts @@ -1,35 +1,38 @@ /** * @adonisjs/session * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -/// - import { test } from '@japa/runner' import supertest from 'supertest' import { createServer } from 'http' -import { MessageBuilder } from '@poppinss/utils/build/helpers' - -import { Store } from '../src/Store' -import { SessionManager } from '../src/SessionManager' -import { setup, fs, sessionConfig, unsignCookie, getRedisManager } from '../test-helpers' - -test.group('Session Manager', (group) => { - group.each.teardown(async () => { - await fs.cleanup() - }) - - test('do not set maxAge when clearWithBrowser is true', async ({ assert }) => { - const app = await setup() +import { MessageBuilder } from '@poppinss/utils' +import setCookieParser from 'set-cookie-parser' +import { HttpContextFactory } from '@adonisjs/core/factories/http' + +import { Store } from '../src/store.js' +import { + setup, + sessionConfig, + unsignCookie, + getRedisManager, + createHttpContext, +} from '../test_helpers/index.js' +import { SessionManagerFactory } from '../factories/session_manager_factory.js' +import { SessionDriverContract } from '../src/types.js' + +test.group('Session Manager', () => { + test('do not set maxAge when clearWithBrowser is true', async ({ assert, fs }) => { + const { app } = await setup(fs) const config = Object.assign({}, sessionConfig, { clearWithBrowser: true }) - const manager = new SessionManager(app, config) + const manager = new SessionManagerFactory().merge(config).create(app) const server = createServer(async (req, res) => { - const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res) + const ctx = await createHttpContext(app, req, res) const session = manager.create(ctx) await session.initiate(false) @@ -40,15 +43,15 @@ test.group('Session Manager', (group) => { }) const { header } = await supertest(server).get('/') - assert.lengthOf(header['set-cookie'][0].split(';'), 2) + assert.lengthOf(header['set-cookie'][0].split(';'), 3) }) - test('set maxAge when clearWithBrowser is false', async ({ assert }) => { - const app = await setup() - const manager = new SessionManager(app, sessionConfig) + test('set maxAge when clearWithBrowser is false', async ({ assert, fs }) => { + const { app } = await setup(fs) + const manager = new SessionManagerFactory().create(app) const server = createServer(async (req, res) => { - const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res) + const ctx = await createHttpContext(app, req, res) const session = manager.create(ctx) await session.initiate(false) @@ -60,24 +63,22 @@ test.group('Session Manager', (group) => { }) const { header } = await supertest(server).get('/') - assert.lengthOf(header['set-cookie'][0].split(';'), 3) + const cookies = setCookieParser.parse(header['set-cookie'][0]) - const maxAge = header['set-cookie'][0].split(';')[1].replace(' Max-Age=', '') - assert.equal(maxAge, '3000') + assert.isDefined(cookies[0].maxAge) + assert.equal(cookies[0].maxAge, sessionConfig.age) }) - test('use file driver to persist session value', async ({ assert }) => { - const app = await setup() + test('use file driver to persist session value', async ({ assert, fs }) => { + const { app } = await setup(fs) const config = Object.assign({}, sessionConfig, { driver: 'file', - file: { - location: fs.basePath, - }, + file: { location: fs.basePath }, }) - const manager = new SessionManager(app, config) + const manager = new SessionManagerFactory().merge(config).create(app) const server = createServer(async (req, res) => { - const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res) + const ctx = await createHttpContext(app, req, res) const session = manager.create(ctx) await session.initiate(false) @@ -89,26 +90,33 @@ test.group('Session Manager', (group) => { const { header } = await supertest(server).get('/') - const sessionId = unsignCookie(app, header, sessionConfig.cookieName) - const sessionContents = await fs.get(`${sessionId}.txt`) + const sessionId = await unsignCookie(app, header, sessionConfig.cookieName) + const sessionContents = await fs.contents(`${sessionId}.txt`) const sessionValues = new MessageBuilder().verify(sessionContents, sessionId) assert.deepEqual(new Store(sessionValues).all(), { user: { username: 'virk' } }) }) - test('use redis driver to persist session value', async ({ assert }) => { - const app = await setup() + test('use redis driver to persist session value', async ({ assert, fs }) => { + const { app } = await setup(fs) const config = Object.assign({}, sessionConfig, { driver: 'redis', redisConnection: 'session', }) const redis = getRedisManager(app) - const manager = new SessionManager(app, config) + const manager = new SessionManagerFactory() + .merge(config) + .mergeRedisManagerOptions({ + connection: 'session', + connections: { session: { host: 'localhost', port: 6379 } }, + }) + .create(app) - app.container.singleton('Adonis/Addons/Redis', () => redis) + // @ts-ignore + app.container.singleton('redis', () => redis) const server = createServer(async (req, res) => { - const ctx = app.container.use('Adonis/Core/HttpContext').create('/', {}, req, res) + const ctx = await createHttpContext(app, req, res) const session = manager.create(ctx) await session.initiate(false) @@ -119,7 +127,7 @@ test.group('Session Manager', (group) => { }) const { header } = await supertest(server).get('/') - const sessionId = unsignCookie(app, header, sessionConfig.cookieName) + const sessionId = await unsignCookie(app, header, sessionConfig.cookieName) const sessionContents = await redis.connection('session').get(sessionId) const sessionValues = new MessageBuilder().verify(sessionContents, sessionId) @@ -130,35 +138,29 @@ test.group('Session Manager', (group) => { assert.deepEqual(new Store(sessionValues).all(), { user: { username: 'virk' } }) }) - test('extend by adding a custom driver', async ({ assert }) => { - assert.plan(2) + test('extend by adding a custom driver', async ({ assert, fs }) => { + assert.plan(1) - const app = await setup( - Object.assign({}, sessionConfig, { + const { app } = await setup(fs, { + session: { driver: 'mongo', - }) - ) + }, + }) - class MongoDriver { + class MongoDriver implements SessionDriverContract { public read() { return {} } - public write(_, data: any) { + public write(_: string, data: any) { assert.deepEqual(data, { name: 'virk' }) } public touch() {} public destroy() {} } - app.container.singleton('Adonis/Addons/Redis', () => getRedisManager(app)) - app.container.use('Adonis/Addons/Session').extend('mongo', (manager) => { - assert.deepEqual(app.container.use('Adonis/Addons/Session'), manager) - return new MongoDriver() - }) - - const session = app.container - .use('Adonis/Addons/Session') - .create(app.container.use('Adonis/Core/HttpContext').create('/', {})) + const sessionManager = await app.container.make('session') + sessionManager.extend('mongo', () => new MongoDriver()) + const session = sessionManager.create(new HttpContextFactory().create()) await session.initiate(false) session.put('name', 'virk') diff --git a/test/session_middleware.spec.ts b/test/session_middleware.spec.ts new file mode 100644 index 0000000..2a12930 --- /dev/null +++ b/test/session_middleware.spec.ts @@ -0,0 +1,47 @@ +/* + * @adonisjs/cors + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import supertest from 'supertest' +import { test } from '@japa/runner' +import { createServer } from 'node:http' + +import SessionMiddleware from '../src/session_middleware.js' +import { createHttpContext, setup, unsignCookie } from '../test_helpers/index.js' + +test.group('Session', () => { + test('should initiate and commit the session', async ({ assert, fs }) => { + assert.plan(3) + + const { app } = await setup(fs, { + session: { + enabled: true, + cookieName: 'adonis-session', + driver: 'file', + file: { location: fs.basePath }, + }, + }) + + const server = createServer(async (req, res) => { + const middleware = new SessionMiddleware() + const ctx = await createHttpContext(app, req, res) + await middleware.handle(ctx, () => { + assert.isTrue(ctx.session.initiated) + ctx.session.put('username', 'jul') + }) + ctx.response.finish() + }) + + const { header } = await supertest(server).get('/') + const sessionId = await unsignCookie(app, header, 'adonis-session') + + await assert.fileExists(`${sessionId}.txt`) + const content = await fs.contentsJson(`${sessionId}.txt`) + assert.deepInclude(content, { message: { username: 'jul' } }) + }) +}) diff --git a/test/session_provider.spec.ts b/test/session_provider.spec.ts new file mode 100644 index 0000000..244506a --- /dev/null +++ b/test/session_provider.spec.ts @@ -0,0 +1,144 @@ +/* + * @adonisjs/session + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { createServer } from 'http' +import { ApiClient, ApiRequest } from '@japa/api-client' +import { createHttpContext, setup } from '../test_helpers/index.js' +import { SessionManager } from '../src/session_manager.js' +import { MemoryDriver } from '../src/drivers/memory.js' + +test.group('Session Provider', (group) => { + group.each.teardown(async () => { + ApiClient.clearSetupHooks() + ApiClient.clearTeardownHooks() + ApiClient.clearRequestHandlers() + }) + + test('register session provider', async ({ fs, assert }) => { + const { app } = await setup(fs) + + assert.instanceOf(await app.container.make('session'), SessionManager) + assert.deepEqual(await app.container.make('session'), await app.container.make('session')) + }) + + test('register test api request methods', async ({ assert, fs }) => { + await setup(fs) + + assert.isTrue(ApiRequest.hasMacro('session')) + assert.isTrue(ApiRequest.hasMacro('flashMessages')) + assert.isTrue(ApiRequest.hasGetter('sessionClient')) + }) + + test('set session before making the api request', async ({ fs, assert }) => { + const { app } = await setup(fs, { + session: { driver: 'memory', cookieName: 'adonis-session' }, + }) + + const server = createServer(async (req, res) => { + const ctx = await createHttpContext(app, req, res) + + await ctx.session.initiate(false) + + try { + ctx.response.send(ctx.session.all()) + } catch (error) { + ctx.response.status(500).send(error.stack) + } + + ctx.response.finish() + }) + server.listen(3333) + + const client = new ApiClient('http://localhost:3333') + const response = await client.get('/').session({ username: 'virk' }) + server.close() + + assert.deepEqual(response.status(), 200) + assert.deepEqual(response.body(), { username: 'virk' }) + }) + + test('get session data from the response', async ({ fs, assert }) => { + const { app } = await setup(fs, { + session: { driver: 'memory', cookieName: 'adonis-session' }, + }) + + const server = createServer(async (req, res) => { + const ctx = await createHttpContext(app, req, res) + + await ctx.session.initiate(false) + ctx.session.put('username', 'virk') + await ctx.session.commit() + + ctx.response.finish() + }) + server.listen(3333) + + const client = new ApiClient('http://localhost:3333', assert) + const response = await client.get('/') + server.close() + + assert.equal(MemoryDriver.sessions.size, 0) + assert.deepEqual(response.status(), 200) + + response.assertSession('username', 'virk') + response.assertSessionMissing('age') + }) + + test('get flash messages from the response', async ({ fs, assert }) => { + const { app } = await setup(fs, { + session: { driver: 'memory', cookieName: 'adonis-session' }, + }) + + const server = createServer(async (req, res) => { + const ctx = await createHttpContext(app, req, res) + + await ctx.session.initiate(false) + ctx.session.flash({ username: 'virk' }) + await ctx.session.commit() + + ctx.response.finish() + }) + server.listen(3333) + + const client = new ApiClient('http://localhost:3333', assert) + const response = await client.get('/') + server.close() + + assert.equal(MemoryDriver.sessions.size, 0) + assert.deepEqual(response.status(), 200) + + response.assertFlashMessage('username', 'virk') + response.assertFlashMissing('age') + }) + + test('destroy session when request fails', async ({ fs, assert }) => { + const { app } = await setup(fs, { + session: { driver: 'memory', cookieName: 'adonis-session' }, + }) + + const server = createServer(async (req, res) => { + const ctx = await createHttpContext(app, req, res) + + await ctx.session.initiate(false) + ctx.session.put('username', 'virk') + await ctx.session.commit() + + ctx.response.status(500).send('Server error') + ctx.response.finish() + }) + server.listen(3333) + + const client = new ApiClient('http://localhost:3333', assert) + await assert.rejects(() => client.get('/')) + server.close() + + assert.equal(MemoryDriver.sessions.size, 0) + }) +}) diff --git a/test/store.spec.ts b/test/store.spec.ts index d0ed277..092848e 100644 --- a/test/store.spec.ts +++ b/test/store.spec.ts @@ -1,14 +1,14 @@ /* * @adonisjs/session * - * (c) Harminder Virk + * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ import { test } from '@japa/runner' -import { Store } from '../src/Store' +import { Store } from '../src/store.js' test.group('Store', () => { test('return empty object for empty store', ({ assert }) => { diff --git a/test_helpers/index.ts b/test_helpers/index.ts new file mode 100644 index 0000000..48d9466 --- /dev/null +++ b/test_helpers/index.ts @@ -0,0 +1,143 @@ +/* + * @adonisjs/session + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { IgnitorFactory } from '@adonisjs/core/factories' +import { FileSystem } from '@japa/file-system' +import { SessionConfig } from '../src/types.js' +import { ApplicationService } from '@adonisjs/core/types' +import { RedisService } from '@adonisjs/redis/types' +import { RedisManagerFactory } from '@adonisjs/redis/factories' +import { Application } from '@adonisjs/core/app' +import { IncomingMessage, ServerResponse } from 'http' +import { Server } from '@adonisjs/core/http' +import { pluginAdonisJS } from '@japa/plugin-adonisjs' +import { Runner } from '@japa/runner/core' + +/** + * Session default config + */ +export const sessionConfig: SessionConfig = { + enabled: true, + driver: 'cookie', + cookieName: 'adonis-session', + clearWithBrowser: false, + age: 3000, + cookie: { + path: '/', + }, +} + +export const BASE_URL = new URL('./tmp/', import.meta.url) + +export async function setup(fs: FileSystem, config?: any) { + const IMPORTER = (filePath: string) => { + if (filePath.startsWith('./') || filePath.startsWith('../')) { + return import(new URL(filePath, BASE_URL).href) + } + return import(filePath) + } + + const ignitor = new IgnitorFactory() + .withCoreConfig() + .withCoreProviders() + .merge({ + rcFileContents: { providers: ['../../providers/session_provider.js'] }, + config: config || { session: sessionConfig }, + }) + .create(fs.baseUrl, { importer: IMPORTER }) + + const app = ignitor.createApp('web') + + await app.init() + await app.boot() + + // @ts-ignore + await pluginAdonisJS(app)({ + runner: new Runner({} as any), + }) + + return { app, ignitor } +} + +/** + * Sleep for a while + */ +export function sleep(time: number): Promise { + return new Promise((resolve) => setTimeout(resolve, time)) +} + +/** + * Signs value to be set as cookie header + */ +export async function signCookie(app: ApplicationService, value: any, name: string) { + const encryption = await app.container.make('encryption') + return `${name}=s:${encryption.verifier.sign(value, undefined, name)}` +} + +/** + * Encrypt value to be set as cookie header + */ +export async function encryptCookie(app: ApplicationService, value: any, name: string) { + const encryption = await app.container.make('encryption') + return `${name}=e:${encryption.encrypt(value, undefined, name)}` +} + +/** + * Decrypt cookie + */ +export async function decryptCookie(app: ApplicationService, header: any, name: string) { + const encryption = await app.container.make('encryption') + const cookieValue = decodeURIComponent(header['set-cookie'][0].split(';')[0]) + .replace(`${name}=`, '') + .slice(2) + + return encryption.decrypt(cookieValue, name) +} + +/** + * Unsign cookie + */ +export async function unsignCookie(app: ApplicationService, header: any, name: string) { + const encryption = await app.container.make('encryption') + + const cookieValue = decodeURIComponent(header['set-cookie'][0].split(';')[0]) + .replace(`${name}=`, '') + .slice(2) + + return encryption.verifier.unsign(cookieValue, name) +} + +/** + * Reference to the redis manager + */ +export function getRedisManager(application: ApplicationService) { + return new RedisManagerFactory({ + connection: 'session', + connections: { + session: { + host: process.env.REDIS_HOST || '0.0.0.0', + port: process.env.REDIS_PORT || 6379, + }, + }, + }).create(application) as RedisService +} + +export async function createHttpContext( + app: Application, + req: IncomingMessage, + res: ServerResponse +) { + const adonisServer = (await app.container.make('server')) as Server + + const request = adonisServer.createRequest(req, res) + const response = adonisServer.createResponse(req, res) + const ctx = adonisServer.createHttpContext(request, response, app.container.createResolver()) + + return ctx +} diff --git a/tsconfig.json b/tsconfig.json index ec9a058..0cfd318 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,7 @@ { - "extends": "./node_modules/@adonisjs/mrm-preset/_tsconfig", + "extends": "@adonisjs/tsconfig/tsconfig.package.json", "compilerOptions": { - "skipLibCheck": true - }, - "files": [ - "./node_modules/@adonisjs/core/build/adonis-typings/index.d.ts", - "./node_modules/@japa/preset-adonis/build/adonis-typings/index.d.ts", - "./node_modules/@adonisjs/redis/build/adonis-typings/index.d.ts" - ] + "rootDir": "./", + "outDir": "./build", + } } From ab3bf351fa376ae293b0831963328aad68544cb4 Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 21 Jul 2023 11:48:12 +0200 Subject: [PATCH 004/112] chore: cleanup dependencies --- package.json | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/package.json b/package.json index 21d9da9..3d406ca 100644 --- a/package.json +++ b/package.json @@ -26,10 +26,8 @@ "@adonisjs/application": "7.1.2-8", "@adonisjs/core": "6.1.5-8", "@adonisjs/eslint-config": "^1.1.8", - "@adonisjs/mrm-preset": "^5.0.3", "@adonisjs/prettier-config": "^1.1.8", "@adonisjs/redis": "8.0.0-1", - "@adonisjs/require-ts": "^2.0.12", "@adonisjs/tsconfig": "^1.1.8", "@adonisjs/view": "7.0.0-4", "@japa/api-client": "^1.4.4", @@ -37,24 +35,17 @@ "@japa/file-system": "2.0.0-1", "@japa/plugin-adonisjs": "2.0.0-1", "@japa/runner": "3.0.0-6", - "@paralleldrive/cuid2": "^2.2.1", "@poppinss/dev-utils": "^2.0.3", "@swc/core": "^1.3.69", "@types/fs-extra": "^11.0.1", "@types/node": "^18.7.15", "@types/set-cookie-parser": "^2.4.3", "@types/supertest": "^2.0.12", - "commitizen": "^4.2.5", "copyfiles": "^2.4.1", - "cz-conventional-changelog": "^3.3.0", "del-cli": "^5.0.0", "eslint": "^8.23.0", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-adonis": "^2.1.0", - "eslint-plugin-prettier": "^4.2.1", "github-label-sync": "^2.2.0", "husky": "^8.0.1", - "mrm": "^4.1.0", "np": "^7.6.2", "prettier": "^2.7.1", "set-cookie-parser": "^2.5.1", @@ -106,11 +97,6 @@ "commit-msg": "node ./node_modules/@adonisjs/mrm-preset/validateCommit/conventional/validate.js" } }, - "config": { - "commitizen": { - "path": "cz-conventional-changelog" - } - }, "publishConfig": { "tag": "latest", "access": "public" From 0299ee6d9a3081d6b0940424ca0db499f4bfb5ae Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 21 Jul 2023 11:48:57 +0200 Subject: [PATCH 005/112] chore: update published files --- package.json | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 3d406ca..7fa162a 100644 --- a/package.json +++ b/package.json @@ -2,17 +2,16 @@ "name": "@adonisjs/session", "version": "6.4.0", "description": "Session provider for AdonisJS", - "typings": "./build/adonis-typings/index.d.ts", - "main": "build/providers/SessionProvider.js", + "main": "build/index.js", "type": "module", "files": [ - "build/adonis-typings", - "build/providers", "build/src", - "build/config.js", - "build/config.d.ts", - "build/templates", - "build/instructions.md" + "build/stubs", + "build/providers", + "build/index.d.ts", + "build/index.js", + "build/configure.d.ts", + "build/configure.js" ], "dependencies": { "@poppinss/utils": "6.5.0-3", From 9a74d1026d80bd4b76209e2e356f1439c6a34442 Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 21 Jul 2023 11:49:44 +0200 Subject: [PATCH 006/112] chore: update dependencies --- package.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 7fa162a..7006242 100644 --- a/package.json +++ b/package.json @@ -35,22 +35,22 @@ "@japa/plugin-adonisjs": "2.0.0-1", "@japa/runner": "3.0.0-6", "@poppinss/dev-utils": "^2.0.3", - "@swc/core": "^1.3.69", + "@swc/core": "^1.3.70", "@types/fs-extra": "^11.0.1", - "@types/node": "^18.7.15", + "@types/node": "^20.4.2", "@types/set-cookie-parser": "^2.4.3", "@types/supertest": "^2.0.12", "copyfiles": "^2.4.1", "del-cli": "^5.0.0", - "eslint": "^8.23.0", - "github-label-sync": "^2.2.0", - "husky": "^8.0.1", - "np": "^7.6.2", - "prettier": "^2.7.1", - "set-cookie-parser": "^2.5.1", - "supertest": "^6.2.4", + "eslint": "^8.45.0", + "github-label-sync": "^2.3.1", + "husky": "^8.0.3", + "np": "^8.0.4", + "prettier": "^3.0.0", + "set-cookie-parser": "^2.6.0", + "supertest": "^6.3.3", "ts-node": "^10.9.1", - "typescript": "^4.8.2" + "typescript": "^5.1.6" }, "scripts": { "mrm": "mrm --preset=@adonisjs/mrm-preset", From 8bc73013068529da8859a9f080a5ab596919dee6 Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 21 Jul 2023 11:50:12 +0200 Subject: [PATCH 007/112] chore: update pkg scripts --- package.json | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 7006242..dd5ee5a 100644 --- a/package.json +++ b/package.json @@ -53,21 +53,20 @@ "typescript": "^5.1.6" }, "scripts": { - "mrm": "mrm --preset=@adonisjs/mrm-preset", "pretest": "npm run lint", "test": "c8 npm run quick:test", - "quick:test": "node --enable-source-maps --loader=ts-node/esm bin/test.ts", - "clean": "del build", + "clean": "del-cli build", + "typecheck": "tsc --noEmit", + "copy:templates": "copyfiles \"stubs/**/*.stub\" build", "compile": "npm run lint && npm run clean && tsc", - "copyfiles": "copyfiles \"templates/**/*.txt\" \"instructions.md\" build", - "build": "npm run compile && npm run copyfiles", - "commit": "git-cz", - "release": "np --message=\"chore(release): %s\"", - "version": "npm run build", + "build": "npm run compile && npm run copy:templates", "prepublishOnly": "npm run build", "lint": "eslint . --ext=.ts", "format": "prettier --write .", - "sync-labels": "github-label-sync --labels ./node_modules/@adonisjs/mrm-preset/gh-labels.json adonisjs/session" + "release": "np", + "version": "npm run build", + "sync-labels": "github-label-sync --labels .github/labels.json adonisjs/ally", + "quick:test": "node --enable-source-maps --loader=ts-node/esm bin/test.ts" }, "repository": { "type": "git", From 52bbe02a5d782fe4e463e43a54009854e4c8f2af Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 21 Jul 2023 11:51:02 +0200 Subject: [PATCH 008/112] chore: update tooling config --- package.json | 92 +++++++++++++--------------------------------------- 1 file changed, 22 insertions(+), 70 deletions(-) diff --git a/package.json b/package.json index dd5ee5a..ed322c0 100644 --- a/package.json +++ b/package.json @@ -82,83 +82,35 @@ "url": "https://github.com/adonisjs/adonis-session/issues" }, "homepage": "https://github.com/adonisjs/adonis-session#readme", - "nyc": { - "exclude": [ - "test" - ], - "extension": [ - ".ts" - ] - }, - "husky": { - "hooks": { - "commit-msg": "node ./node_modules/@adonisjs/mrm-preset/validateCommit/conventional/validate.js" - } - }, "publishConfig": { - "tag": "latest", - "access": "public" - }, - "adonisjs": { - "instructionsMd": "./build/instructions.md", - "templates": { - "config": [ - "session.txt" - ] - }, - "env": { - "SESSION_DRIVER": "cookie" - }, - "types": "@adonisjs/session", - "providers": [ - "@adonisjs/session" - ] + "access": "public", + "tag": "next" }, "np": { - "contents": ".", + "message": "chore(release): %s", + "tag": "next", + "branch": "main", "anyBranch": false }, - "mrmConfig": { - "core": true, - "license": "MIT", - "services": [ - "github-actions" - ], - "minNodeVersion": "14.15.4", - "probotApps": [ - "stale", - "lock" - ], - "runGhActionsOnWindows": false - }, + "prettier": "@adonisjs/prettier-config", "eslintConfig": { + "extends": "@adonisjs/eslint-config/package" + }, + "commitlint": { "extends": [ - "plugin:adonis/typescriptPackage", - "prettier" - ], - "plugins": [ - "prettier" - ], - "rules": { - "prettier/prettier": [ - "error", - { - "endOfLine": "auto" - } - ] - } + "@commitlint/config-conventional" + ] }, - "eslintIgnore": [ - "build" - ], - "prettier": { - "trailingComma": "es5", - "semi": false, - "singleQuote": true, - "useTabs": false, - "quoteProps": "consistent", - "bracketSpacing": true, - "arrowParens": "always", - "printWidth": 100 + "c8": { + "reporter": [ + "text", + "html" + ], + "exclude": [ + "tests/**", + "src/drivers/**", + "src/abstract_drivers/**", + "stubs/**" + ] } } From 8d822c8f48dc5299cedb2fbf23bf112bc9ce0229 Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 21 Jul 2023 11:51:47 +0200 Subject: [PATCH 009/112] style: lint files --- factories/session_manager_factory.ts | 14 +++--- package.json | 74 ++++++++++++++-------------- src/Drivers/Cookie.ts | 8 +-- src/Drivers/File.ts | 10 ++-- src/Drivers/Memory.ts | 10 ++-- src/Drivers/Redis.ts | 8 +-- src/bindings/api_client.ts | 2 +- src/client.ts | 10 ++-- src/session.ts | 53 ++++++++++---------- src/session_manager.ts | 8 +-- src/session_middleware.ts | 42 ++++++++-------- src/store.ts | 30 +++++------ test/cookie_driver.spec.ts | 2 +- test/file_driver.spec.ts | 4 +- test/session.spec.ts | 2 +- test/session_client.spec.ts | 32 +++++++----- test/session_manager.spec.ts | 10 ++-- test/session_provider.spec.ts | 2 +- test_helpers/index.ts | 2 +- 19 files changed, 163 insertions(+), 160 deletions(-) diff --git a/factories/session_manager_factory.ts b/factories/session_manager_factory.ts index bb50fad..688ddeb 100644 --- a/factories/session_manager_factory.ts +++ b/factories/session_manager_factory.ts @@ -36,7 +36,7 @@ export class SessionManagerFactory { */ #redisManagerOptions = { connection: 'local', - connections: { local: { host: '127.0.0.1', port: 6379, } } + connections: { local: { host: '127.0.0.1', port: 6379 } }, } as const /** @@ -51,10 +51,9 @@ export class SessionManagerFactory { * Merge redis manager parameters */ mergeRedisManagerOptions>(options: { - connection: keyof Connections, - connections: Connections, - } - ) { + connection: keyof Connections + connections: Connections + }) { this.#redisManagerOptions = Object.assign(this.#redisManagerOptions, options) return this } @@ -63,9 +62,10 @@ export class SessionManagerFactory { * Create Session manager instance */ create(app: Application) { - return new SessionManager(this.#options, + return new SessionManager( + this.#options, new EncryptionFactory().create(), - new RedisManagerFactory(this.#redisManagerOptions).create(app), + new RedisManagerFactory(this.#redisManagerOptions).create(app) ) } } diff --git a/package.json b/package.json index ed322c0..c4d8408 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@adonisjs/session", - "version": "6.4.0", "description": "Session provider for AdonisJS", + "version": "6.4.0", "main": "build/index.js", "type": "module", "files": [ @@ -13,13 +13,21 @@ "build/configure.d.ts", "build/configure.js" ], - "dependencies": { - "@poppinss/utils": "6.5.0-3", - "fs-extra": "^11.1.1" - }, - "peerDependencies": { - "@adonisjs/core": "6.1.5-8", - "@adonisjs/redis": "8.0.0-1" + "scripts": { + "pretest": "npm run lint", + "test": "c8 npm run quick:test", + "clean": "del-cli build", + "typecheck": "tsc --noEmit", + "copy:templates": "copyfiles \"stubs/**/*.stub\" build", + "compile": "npm run lint && npm run clean && tsc", + "build": "npm run compile && npm run copy:templates", + "prepublishOnly": "npm run build", + "lint": "eslint . --ext=.ts", + "format": "prettier --write .", + "release": "np", + "version": "npm run build", + "sync-labels": "github-label-sync --labels .github/labels.json adonisjs/ally", + "quick:test": "node --enable-source-maps --loader=ts-node/esm bin/test.ts" }, "devDependencies": { "@adonisjs/application": "7.1.2-8", @@ -52,36 +60,37 @@ "ts-node": "^10.9.1", "typescript": "^5.1.6" }, - "scripts": { - "pretest": "npm run lint", - "test": "c8 npm run quick:test", - "clean": "del-cli build", - "typecheck": "tsc --noEmit", - "copy:templates": "copyfiles \"stubs/**/*.stub\" build", - "compile": "npm run lint && npm run clean && tsc", - "build": "npm run compile && npm run copy:templates", - "prepublishOnly": "npm run build", - "lint": "eslint . --ext=.ts", - "format": "prettier --write .", - "release": "np", - "version": "npm run build", - "sync-labels": "github-label-sync --labels .github/labels.json adonisjs/ally", - "quick:test": "node --enable-source-maps --loader=ts-node/esm bin/test.ts" + "dependencies": { + "@poppinss/utils": "6.5.0-3", + "fs-extra": "^11.1.1" }, + "peerDependencies": { + "@adonisjs/core": "6.1.5-8", + "@adonisjs/redis": "8.0.0-1" + }, + "author": "virk,adonisjs", + "license": "MIT", + "homepage": "https://github.com/adonisjs/adonis-session#readme", "repository": { "type": "git", "url": "git+https://github.com/adonisjs/adonis-session.git" }, + "bugs": { + "url": "https://github.com/adonisjs/adonis-session/issues" + }, "keywords": [ "session", "adonisjs" ], - "author": "virk,adonisjs", - "license": "MIT", - "bugs": { - "url": "https://github.com/adonisjs/adonis-session/issues" + "eslintConfig": { + "extends": "@adonisjs/eslint-config/package" + }, + "prettier": "@adonisjs/prettier-config", + "commitlint": { + "extends": [ + "@commitlint/config-conventional" + ] }, - "homepage": "https://github.com/adonisjs/adonis-session#readme", "publishConfig": { "access": "public", "tag": "next" @@ -92,15 +101,6 @@ "branch": "main", "anyBranch": false }, - "prettier": "@adonisjs/prettier-config", - "eslintConfig": { - "extends": "@adonisjs/eslint-config/package" - }, - "commitlint": { - "extends": [ - "@commitlint/config-conventional" - ] - }, "c8": { "reporter": [ "text", diff --git a/src/Drivers/Cookie.ts b/src/Drivers/Cookie.ts index 32680d9..d1b60f8 100644 --- a/src/Drivers/Cookie.ts +++ b/src/Drivers/Cookie.ts @@ -25,7 +25,7 @@ export class CookieDriver implements SessionDriverContract { /** * Read session value from the cookie */ - public read(sessionId: string): { [key: string]: any } | null { + read(sessionId: string): { [key: string]: any } | null { const cookieValue = this.#ctx.request.encryptedCookie(sessionId) if (typeof cookieValue !== 'object') { return null @@ -36,7 +36,7 @@ export class CookieDriver implements SessionDriverContract { /** * Write session values to the cookie */ - public write(sessionId: string, values: { [key: string]: any }): void { + write(sessionId: string, values: { [key: string]: any }): void { if (typeof values !== 'object') { throw new Error('Session cookie driver expects an object of values') } @@ -47,7 +47,7 @@ export class CookieDriver implements SessionDriverContract { /** * Removes the session cookie */ - public destroy(sessionId: string): void { + destroy(sessionId: string): void { if (this.#ctx.request.cookiesList()[sessionId]) { this.#ctx.response.clearCookie(sessionId) } @@ -56,7 +56,7 @@ export class CookieDriver implements SessionDriverContract { /** * Updates the cookie with existing cookie values */ - public touch(sessionId: string): void { + touch(sessionId: string): void { const value = this.read(sessionId) if (!value) { return diff --git a/src/Drivers/File.ts b/src/Drivers/File.ts index 11ff5ea..0a55763 100644 --- a/src/Drivers/File.ts +++ b/src/Drivers/File.ts @@ -7,7 +7,7 @@ * file that was distributed with this source code. */ -import { join } from 'path' +import { join } from 'node:path' import { Exception } from '@poppinss/utils' import { MessageBuilder } from '@poppinss/utils' import { ensureFile, outputFile, remove } from 'fs-extra/esm' @@ -42,7 +42,7 @@ export class FileDriver implements SessionDriverContract { * Returns file contents. A new file will be created if it's * missing. */ - public async read(sessionId: string): Promise<{ [key: string]: any } | null> { + async read(sessionId: string): Promise<{ [key: string]: any } | null> { const filePath = this.#getFilePath(sessionId) await ensureFile(filePath) @@ -65,7 +65,7 @@ export class FileDriver implements SessionDriverContract { /** * Write session values to a file */ - public async write(sessionId: string, values: { [key: string]: any }): Promise { + async write(sessionId: string, values: { [key: string]: any }): Promise { if (typeof values !== 'object') { throw new Error('Session file driver expects an object of values') } @@ -77,14 +77,14 @@ export class FileDriver implements SessionDriverContract { /** * Cleanup session file by removing it */ - public async destroy(sessionId: string): Promise { + async destroy(sessionId: string): Promise { await remove(this.#getFilePath(sessionId)) } /** * Writes the value by reading it from the store */ - public async touch(sessionId: string): Promise { + async touch(sessionId: string): Promise { const value = await this.read(sessionId) if (!value) { return diff --git a/src/Drivers/Memory.ts b/src/Drivers/Memory.ts index ef04bdf..3209d0b 100644 --- a/src/Drivers/Memory.ts +++ b/src/Drivers/Memory.ts @@ -13,19 +13,19 @@ import { SessionDriverContract } from '../types.js' * Memory driver is meant to be used for writing tests. */ export class MemoryDriver implements SessionDriverContract { - public static sessions: Map = new Map() + static sessions: Map = new Map() /** * Read session id value from the memory */ - public read(sessionId: string): { [key: string]: any } | null { + read(sessionId: string): { [key: string]: any } | null { return MemoryDriver.sessions.get(sessionId) || null } /** * Save in memory value for a given session id */ - public write(sessionId: string, values: { [key: string]: any }): void { + write(sessionId: string, values: { [key: string]: any }): void { if (typeof values !== 'object') { throw new Error('Session memory driver expects an object of values') } @@ -36,9 +36,9 @@ export class MemoryDriver implements SessionDriverContract { /** * Cleanup for a single session */ - public destroy(sessionId: string): void { + destroy(sessionId: string): void { MemoryDriver.sessions.delete(sessionId) } - public touch(): void {} + touch(): void {} } diff --git a/src/Drivers/Redis.ts b/src/Drivers/Redis.ts index 9e4450f..a4f481d 100644 --- a/src/Drivers/Redis.ts +++ b/src/Drivers/Redis.ts @@ -53,7 +53,7 @@ export class RedisDriver implements SessionDriverContract { * Returns file contents. A new file will be created if it's * missing. */ - public async read(sessionId: string): Promise<{ [key: string]: any } | null> { + async read(sessionId: string): Promise<{ [key: string]: any } | null> { const contents = await this.#getRedisConnection().get(sessionId) if (!contents) { return null @@ -70,7 +70,7 @@ export class RedisDriver implements SessionDriverContract { /** * Write session values to a file */ - public async write(sessionId: string, values: Object): Promise { + async write(sessionId: string, values: Object): Promise { if (typeof values !== 'object') { throw new Error('Session file driver expects an object of values') } @@ -85,14 +85,14 @@ export class RedisDriver implements SessionDriverContract { /** * Cleanup session file by removing it */ - public async destroy(sessionId: string): Promise { + async destroy(sessionId: string): Promise { await this.#getRedisConnection().del(sessionId) } /** * Updates the value expiry */ - public async touch(sessionId: string): Promise { + async touch(sessionId: string): Promise { await this.#getRedisConnection().expire(sessionId, this.#ttl) } } diff --git a/src/bindings/api_client.ts b/src/bindings/api_client.ts index 719b8b4..40442ed 100644 --- a/src/bindings/api_client.ts +++ b/src/bindings/api_client.ts @@ -8,7 +8,7 @@ */ import { ApiRequest, ApiResponse, ApiClient } from '@japa/api-client' -import { InspectOptions, inspect } from 'util' +import { InspectOptions, inspect } from 'node:util' import { SessionManager } from '../session_manager.js' import { AllowedSessionValues } from '../types.js' diff --git a/src/client.ts b/src/client.ts index b0cfb97..54ffdcf 100644 --- a/src/client.ts +++ b/src/client.ts @@ -46,7 +46,7 @@ export class SessionClient extends Store { * Flash messages store. They are merged with the session data during * commit */ - public flashMessages = new Store({}) + flashMessages = new Store({}) constructor( config: SessionConfig, @@ -64,14 +64,14 @@ export class SessionClient extends Store { /** * Find if the sessions are enabled */ - public isEnabled() { + isEnabled() { return this.#config.enabled } /** * Load session from the driver */ - public async load(cookies: Record) { + async load(cookies: Record) { const sessionIdCookie = cookies[this.#config.cookieName] const sessionId = sessionIdCookie ? sessionIdCookie.value : this.sessionId @@ -90,7 +90,7 @@ export class SessionClient extends Store { * the session id and cookie name for it to be accessible * by the server */ - public async commit() { + async commit() { this.set(this.#flashMessagesKey, this.flashMessages.all()) await this.#driver.write(this.sessionId, this.toJSON()) @@ -110,7 +110,7 @@ export class SessionClient extends Store { /** * Clear the session store */ - public async forget() { + async forget() { /** * Clear from the session client memory */ diff --git a/src/session.ts b/src/session.ts index 948c5b4..0858345 100644 --- a/src/session.ts +++ b/src/session.ts @@ -61,31 +61,31 @@ export class Session { /** * Set to true inside the `initiate` method */ - public initiated = false + initiated = false /** * A boolean to know if it's a fresh session or not. Fresh * sessions are those, whose session id is not present * in cookie */ - public fresh = false + fresh = false /** * A boolean to know if store is initiated in readonly mode * or not. This is done during Websocket requests */ - public readonly = false + readonly = false /** * Session id for the given request. A new session id is only * generated when the cookie for the session id is missing */ - public sessionId: string + sessionId: string /** * A copy of previously set flash messages */ - public flashMessages = new Store({}) + flashMessages = new Store({}) /** * A copy of flash messages. The `input` messages @@ -94,7 +94,7 @@ export class Session { * * The `others` object is expanded with each call. */ - public responseFlashMessages = new Store({}) + responseFlashMessages = new Store({}) constructor(ctx: HttpContext, config: SessionConfig, driver: SessionDriverContract) { this.#ctx = ctx @@ -216,7 +216,7 @@ export class Session { * * Multiple calls to `initiate` results in a noop. */ - public async initiate(readonly: boolean): Promise { + async initiate(readonly: boolean): Promise { if (this.initiated) { return } @@ -235,7 +235,7 @@ export class Session { * Re-generates the session id. This can is used to avoid * session fixation attacks. */ - public regenerate(): void { + regenerate(): void { this.#ctx.logger.trace('explicitly re-generating session id') this.sessionId = cuid() this.#regeneratedSessionId = true @@ -244,7 +244,7 @@ export class Session { /** * Set/update session value */ - public put(key: string, value: AllowedSessionValues): void { + put(key: string, value: AllowedSessionValues): void { this.#ensureIsReady() this.#ensureIsMutable() this.#store.set(key, value) @@ -253,7 +253,7 @@ export class Session { /** * Find if the value exists in the session */ - public has(key: string): boolean { + has(key: string): boolean { this.#ensureIsReady() return this.#store.has(key) } @@ -262,7 +262,7 @@ export class Session { * Get value from the session. The default value is returned * when actual value is `undefined` */ - public get(key: string, defaultValue?: any): any { + get(key: string, defaultValue?: any): any { this.#ensureIsReady() return this.#store.get(key, defaultValue) } @@ -270,7 +270,7 @@ export class Session { /** * Returns everything from the session */ - public all(): any { + all(): any { this.#ensureIsReady() return this.#store.all() } @@ -278,7 +278,7 @@ export class Session { /** * Remove value for a given key from the session */ - public forget(key: string): void { + forget(key: string): void { this.#ensureIsReady() this.#ensureIsMutable() this.#store.unset(key) @@ -288,7 +288,7 @@ export class Session { * The method is equivalent to calling `session.get` followed * by `session.forget` */ - public pull(key: string, defaultValue?: any): any { + pull(key: string, defaultValue?: any): any { this.#ensureIsReady() this.#ensureIsMutable() return this.#store.pull(key, defaultValue) @@ -299,7 +299,7 @@ export class Session { * method raises an error when underlying value is not * a number */ - public increment(key: string, steps: number = 1): void { + increment(key: string, steps: number = 1): void { this.#ensureIsReady() this.#ensureIsMutable() this.#store.increment(key, steps) @@ -310,7 +310,7 @@ export class Session { * method raises an error when underlying value is not * a number */ - public decrement(key: string, steps: number = 1): void { + decrement(key: string, steps: number = 1): void { this.#ensureIsReady() this.#ensureIsMutable() this.#store.decrement(key, steps) @@ -319,7 +319,7 @@ export class Session { /** * Remove everything from the session */ - public clear(): void { + clear(): void { this.#ensureIsReady() this.#ensureIsMutable() this.#store.clear() @@ -328,10 +328,7 @@ export class Session { /** * Add a new flash message */ - public flash( - key: string | { [key: string]: AllowedSessionValues }, - value?: AllowedSessionValues - ): void { + flash(key: string | { [key: string]: AllowedSessionValues }, value?: AllowedSessionValues): void { this.#ensureIsReady() this.#ensureIsMutable() @@ -350,7 +347,7 @@ export class Session { /** * Flash all form values */ - public flashAll(): void { + flashAll(): void { this.#ensureIsReady() this.#ensureIsMutable() this.responseFlashMessages.set('input', this.#ctx.request.original()) @@ -359,7 +356,7 @@ export class Session { /** * Flash all form values except mentioned keys */ - public flashExcept(keys: string[]): void { + flashExcept(keys: string[]): void { this.#ensureIsReady() this.#ensureIsMutable() this.responseFlashMessages.set('input', lodash.omit(this.#ctx.request.original(), keys)) @@ -368,7 +365,7 @@ export class Session { /** * Flash only defined keys from the form values */ - public flashOnly(keys: string[]): void { + flashOnly(keys: string[]): void { this.#ensureIsReady() this.#ensureIsMutable() this.responseFlashMessages.set('input', lodash.pick(this.#ctx.request.original(), keys)) @@ -377,14 +374,14 @@ export class Session { /** * Reflash existing flash messages */ - public reflash() { + reflash() { this.flash(this.flashMessages.all()) } /** * Reflash selected keys from the existing flash messages */ - public reflashOnly(keys: string[]) { + reflashOnly(keys: string[]) { this.flash(lodash.pick(this.flashMessages.all(), keys)) } @@ -392,14 +389,14 @@ export class Session { * Omit selected keys from the existing flash messages * and flash the rest of values */ - public reflashExcept(keys: string[]) { + reflashExcept(keys: string[]) { this.flash(lodash.omit(this.flashMessages.all(), keys)) } /** * Writes value to the underlying session driver. */ - public async commit(): Promise { + async commit(): Promise { if (!this.initiated) { console.log('session not initiated') this.#touchSessionCookie() diff --git a/src/session_manager.ts b/src/session_manager.ts index 3cdc549..5aea9cf 100644 --- a/src/session_manager.ts +++ b/src/session_manager.ts @@ -158,14 +158,14 @@ export class SessionManager { /** * Find if the sessions are enabled */ - public isEnabled() { + isEnabled() { return this.#config.enabled } /** * Creates an instance of the session client */ - public client() { + client() { const cookieClient = new CookieClient(this.#encryption) return new SessionClient(this.#config, this.#createMemoryDriver(), cookieClient, {}) @@ -174,14 +174,14 @@ export class SessionManager { /** * Creates a new session instance for a given HTTP request */ - public create(ctx: HttpContext) { + create(ctx: HttpContext) { return new Session(ctx, this.#config, this.#createDriver(ctx)) } /** * Extend the drivers list by adding a new one. */ - public extend(driver: string, callback: ExtendCallback): void { + extend(driver: string, callback: ExtendCallback): void { this.#extendedDrivers.set(driver, callback) } } diff --git a/src/session_middleware.ts b/src/session_middleware.ts index 2d5abed..1397689 100644 --- a/src/session_middleware.ts +++ b/src/session_middleware.ts @@ -1,27 +1,27 @@ -import type { HttpContext } from "@adonisjs/core/http"; -import type { NextFn } from "@adonisjs/core/types/http"; -import { SessionManager } from "./session_manager.js"; +import type { HttpContext } from '@adonisjs/core/http' +import type { NextFn } from '@adonisjs/core/types/http' +import { SessionManager } from './session_manager.js' export default class SessionMiddleware { - async handle(ctx: HttpContext, next: NextFn) { - const sessionManager = await ctx.containerResolver.make('session') as SessionManager - if (!sessionManager.isEnabled()) { - return - } + async handle(ctx: HttpContext, next: NextFn) { + const sessionManager = (await ctx.containerResolver.make('session')) as SessionManager + if (!sessionManager.isEnabled()) { + return + } - /** - * Initiate session store - */ - await ctx.session.initiate(false) + /** + * Initiate session store + */ + await ctx.session.initiate(false) - /** - * Call next middlewares or route handler - */ - await next() + /** + * Call next middlewares or route handler + */ + await next() - /** - * Commit store mutations - */ - await ctx.session.commit() - } + /** + * Commit store mutations + */ + await ctx.session.commit() + } } diff --git a/src/store.ts b/src/store.ts index 8d4c6ff..079ae57 100644 --- a/src/store.ts +++ b/src/store.ts @@ -28,35 +28,35 @@ export class Store { /** * Find if store is empty or not */ - public get isEmpty() { + get isEmpty() { return !this.#values || Object.keys(this.#values).length === 0 } /** * Set key/value pair */ - public set(key: string, value: AllowedSessionValues): void { + set(key: string, value: AllowedSessionValues): void { lodash.set(this.#values, key, value) } /** * Get value for a given key */ - public get(key: string, defaultValue?: any): any { + get(key: string, defaultValue?: any): any { return lodash.get(this.#values, key, defaultValue) } /** * Remove key */ - public unset(key: string): void { + unset(key: string): void { lodash.unset(this.#values, key) } /** * Reset store by clearing it's values. */ - public clear(): void { + clear(): void { this.update({}) } @@ -64,7 +64,7 @@ export class Store { * Pull value from the store. It is same as calling * store.get and then store.unset */ - public pull(key: string, defaultValue?: any): any { + pull(key: string, defaultValue?: any): any { return ((value): any => { this.unset(key) return value @@ -75,7 +75,7 @@ export class Store { * Increment number. The method raises an error when * nderlying value is not a number */ - public increment(key: string, steps: number = 1): void { + increment(key: string, steps: number = 1): void { const value = this.get(key, 0) if (typeof value !== 'number') { throw new Exception(`Cannot increment "${key}", since original value is not a number`) @@ -88,7 +88,7 @@ export class Store { * Increment number. The method raises an error when * nderlying value is not a number */ - public decrement(key: string, steps: number = 1): void { + decrement(key: string, steps: number = 1): void { const value = this.get(key, 0) if (typeof value !== 'number') { throw new Exception(`Cannot increment "${key}", since original value is not a number`) @@ -100,14 +100,14 @@ export class Store { /** * Overwrite the underlying values object */ - public update(values: { [key: string]: any }): void { + update(values: { [key: string]: any }): void { this.#values = values } /** * Update to merge values */ - public merge(values: { [key: string]: any }): any { + merge(values: { [key: string]: any }): any { lodash.merge(this.#values, values) } @@ -115,7 +115,7 @@ export class Store { * A boolean to know if value exists. Extra guards to check * arrays for it's length as well. */ - public has(key: string, checkForArraysLength: boolean = true): boolean { + has(key: string, checkForArraysLength: boolean = true): boolean { const value = this.get(key) if (!Array.isArray(value)) { return !!value @@ -127,28 +127,28 @@ export class Store { /** * Get all values */ - public all(): any { + all(): any { return this.#values } /** * Returns object representation of values */ - public toObject() { + toObject() { return this.all() } /** * Returns the store values */ - public toJSON(): any { + toJSON(): any { return this.all() } /** * Returns string representation of the store */ - public toString() { + toString() { return JSON.stringify(this.all()) } } diff --git a/test/cookie_driver.spec.ts b/test/cookie_driver.spec.ts index 4989664..13606af 100644 --- a/test/cookie_driver.spec.ts +++ b/test/cookie_driver.spec.ts @@ -9,7 +9,7 @@ import { test } from '@japa/runner' import supertest from 'supertest' -import { createServer } from 'http' +import { createServer } from 'node:http' import { CookieDriver } from '../src/drivers/cookie.js' import { diff --git a/test/file_driver.spec.ts b/test/file_driver.spec.ts index 0a9b86a..3027f50 100644 --- a/test/file_driver.spec.ts +++ b/test/file_driver.spec.ts @@ -8,10 +8,10 @@ */ import { test } from '@japa/runner' -import { join } from 'path' +import { join } from 'node:path' import { FileDriver } from '../src/drivers/file.js' import { sleep, sessionConfig, BASE_URL } from '../test_helpers/index.js' -import { fileURLToPath } from 'url' +import { fileURLToPath } from 'node:url' const config = Object.assign({}, sessionConfig, { driver: 'file', diff --git a/test/session.spec.ts b/test/session.spec.ts index 4c3cd44..c547351 100644 --- a/test/session.spec.ts +++ b/test/session.spec.ts @@ -9,7 +9,7 @@ import { test } from '@japa/runner' import supertest from 'supertest' -import { createServer } from 'http' +import { createServer } from 'node:http' import { Store } from '../src/store.js' import { Session } from '../src/session.js' diff --git a/test/session_client.spec.ts b/test/session_client.spec.ts index 0f52765..909cade 100644 --- a/test/session_client.spec.ts +++ b/test/session_client.spec.ts @@ -9,7 +9,7 @@ import { test } from '@japa/runner' import supertest from 'supertest' -import { createServer } from 'http' +import { createServer } from 'node:http' import setCookieParser from 'set-cookie-parser' import { MemoryDriver } from '../src/drivers/memory.js' @@ -120,12 +120,15 @@ test.group('Session Client', (group) => { const cookieClient = new CookieClient(await app.container.make('encryption')) const cookies = setCookieParser.parse(response.header['set-cookie'], { map: true }) - const parsedCookies = Object.keys(cookies).reduce((result, key) => { - const value = cookies[key] - value.value = cookieClient.parse(value.name, value.value) - result[key] = value - return result - }, {} as Record) + const parsedCookies = Object.keys(cookies).reduce( + (result, key) => { + const value = cookies[key] + value.value = cookieClient.parse(value.name, value.value) + result[key] = value + return result + }, + {} as Record + ) const { session, flashMessages } = await client.load(parsedCookies) @@ -160,12 +163,15 @@ test.group('Session Client', (group) => { const cookieClient = new CookieClient(await app.container.make('encryption')) const cookies = setCookieParser.parse(response.header['set-cookie'], { map: true }) - const parsedCookies = Object.keys(cookies).reduce((result, key) => { - const value = cookies[key] - value.value = cookieClient.parse(value.name, value.value) - result[key] = value - return result - }, {} as Record) + const parsedCookies = Object.keys(cookies).reduce( + (result, key) => { + const value = cookies[key] + value.value = cookieClient.parse(value.name, value.value) + result[key] = value + return result + }, + {} as Record + ) const { session, flashMessages } = await client.load(parsedCookies) diff --git a/test/session_manager.spec.ts b/test/session_manager.spec.ts index b392ae8..d7192ba 100644 --- a/test/session_manager.spec.ts +++ b/test/session_manager.spec.ts @@ -9,7 +9,7 @@ import { test } from '@japa/runner' import supertest from 'supertest' -import { createServer } from 'http' +import { createServer } from 'node:http' import { MessageBuilder } from '@poppinss/utils' import setCookieParser from 'set-cookie-parser' import { HttpContextFactory } from '@adonisjs/core/factories/http' @@ -148,14 +148,14 @@ test.group('Session Manager', () => { }) class MongoDriver implements SessionDriverContract { - public read() { + read() { return {} } - public write(_: string, data: any) { + write(_: string, data: any) { assert.deepEqual(data, { name: 'virk' }) } - public touch() {} - public destroy() {} + touch() {} + destroy() {} } const sessionManager = await app.container.make('session') diff --git a/test/session_provider.spec.ts b/test/session_provider.spec.ts index 244506a..710d8bf 100644 --- a/test/session_provider.spec.ts +++ b/test/session_provider.spec.ts @@ -8,7 +8,7 @@ */ import { test } from '@japa/runner' -import { createServer } from 'http' +import { createServer } from 'node:http' import { ApiClient, ApiRequest } from '@japa/api-client' import { createHttpContext, setup } from '../test_helpers/index.js' import { SessionManager } from '../src/session_manager.js' diff --git a/test_helpers/index.ts b/test_helpers/index.ts index 48d9466..d91356d 100644 --- a/test_helpers/index.ts +++ b/test_helpers/index.ts @@ -14,7 +14,7 @@ import { ApplicationService } from '@adonisjs/core/types' import { RedisService } from '@adonisjs/redis/types' import { RedisManagerFactory } from '@adonisjs/redis/factories' import { Application } from '@adonisjs/core/app' -import { IncomingMessage, ServerResponse } from 'http' +import { IncomingMessage, ServerResponse } from 'node:http' import { Server } from '@adonisjs/core/http' import { pluginAdonisJS } from '@japa/plugin-adonisjs' import { Runner } from '@japa/runner/core' From 32adf4a59f5240dddb90c898ffac70e7c4f4cf64 Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 21 Jul 2023 11:52:16 +0200 Subject: [PATCH 010/112] chore: add adonisjs/redis as optional peer dep --- package.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/package.json b/package.json index c4d8408..fe34629 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,11 @@ "@adonisjs/core": "6.1.5-8", "@adonisjs/redis": "8.0.0-1" }, + "peerDependenciesMeta": { + "@adonisjs/redis": { + "optional": true + } + }, "author": "virk,adonisjs", "license": "MIT", "homepage": "https://github.com/adonisjs/adonis-session#readme", From 27b8d49934f21b7d0d16945525d5956db3ea6d8f Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 21 Jul 2023 11:52:39 +0200 Subject: [PATCH 011/112] chore: update c8 config --- package.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/package.json b/package.json index fe34629..86b49c7 100644 --- a/package.json +++ b/package.json @@ -113,8 +113,6 @@ ], "exclude": [ "tests/**", - "src/drivers/**", - "src/abstract_drivers/**", "stubs/**" ] } From 7ef28d61b563305584a36621b0255eab9f69a9a2 Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 21 Jul 2023 11:53:40 +0200 Subject: [PATCH 012/112] chore: update .github config --- .github/COMMIT_CONVENTION.md | 70 --------- .github/CONTRIBUTING.md | 46 ------ .github/ISSUE_TEMPLATE/bug_report.md | 29 ---- .github/ISSUE_TEMPLATE/feature_request.md | 28 ---- .github/PULL_REQUEST_TEMPLATE.md | 28 ---- .github/labels.json | 170 ++++++++++++++++++++++ .github/stale.yml | 4 +- 7 files changed, 172 insertions(+), 203 deletions(-) delete mode 100644 .github/COMMIT_CONVENTION.md delete mode 100644 .github/CONTRIBUTING.md delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md delete mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/labels.json diff --git a/.github/COMMIT_CONVENTION.md b/.github/COMMIT_CONVENTION.md deleted file mode 100644 index fc852af..0000000 --- a/.github/COMMIT_CONVENTION.md +++ /dev/null @@ -1,70 +0,0 @@ -## Git Commit Message Convention - -> This is adapted from [Angular's commit convention](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular). - -Using conventional commit messages, we can automate the process of generating the CHANGELOG file. All commits messages will automatically be validated against the following regex. - -``` js -/^(revert: )?(feat|fix|docs|style|refactor|perf|test|workflow|ci|chore|types|build|improvement)((.+))?: .{1,50}/ -``` - -## Commit Message Format -A commit message consists of a **header**, **body** and **footer**. The header has a **type**, **scope** and **subject**: - -> The **scope** is optional - -``` -feat(router): add support for prefix - -Prefix makes it easier to append a path to a group of routes -``` - -1. `feat` is type. -2. `router` is scope and is optional -3. `add support for prefix` is the subject -4. The **body** is followed by a blank line. -5. The optional **footer** can be added after the body, followed by a blank line. - -## Types -Only one type can be used at a time and only following types are allowed. - -- feat -- fix -- docs -- style -- refactor -- perf -- test -- workflow -- ci -- chore -- types -- build - -If a type is `feat`, `fix` or `perf`, then the commit will appear in the CHANGELOG.md file. However if there is any BREAKING CHANGE, the commit will always appear in the changelog. - -### Revert -If the commit reverts a previous commit, it should begin with `revert:`, followed by the header of the reverted commit. In the body it should say: `This reverts commit `., where the hash is the SHA of the commit being reverted. - -## Scope -The scope could be anything specifying place of the commit change. For example: `router`, `view`, `querybuilder`, `database`, `model` and so on. - -## Subject -The subject contains succinct description of the change: - -- use the imperative, present tense: "change" not "changed" nor "changes". -- don't capitalize first letter -- no dot (.) at the end - -## Body - -Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes". -The body should include the motivation for the change and contrast this with previous behavior. - -## Footer - -The footer should contain any information about **Breaking Changes** and is also the place to -reference GitHub issues that this commit **Closes**. - -**Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this. - diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md deleted file mode 100644 index f0c5446..0000000 --- a/.github/CONTRIBUTING.md +++ /dev/null @@ -1,46 +0,0 @@ -# Contributing - -AdonisJS is a community driven project. You are free to contribute in any of the following ways. - -- [Coding style](coding-style) -- [Fix bugs by creating PR's](fix-bugs-by-creating-prs) -- [Share an RFC for new features or big changes](share-an-rfc-for-new-features-or-big-changes) -- [Report security issues](report-security-issues) -- [Be a part of the community](be-a-part-of-community) - -## Coding style - -Majority of AdonisJS core packages are written in Typescript. Having a brief knowledge of Typescript is required to contribute to the core. - -## Fix bugs by creating PR's - -We appreciate every time you report a bug in the framework or related libraries. However, taking time to submit a PR can help us in fixing bugs quickly and ensure a healthy and stable eco-system. - -Go through the following points, before creating a new PR. - -1. Create an issue discussing the bug or short-coming in the framework. -2. Once approved, go ahead and fork the REPO. -3. Make sure to start from the `develop`, since this is the upto date branch. -4. Make sure to keep commits small and relevant. -5. We follow [conventional-commits](https://github.com/conventional-changelog/conventional-changelog) to structure our commit messages. Instead of running `git commit`, you must run `npm commit`, which will show you prompts to create a valid commit message. -6. Once done with all the changes, create a PR against the `develop` branch. - -## Share an RFC for new features or big changes - -Sharing PR's for small changes works great. However, when contributing big features to the framework, it is required to go through the RFC process. - -### What is an RFC? - -RFC stands for **Request for Commits**, a standard process followed by many other frameworks including [Ember](https://github.com/emberjs/rfcs), [yarn](https://github.com/yarnpkg/rfcs) and [rust](https://github.com/rust-lang/rfcs). - -In brief, RFC process allows you to talk about the changes with everyone in the community and get a view of the core team before dedicating your time to work on the feature. - -The RFC proposals are created as Pull Request on [adonisjs/rfcs](https://github.com/adonisjs/rfcs) repo. Make sure to read the README to learn about the process in depth. - -## Report security issues - -All of the security issues, must be reported via [email](mailto:virk@adonisjs.com) and not using any of the public channels. - -## Be a part of community - -We welcome you to participate in [GitHub Discussion](https://github.com/adonisjs/core/discussions) and the AdonisJS [Discord Server](https://discord.gg/vDcEjq6). You are free to ask your questions and share your work or contributions made to AdonisJS eco-system. diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index e65000c..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -name: Bug report -about: Report identified bugs ---- - - - -## Prerequisites - -We do our best to reply to all the issues on time. If you will follow the given guidelines, the turn around time will be faster. - -- Lots of raised issues are directly not bugs but instead are design decisions taken by us. -- Make use of our [GH discussions](https://github.com/adonisjs/core/discussions), or [discord server](https://discord.me/adonisjs), if you are not sure that you are reporting a bug. -- Ensure the issue isn't already reported. -- Ensure you are reporting the bug in the correct repo. - -*Delete the above section and the instructions in the sections below before submitting* - -## Package version - - -## Node.js and npm version - - -## Sample Code (to reproduce the issue) - - -## BONUS (a sample repo to reproduce the issue) - diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index abd44a5..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -name: Feature request -about: Propose changes for adding a new feature ---- - - - -## Prerequisites - -We do our best to reply to all the issues on time. If you will follow the given guidelines, the turn around time will be faster. - -## Consider an RFC - -Please create an [RFC](https://github.com/adonisjs/rfcs) instead, if - -- Feature introduces a breaking change -- Demands lots of time and changes in the current code base. - -*Delete the above section and the instructions in the sections below before submitting* - -## Why this feature is required (specific use-cases will be appreciated)? - - -## Have you tried any other work arounds? - - -## Are you willing to work on it with little guidance? - diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 044e69e..0000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,28 +0,0 @@ - - -## Proposed changes - -Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. - -## Types of changes - -What types of changes does your code introduce? - -_Put an `x` in the boxes that apply_ - -- [ ] Bugfix (non-breaking change which fixes an issue) -- [ ] New feature (non-breaking change which adds functionality) -- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - -## Checklist - -_Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._ - -- [ ] I have read the [CONTRIBUTING](https://github.com/adonisjs/session/blob/master/.github/CONTRIBUTING.md) doc -- [ ] Lint and unit tests pass locally with my changes -- [ ] I have added tests that prove my fix is effective or that my feature works. -- [ ] I have added necessary documentation (if appropriate) - -## Further comments - -If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... diff --git a/.github/labels.json b/.github/labels.json new file mode 100644 index 0000000..ba001c6 --- /dev/null +++ b/.github/labels.json @@ -0,0 +1,170 @@ +[ + { + "name": "Priority: Critical", + "color": "ea0056", + "description": "The issue needs urgent attention", + "aliases": [] + }, + { + "name": "Priority: High", + "color": "5666ed", + "description": "Look into this issue before picking up any new work", + "aliases": [] + }, + { + "name": "Priority: Medium", + "color": "f4ff61", + "description": "Try to fix the issue for the next patch/minor release", + "aliases": [] + }, + { + "name": "Priority: Low", + "color": "87dfd6", + "description": "Something worth considering, but not a top priority for the team", + "aliases": [] + }, + { + "name": "Semver: Alpha", + "color": "008480", + "description": "Will make it's way to the next alpha version of the package", + "aliases": [] + }, + { + "name": "Semver: Major", + "color": "ea0056", + "description": "Has breaking changes", + "aliases": [] + }, + { + "name": "Semver: Minor", + "color": "fbe555", + "description": "Mainly new features and improvements", + "aliases": [] + }, + { + "name": "Semver: Next", + "color": "5666ed", + "description": "Will make it's way to the bleeding edge version of the package", + "aliases": [] + }, + { + "name": "Semver: Patch", + "color": "87dfd6", + "description": "A bug fix", + "aliases": [] + }, + { + "name": "Status: Abandoned", + "color": "ffffff", + "description": "Dropped and not into consideration", + "aliases": ["wontfix"] + }, + { + "name": "Status: Accepted", + "color": "e5fbf2", + "description": "The proposal or the feature has been accepted for the future versions", + "aliases": [] + }, + { + "name": "Status: Blocked", + "color": "ea0056", + "description": "The work on the issue or the PR is blocked. Check comments for reasoning", + "aliases": [] + }, + { + "name": "Status: Completed", + "color": "008672", + "description": "The work has been completed, but not released yet", + "aliases": [] + }, + { + "name": "Status: In Progress", + "color": "73dbc4", + "description": "Still banging the keyboard", + "aliases": ["in progress"] + }, + { + "name": "Status: On Hold", + "color": "f4ff61", + "description": "The work was started earlier, but is on hold now. Check comments for reasoning", + "aliases": ["On Hold"] + }, + { + "name": "Status: Review Needed", + "color": "fbe555", + "description": "Review from the core team is required before moving forward", + "aliases": [] + }, + { + "name": "Status: Awaiting More Information", + "color": "89f8ce", + "description": "Waiting on the issue reporter or PR author to provide more information", + "aliases": [] + }, + { + "name": "Status: Need Contributors", + "color": "7057ff", + "description": "Looking for contributors to help us move forward with this issue or PR", + "aliases": [] + }, + { + "name": "Type: Bug", + "color": "ea0056", + "description": "The issue has indentified a bug", + "aliases": ["bug"] + }, + { + "name": "Type: Security", + "color": "ea0056", + "description": "Spotted security vulnerability and is a top priority for the core team", + "aliases": [] + }, + { + "name": "Type: Duplicate", + "color": "00837e", + "description": "Already answered or fixed previously", + "aliases": ["duplicate"] + }, + { + "name": "Type: Enhancement", + "color": "89f8ce", + "description": "Improving an existing feature", + "aliases": ["enhancement"] + }, + { + "name": "Type: Feature Request", + "color": "483add", + "description": "Request to add a new feature to the package", + "aliases": [] + }, + { + "name": "Type: Invalid", + "color": "dbdbdb", + "description": "Doesn't really belong here. Maybe use discussion threads?", + "aliases": ["invalid"] + }, + { + "name": "Type: Question", + "color": "eceafc", + "description": "Needs clarification", + "aliases": ["help wanted", "question"] + }, + { + "name": "Type: Documentation Change", + "color": "7057ff", + "description": "Documentation needs some improvements", + "aliases": ["documentation"] + }, + { + "name": "Type: Dependencies Update", + "color": "00837e", + "description": "Bump dependencies", + "aliases": ["dependencies"] + }, + { + "name": "Good First Issue", + "color": "008480", + "description": "Want to contribute? Just filter by this label", + "aliases": ["good first issue"] + } +] diff --git a/.github/stale.yml b/.github/stale.yml index 7a6a571..f767674 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -6,10 +6,10 @@ daysUntilClose: 7 # Issues with these labels will never be considered stale exemptLabels: - - "Type: Security" + - 'Type: Security' # Label to use when marking an issue as stale -staleLabel: "Status: Abandoned" +staleLabel: 'Status: Abandoned' # Comment to post when marking an issue as stale. Set to `false` to disable markComment: > From e9b04c0b2ad3dcd510e8549f558b9637b73035b4 Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 21 Jul 2023 11:53:55 +0200 Subject: [PATCH 013/112] chore: update husky config --- .husky/commit-msg | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.husky/commit-msg b/.husky/commit-msg index 4654c12..988eb59 100755 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -1,3 +1,4 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" -HUSKY_GIT_PARAMS=$1 node ./node_modules/@adonisjs/mrm-preset/validate-commit/conventional/validate.js +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npx --no -- commitlint --edit From 0456b38515e06d4862e6c9588670b72ae1d16aef Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 21 Jul 2023 12:05:59 +0200 Subject: [PATCH 014/112] feat: add configure command --- configure.ts | 20 +++++++++ index.ts | 2 + src/session.ts | 2 + templates/session.txt => stubs/config.stub | 27 ++++++------ stubs/main.ts | 12 ++++++ test/configure.spec.ts | 50 ++++++++++++++++++++++ 6 files changed, 98 insertions(+), 15 deletions(-) create mode 100644 configure.ts rename templates/session.txt => stubs/config.stub (83%) create mode 100644 stubs/main.ts create mode 100644 test/configure.spec.ts diff --git a/configure.ts b/configure.ts new file mode 100644 index 0000000..726853c --- /dev/null +++ b/configure.ts @@ -0,0 +1,20 @@ +/* + * @adonisjs/session + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import type Configure from '@adonisjs/core/commands/configure' + +/** + * Configures the package + */ +export async function configure(command: Configure) { + await command.publishStub('config.stub') + await command.updateRcFile((rcFile) => { + rcFile.addProvider('@adonisjs/session/session_provider') + }) +} diff --git a/index.ts b/index.ts index a9d4008..8ca4826 100644 --- a/index.ts +++ b/index.ts @@ -8,3 +8,5 @@ */ export { defineConfig } from './src/define_config.js' +export { stubsRoot } from './stubs/main.js' +export { configure } from './configure.js' diff --git a/src/session.ts b/src/session.ts index 0858345..33b955b 100644 --- a/src/session.ts +++ b/src/session.ts @@ -196,10 +196,12 @@ export class Session { * (only when view property exists) */ #shareLocalsWithView() { + // @ts-ignore may need to expose ./types/extended from adonisjs/view if (!this.#ctx['view'] || typeof this.#ctx['view'].share !== 'function') { return } + // @ts-ignore may need to expose ./types/extended from adonisjs/view this.#ctx['view'].share({ flashMessages: this.flashMessages, session: { diff --git a/templates/session.txt b/stubs/config.stub similarity index 83% rename from templates/session.txt rename to stubs/config.stub index 3d359cc..0c5ceb6 100644 --- a/templates/session.txt +++ b/stubs/config.stub @@ -1,15 +1,12 @@ -/** - * Config source: https://git.io/JeYHp - * - * Feel free to let us know via PR, if you find something broken in this config - * file. - */ +--- +to: {{ app.configPath('session.ts') }} +--- -import Env from '@ioc:Adonis/Core/Env' -import Application from '@ioc:Adonis/Core/Application' -import { sessionConfig } from '@adonisjs/session/build/config' +import env from '#start/env' +import app from '@adonisjs/core/services/app' +import { defineConfig } from '@adonisjs/session' -export default sessionConfig({ +export default defineConfig({ /* |-------------------------------------------------------------------------- | Enable/Disable sessions @@ -36,7 +33,7 @@ export default sessionConfig({ | Note: Switching drivers will make existing sessions invalid. | */ - driver: Env.get('SESSION_DRIVER'), + driver: env.get('SESSION_DRIVER'), /* |-------------------------------------------------------------------------- @@ -54,7 +51,7 @@ export default sessionConfig({ |-------------------------------------------------------------------------- | | Whether or not you want to destroy the session when browser closes. Setting - | this value to `true` will ignore the `age`. + | this value to "true" will ignore the "age". | */ clearWithBrowser: false, @@ -70,7 +67,7 @@ export default sessionConfig({ | The value can be a number in milliseconds or a string that must be valid | as per https://npmjs.org/package/ms package. | - | Example: `2 days`, `2.5 hrs`, `1y`, `5s` and so on. + | Example: "2 days", "2.5 hrs", "1y", "5s" and so on. | */ age: '2h', @@ -100,7 +97,7 @@ export default sessionConfig({ | */ file: { - location: Application.tmpPath('sessions'), + location: app.tmpPath('sessions'), }, /* @@ -109,7 +106,7 @@ export default sessionConfig({ |-------------------------------------------------------------------------- | | The redis connection you want session driver to use. The same connection - | must be defined inside `config/redis.ts` file as well. + | must be defined inside "config/redis.ts" file as well. | */ redisConnection: 'local', diff --git a/stubs/main.ts b/stubs/main.ts new file mode 100644 index 0000000..cd7dca9 --- /dev/null +++ b/stubs/main.ts @@ -0,0 +1,12 @@ +/* + * @adonisjs/session + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { getDirname } from '@poppinss/utils' + +export const stubsRoot = getDirname(import.meta.url) diff --git a/test/configure.spec.ts b/test/configure.spec.ts new file mode 100644 index 0000000..dd658ac --- /dev/null +++ b/test/configure.spec.ts @@ -0,0 +1,50 @@ +/* + * @adonisjs/session + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { fileURLToPath } from 'node:url' +import { IgnitorFactory } from '@adonisjs/core/factories' +import Configure from '@adonisjs/core/commands/configure' + +const BASE_URL = new URL('./tmp/', import.meta.url) + +test.group('Configure', (group) => { + group.each.setup(({ context }) => { + context.fs.baseUrl = BASE_URL + context.fs.basePath = fileURLToPath(BASE_URL) + }) + + test('create config file and register provider', async ({ assert }) => { + const ignitor = new IgnitorFactory() + .withCoreProviders() + .withCoreConfig() + .create(BASE_URL, { + importer: (filePath) => { + if (filePath.startsWith('./') || filePath.startsWith('../')) { + return import(new URL(filePath, BASE_URL).href) + } + + return import(filePath) + }, + }) + + const app = ignitor.createApp('web') + await app.init() + await app.boot() + + const ace = await app.container.make('ace') + const command = await ace.create(Configure, ['../../index.js']) + await command.exec() + + await assert.fileExists('config/session.ts') + await assert.fileExists('.adonisrc.json') + await assert.fileContains('.adonisrc.json', '@adonisjs/session/session_provider') + await assert.fileContains('config/session.ts', 'defineConfig') + }) +}) From 1744f899bf7d4f3bfb043fb5b3e5622ade3f4b36 Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 21 Jul 2023 12:07:03 +0200 Subject: [PATCH 015/112] chore: update package exports map --- package.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/package.json b/package.json index 86b49c7..d4f67e8 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,11 @@ "build/configure.d.ts", "build/configure.js" ], + "exports": { + ".": "./build/index.js", + "./session_provider": "./build/providers/session_provider.js", + "./types": "./build/src/types.js" + }, "scripts": { "pretest": "npm run lint", "test": "c8 npm run quick:test", From 24ff390d287ddbe970e84696247291688534bfe9 Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 21 Jul 2023 12:07:11 +0200 Subject: [PATCH 016/112] chore: export bindings types from root --- index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/index.ts b/index.ts index 8ca4826..f421a82 100644 --- a/index.ts +++ b/index.ts @@ -7,6 +7,8 @@ * file that was distributed with this source code. */ +import './src/bindings/types.js' + export { defineConfig } from './src/define_config.js' export { stubsRoot } from './stubs/main.js' export { configure } from './configure.js' From 434284378cf3372f1c40ea9d89555aa66ae5db3d Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 21 Jul 2023 13:15:37 +0200 Subject: [PATCH 017/112] chore: add session_middleware export --- configure.ts | 1 + package.json | 1 + 2 files changed, 2 insertions(+) diff --git a/configure.ts b/configure.ts index 726853c..be92083 100644 --- a/configure.ts +++ b/configure.ts @@ -14,6 +14,7 @@ import type Configure from '@adonisjs/core/commands/configure' */ export async function configure(command: Configure) { await command.publishStub('config.stub') + await command.defineEnvVariables({ sSESSION_DRIVER: 'cookie' }) await command.updateRcFile((rcFile) => { rcFile.addProvider('@adonisjs/session/session_provider') }) diff --git a/package.json b/package.json index d4f67e8..7fcc7e9 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "exports": { ".": "./build/index.js", "./session_provider": "./build/providers/session_provider.js", + "./session_middleware": "./build/src/session_middleware.js", "./types": "./build/src/types.js" }, "scripts": { From 3ae1a4310dcae0a9a5a9efbdb4dc1b18e4e27343 Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 21 Jul 2023 14:10:27 +0200 Subject: [PATCH 018/112] chore: add redis insight in docker compose for easier testing --- docker-compose.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 18b513a..561075b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,3 +6,10 @@ services: ports: - 6379:6379 command: redis-server --appendonly yes + + redis-insight: + image: redislabs/redisinsight:latest + ports: + - 8001:8001 + environment: + - REDIS_URI=redis://redis:6379 From d398014ff4655088131b14bd1b70c2012025391f Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 21 Jul 2023 14:11:29 +0200 Subject: [PATCH 019/112] feat: delete sessions with empty content --- src/session.ts | 13 +++++- test/session.spec.ts | 94 ++++++++++++++++++++++++++++++++++++++----- test_helpers/index.ts | 7 +++- 3 files changed, 102 insertions(+), 12 deletions(-) diff --git a/src/session.ts b/src/session.ts index 33b955b..dee9ddd 100644 --- a/src/session.ts +++ b/src/session.ts @@ -400,7 +400,6 @@ export class Session { */ async commit(): Promise { if (!this.initiated) { - console.log('session not initiated') this.#touchSessionCookie() await this.#touchDriver() return @@ -418,6 +417,16 @@ export class Session { */ this.#touchSessionCookie() this.#setFlashMessages() - await this.#commitValuesToStore() + + /** + * Commit values to the store if not empty. + * Otherwise delete the session store to cleanup + * the storage space. + */ + if (!this.#store.isEmpty) { + await this.#commitValuesToStore() + } else { + await this.#driver.destroy(this.sessionId) + } } } diff --git a/test/session.spec.ts b/test/session.spec.ts index c547351..621173d 100644 --- a/test/session.spec.ts +++ b/test/session.spec.ts @@ -8,6 +8,7 @@ */ import { test } from '@japa/runner' +import type { RedisService } from '@adonisjs/redis/types' import supertest from 'supertest' import { createServer } from 'node:http' @@ -755,21 +756,96 @@ test.group('Session | Flash', (group) => { }) }) - test('should not store session if empty', async ({ assert, fs }) => { - const { app } = await setup(fs) + test('should not store session if empty - redis', async ({ assert, fs }) => { + const { app } = await setup(fs, { + redis: { + connection: 'session', + connections: { + session: { + host: process.env.REDIS_HOST || '0.0.0.0', + port: process.env.REDIS_PORT || 6379, + }, + }, + }, + + session: { + driver: 'redis', + cookieName: 'adonis-session', + age: '2h', + redisConnection: 'session', + }, + }) const server = createServer(async (req, res) => { const ctx = await createHttpContext(app, req, res) - const driver = new MemoryDriver() - const session = new Session(ctx, sessionConfig, driver) - await session.initiate(false) + await ctx.session.initiate(false) + await ctx.session.commit() - assert.isTrue(session.fresh) - assert.isTrue(session.initiated) - res.end() + ctx.response.finish() }) - await supertest(server).get('/') + const { header } = await supertest(server).get('/') + const sessionId = await unsignCookie(app, header, 'adonis-session') + const redis = (await app.container.make('redis')) as RedisService + + const sessionValue = await redis.get(sessionId) + assert.isNull(sessionValue) + }) + + test('should delete session storage if empty - redis', async ({ assert, fs }) => { + const { app } = await setup(fs, { + redis: { + connection: 'session', + connections: { + session: { + host: process.env.REDIS_HOST || '0.0.0.0', + port: process.env.REDIS_PORT || 6379, + }, + }, + }, + + session: { + driver: 'redis', + cookieName: 'adonis-session', + age: '2h', + redisConnection: 'session', + }, + }) + + const server = createServer(async (req, res) => { + const ctx = await createHttpContext(app, req, res) + + await ctx.session.initiate(false) + + if (ctx.request.qs().set) { + ctx.session.put('user', { username: 'jul' }) + } + + if (ctx.request.qs().delete) { + ctx.session.forget('user') + } + + await ctx.session.commit() + + ctx.response.finish() + }) + + const { header } = await supertest(server).get('/?set=1') + const sessionId = await unsignCookie(app, header, 'adonis-session') + const redis = (await app.container.make('redis')) as RedisService + + const sessionValue = await redis.get(sessionId) + assert.isNotNull(sessionValue) + + const { header: header2 } = await supertest(server) + .get('/?delete=1') + .set('cookie', await signCookie(app, sessionId, 'adonis-session')) + + const sessionId2 = await unsignCookie(app, header2, 'adonis-session') + assert.equal(sessionId, sessionId2) + + const sessionValue2 = await redis.get(sessionId2) + assert.isNull(sessionValue2) }) }) diff --git a/test_helpers/index.ts b/test_helpers/index.ts index d91356d..972afe6 100644 --- a/test_helpers/index.ts +++ b/test_helpers/index.ts @@ -47,7 +47,12 @@ export async function setup(fs: FileSystem, config?: any) { .withCoreConfig() .withCoreProviders() .merge({ - rcFileContents: { providers: ['../../providers/session_provider.js'] }, + rcFileContents: { + providers: [ + '../../providers/session_provider.js', + '@adonisjs/redis/providers/redis_provider', + ], + }, config: config || { session: sessionConfig }, }) .create(fs.baseUrl, { importer: IMPORTER }) From 40f744f0599480f89599cd044dae2c0cbf79edb0 Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 21 Jul 2023 14:13:47 +0200 Subject: [PATCH 020/112] fix: configure.ts env typo --- configure.ts | 2 +- test/configure.spec.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/configure.ts b/configure.ts index be92083..6f2ef6d 100644 --- a/configure.ts +++ b/configure.ts @@ -14,7 +14,7 @@ import type Configure from '@adonisjs/core/commands/configure' */ export async function configure(command: Configure) { await command.publishStub('config.stub') - await command.defineEnvVariables({ sSESSION_DRIVER: 'cookie' }) + await command.defineEnvVariables({ SESSION_DRIVER: 'cookie' }) await command.updateRcFile((rcFile) => { rcFile.addProvider('@adonisjs/session/session_provider') }) diff --git a/test/configure.spec.ts b/test/configure.spec.ts index dd658ac..c86f3d7 100644 --- a/test/configure.spec.ts +++ b/test/configure.spec.ts @@ -20,7 +20,7 @@ test.group('Configure', (group) => { context.fs.basePath = fileURLToPath(BASE_URL) }) - test('create config file and register provider', async ({ assert }) => { + test('create config file and register provider', async ({ fs, assert }) => { const ignitor = new IgnitorFactory() .withCoreProviders() .withCoreConfig() @@ -34,6 +34,8 @@ test.group('Configure', (group) => { }, }) + await fs.create('.env', '') + const app = ignitor.createApp('web') await app.init() await app.boot() @@ -46,5 +48,6 @@ test.group('Configure', (group) => { await assert.fileExists('.adonisrc.json') await assert.fileContains('.adonisrc.json', '@adonisjs/session/session_provider') await assert.fileContains('config/session.ts', 'defineConfig') + await assert.fileContains('.env', 'SESSION_DRIVER=cookie') }) }) From 925652ba47734c7a509c0919e5f93fde5f769ae7 Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 21 Jul 2023 14:15:42 +0200 Subject: [PATCH 021/112] ci: add lint and typecheck jobs --- .github/workflows/test.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 134f3f2..30cdaea 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,6 +3,12 @@ on: - push - pull_request jobs: + lint: + uses: adonisjs/.github/.github/workflows/lint.yml@main + + typecheck: + uses: adonisjs/.github/.github/workflows/typecheck.yml@main + linux: runs-on: ubuntu-latest services: From 6f78a3e27102b3adc019911b870b098e49085836 Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 21 Jul 2023 14:17:59 +0200 Subject: [PATCH 022/112] refactor: upgrade japa/api-client to latest --- package.json | 2 +- test/session_provider.spec.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 7fcc7e9..11daf46 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "@adonisjs/redis": "8.0.0-1", "@adonisjs/tsconfig": "^1.1.8", "@adonisjs/view": "7.0.0-4", - "@japa/api-client": "^1.4.4", + "@japa/api-client": "2.0.0-0", "@japa/assert": "2.0.0-1", "@japa/file-system": "2.0.0-1", "@japa/plugin-adonisjs": "2.0.0-1", diff --git a/test/session_provider.spec.ts b/test/session_provider.spec.ts index 710d8bf..724b6af 100644 --- a/test/session_provider.spec.ts +++ b/test/session_provider.spec.ts @@ -31,9 +31,9 @@ test.group('Session Provider', (group) => { test('register test api request methods', async ({ assert, fs }) => { await setup(fs) - assert.isTrue(ApiRequest.hasMacro('session')) - assert.isTrue(ApiRequest.hasMacro('flashMessages')) - assert.isTrue(ApiRequest.hasGetter('sessionClient')) + assert.isTrue(ApiRequest.prototype.hasOwnProperty('session')) + assert.isTrue(ApiRequest.prototype.hasOwnProperty('flashMessages')) + assert.isTrue(ApiRequest.prototype.hasOwnProperty('sessionClient')) }) test('set session before making the api request', async ({ fs, assert }) => { From 819993b70bd006009a9898ac348642f30199c11a Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 21 Jul 2023 14:19:12 +0200 Subject: [PATCH 023/112] ci: upgrade node.js versions to test --- .github/workflows/test.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 30cdaea..63ac71d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,6 +11,9 @@ jobs: linux: runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18.16.0, 20.x] services: redis: image: redis @@ -21,11 +24,6 @@ jobs: --health-retries 5 ports: - 6379:6379 - strategy: - matrix: - node-version: - - 14.15.4 - - 17.x steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} From 682fb09870228f4ef74080f6708e5ae5c10370cb Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 21 Jul 2023 14:24:24 +0200 Subject: [PATCH 024/112] chore: fix file casing issues --- src/{Drivers/Cookie.ts => drivers/cookie.ts} | 0 src/{Drivers/File.ts => drivers/file.ts} | 0 src/{Drivers/Memory.ts => drivers/memory.ts} | 0 src/{Drivers/Redis.ts => drivers/redis.ts} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename src/{Drivers/Cookie.ts => drivers/cookie.ts} (100%) rename src/{Drivers/File.ts => drivers/file.ts} (100%) rename src/{Drivers/Memory.ts => drivers/memory.ts} (100%) rename src/{Drivers/Redis.ts => drivers/redis.ts} (100%) diff --git a/src/Drivers/Cookie.ts b/src/drivers/cookie.ts similarity index 100% rename from src/Drivers/Cookie.ts rename to src/drivers/cookie.ts diff --git a/src/Drivers/File.ts b/src/drivers/file.ts similarity index 100% rename from src/Drivers/File.ts rename to src/drivers/file.ts diff --git a/src/Drivers/Memory.ts b/src/drivers/memory.ts similarity index 100% rename from src/Drivers/Memory.ts rename to src/drivers/memory.ts diff --git a/src/Drivers/Redis.ts b/src/drivers/redis.ts similarity index 100% rename from src/Drivers/Redis.ts rename to src/drivers/redis.ts From ff23cde107b1ce65e039b9f0b95862361daec13a Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 21 Jul 2023 14:27:25 +0200 Subject: [PATCH 025/112] fix: add this annotation on ApiClient macros --- src/bindings/api_client.ts | 54 ++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/src/bindings/api_client.ts b/src/bindings/api_client.ts index 40442ed..28c1d49 100644 --- a/src/bindings/api_client.ts +++ b/src/bindings/api_client.ts @@ -27,35 +27,41 @@ export function extendApiClient(sessionManager: SessionManager) { /** * Send session values in the request */ - ApiRequest.macro('session', function (session: Record) { - if (!this.sessionClient.isEnabled()) { - throw new Error('Cannot set session. Make sure to enable it inside "config/session" file') - } + ApiRequest.macro( + 'session', + function (this: ApiRequest, session: Record) { + if (!this.sessionClient.isEnabled()) { + throw new Error('Cannot set session. Make sure to enable it inside "config/session" file') + } - this.sessionClient.merge(session) + this.sessionClient.merge(session) - return this - }) + return this + } + ) /** * Send flash messages in the request */ - ApiRequest.macro('flashMessages', function (messages: Record) { - if (!this.sessionClient.isEnabled()) { - throw new Error( - 'Cannot set flash messages. Make sure to enable the session inside "config/session" file' - ) - } + ApiRequest.macro( + 'flashMessages', + function (this: ApiRequest, messages: Record) { + if (!this.sessionClient.isEnabled()) { + throw new Error( + 'Cannot set flash messages. Make sure to enable the session inside "config/session" file' + ) + } - this.sessionClient.flashMessages.merge(messages) - return this - }) + this.sessionClient.flashMessages.merge(messages) + return this + } + ) /** * Returns reference to the session data from the session * jar */ - ApiResponse.macro('session', function () { + ApiResponse.macro('session', function (this: ApiResponse) { return this.sessionJar.session }) @@ -63,7 +69,7 @@ export function extendApiClient(sessionManager: SessionManager) { * Returns reference to the flash messages from the session * jar */ - ApiResponse.macro('flashMessages', function () { + ApiResponse.macro('flashMessages', function (this: ApiResponse) { return this.sessionJar.flashMessages || {} }) @@ -71,7 +77,7 @@ export function extendApiClient(sessionManager: SessionManager) { * Assert response to contain a given session and optionally * has the expected value */ - ApiResponse.macro('assertSession', function (name: string, value?: any) { + ApiResponse.macro('assertSession', function (this: ApiResponse, name: string, value?: any) { this.ensureHasAssert() this.assert!.property(this.session(), name) @@ -83,7 +89,7 @@ export function extendApiClient(sessionManager: SessionManager) { /** * Assert response to not contain a given session */ - ApiResponse.macro('assertSessionMissing', function (name: string) { + ApiResponse.macro('assertSessionMissing', function (this: ApiResponse, name: string) { this.ensureHasAssert() this.assert!.notProperty(this.session(), name) }) @@ -92,7 +98,7 @@ export function extendApiClient(sessionManager: SessionManager) { * Assert response to contain a given flash message and optionally * has the expected value */ - ApiResponse.macro('assertFlashMessage', function (name: string, value?: any) { + ApiResponse.macro('assertFlashMessage', function (this: ApiResponse, name: string, value?: any) { this.ensureHasAssert() this.assert!.property(this.flashMessages(), name) @@ -104,7 +110,7 @@ export function extendApiClient(sessionManager: SessionManager) { /** * Assert response to not contain a given session */ - ApiResponse.macro('assertFlashMissing', function (name: string) { + ApiResponse.macro('assertFlashMissing', function (this: ApiResponse, name: string) { this.ensureHasAssert() this.assert!.notProperty(this.flashMessages(), name) }) @@ -112,10 +118,12 @@ export function extendApiClient(sessionManager: SessionManager) { /** * Dump session to the console */ - ApiResponse.macro('dumpSession', function (options?: InspectOptions) { + ApiResponse.macro('dumpSession', function (this: ApiResponse, options?: InspectOptions) { const inspectOptions = { depth: 2, showHidden: false, colors: true, ...options } console.log(`"session" => ${inspect(this.session(), inspectOptions)}`) console.log(`"flashMessages" => ${inspect(this.flashMessages(), inspectOptions)}`) + + return this }) /** From 92dfe44b350a88a54641dd2a4414947ec66c275c Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 21 Jul 2023 14:29:09 +0200 Subject: [PATCH 026/112] chore: add c8 as dev dep --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 11daf46..8ade5cd 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "@types/node": "^20.4.2", "@types/set-cookie-parser": "^2.4.3", "@types/supertest": "^2.0.12", + "c8": "^8.0.0", "copyfiles": "^2.4.1", "del-cli": "^5.0.0", "eslint": "^8.45.0", From 327488b5086f40bfd0c7368d13c996789831e764 Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 21 Jul 2023 14:32:29 +0200 Subject: [PATCH 027/112] chore: add engines.node --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index 8ade5cd..6e1b559 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,9 @@ "name": "@adonisjs/session", "description": "Session provider for AdonisJS", "version": "6.4.0", + "engines": { + "node": ">=18.16.0" + }, "main": "build/index.js", "type": "module", "files": [ From 3706c1fe4a268b3fc347a7c6076f7a81aee906c4 Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 21 Jul 2023 14:33:51 +0200 Subject: [PATCH 028/112] chore(release): 7.0.0-0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6e1b559..8b1d3a5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@adonisjs/session", "description": "Session provider for AdonisJS", - "version": "6.4.0", + "version": "7.0.0-0", "engines": { "node": ">=18.16.0" }, From ffff24c4bfd338c193b06512044587351381eb01 Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 21 Jul 2023 14:45:32 +0200 Subject: [PATCH 029/112] chore: update readme --- README.md | 67 +++++++++++++++++++++---------------------------------- 1 file changed, 25 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 475155b..bf78a66 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,36 @@ -
- -
+# @adonisjs/session
-
-

Sessions

-

This package adds support for sessions to AdonisJS

-
+[![gh-workflow-image]][gh-workflow-url] [![npm-image]][npm-url] ![][typescript-image] [![license-image]][license-url] [![snyk-image]][snyk-url] -
+## Introduction +Add sessions to your AdonisJS application. Cookie, Redis, and File drivers are included out of the box. -
- -[![gh-workflow-image]][gh-workflow-url] [![typescript-image]][typescript-url] [![npm-image]][npm-url] [![license-image]][license-url] [![synk-image]][synk-url] - -
- - - -
- Built with ❤︎ by Harminder Virk -
- -[gh-workflow-image]: https://img.shields.io/github/workflow/status/adonisjs/session/test?style=for-the-badge -[gh-workflow-url]: https://github.com/adonisjs/session/actions/workflows/test.yml "Github action" +## Official Documentation +The documentation is available on the [AdonisJS website](https://docs.adonisjs.com/guides/sessions) -[typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript -[typescript-url]: "typescript" +## Contributing +One of the primary goals of AdonisJS is to have a vibrant community of users and contributors who believes in the principles of the framework. + +We encourage you to read the [contribution guide](https://github.com/adonisjs/.github/blob/main/docs/CONTRIBUTING.md) before contributing to the framework. + +## Code of Conduct +In order to ensure that the AdonisJS community is welcoming to all, please review and abide by the [Code of Conduct](https://github.com/adonisjs/.github/blob/main/docs/CODE_OF_CONDUCT.md). + +## License +AdonisJS ally is open-sourced software licensed under the [MIT license](LICENSE.md). + +[gh-workflow-image]: https://img.shields.io/github/actions/workflow/status/adonisjs/session/test.yml?style=for-the-badge +[gh-workflow-url]: https://github.com/adonisjs/session/actions/workflows/test.yml "Github action" [npm-image]: https://img.shields.io/npm/v/@adonisjs/session/latest.svg?style=for-the-badge&logo=npm [npm-url]: https://www.npmjs.com/package/@adonisjs/session/v/latest "npm" -[license-image]: https://img.shields.io/npm/l/@adonisjs/session?color=blueviolet&style=for-the-badge -[license-url]: LICENSE.md "license" +[typescript-image]: https://img.shields.io/badge/Typescript-294E80.svg?style=for-the-badge&logo=typescript + +[license-url]: LICENSE.md +[license-image]: https://img.shields.io/github/license/adonisjs/session?style=for-the-badge -[synk-image]: https://img.shields.io/snyk/vulnerabilities/github/adonisjs/session?label=Synk%20Vulnerabilities&style=for-the-badge -[synk-url]: https://snyk.io/test/github/adonisjs/session?targetFile=package.json "synk" +[snyk-image]: https://img.shields.io/snyk/vulnerabilities/github/adonisjs/session?label=Snyk%20Vulnerabilities&style=for-the-badge +[snyk-url]: https://snyk.io/test/github/adonisjs/session?targetFile=package.json "snyk" From 74e12774d530ae2a3a63dcabb3ef8f27c5ed4415 Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 21 Jul 2023 15:25:51 +0200 Subject: [PATCH 030/112] chore: update dependencies --- package.json | 2 +- test_helpers/index.ts | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 8b1d3a5..3f62c3e 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "@adonisjs/core": "6.1.5-8", "@adonisjs/eslint-config": "^1.1.8", "@adonisjs/prettier-config": "^1.1.8", - "@adonisjs/redis": "8.0.0-1", + "@adonisjs/redis": "8.0.0-2", "@adonisjs/tsconfig": "^1.1.8", "@adonisjs/view": "7.0.0-4", "@japa/api-client": "2.0.0-0", diff --git a/test_helpers/index.ts b/test_helpers/index.ts index 972afe6..e86ab30 100644 --- a/test_helpers/index.ts +++ b/test_helpers/index.ts @@ -48,10 +48,7 @@ export async function setup(fs: FileSystem, config?: any) { .withCoreProviders() .merge({ rcFileContents: { - providers: [ - '../../providers/session_provider.js', - '@adonisjs/redis/providers/redis_provider', - ], + providers: ['../../providers/session_provider.js', '@adonisjs/redis/redis_provider'], }, config: config || { session: sessionConfig }, }) From c4006681fd76926a292352d1df37f6263d7e7700 Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 21 Jul 2023 15:33:59 +0200 Subject: [PATCH 031/112] chore: update dependencies --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 3f62c3e..1fa1153 100644 --- a/package.json +++ b/package.json @@ -40,10 +40,10 @@ }, "devDependencies": { "@adonisjs/application": "7.1.2-8", - "@adonisjs/core": "6.1.5-8", + "@adonisjs/core": "6.1.5-10", "@adonisjs/eslint-config": "^1.1.8", "@adonisjs/prettier-config": "^1.1.8", - "@adonisjs/redis": "8.0.0-2", + "@adonisjs/redis": "8.0.0-3", "@adonisjs/tsconfig": "^1.1.8", "@adonisjs/view": "7.0.0-4", "@japa/api-client": "2.0.0-0", @@ -75,8 +75,8 @@ "fs-extra": "^11.1.1" }, "peerDependencies": { - "@adonisjs/core": "6.1.5-8", - "@adonisjs/redis": "8.0.0-1" + "@adonisjs/core": "6.1.5-10", + "@adonisjs/redis": "8.0.0-3" }, "peerDependenciesMeta": { "@adonisjs/redis": { From 5b059072689c01cecd97bd4f3ed59eb9beaea5eb Mon Sep 17 00:00:00 2001 From: Julien Date: Sat, 22 Jul 2023 00:52:46 +0200 Subject: [PATCH 032/112] chore: include @adonisjs/view typings --- src/session.ts | 2 -- tsconfig.json | 5 ++++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/session.ts b/src/session.ts index dee9ddd..a948d60 100644 --- a/src/session.ts +++ b/src/session.ts @@ -196,12 +196,10 @@ export class Session { * (only when view property exists) */ #shareLocalsWithView() { - // @ts-ignore may need to expose ./types/extended from adonisjs/view if (!this.#ctx['view'] || typeof this.#ctx['view'].share !== 'function') { return } - // @ts-ignore may need to expose ./types/extended from adonisjs/view this.#ctx['view'].share({ flashMessages: this.flashMessages, session: { diff --git a/tsconfig.json b/tsconfig.json index 0cfd318..183f742 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,5 +3,8 @@ "compilerOptions": { "rootDir": "./", "outDir": "./build", - } + "types": [ + "@adonisjs/view" + ] + }, } From 42794ddc687db88723cfc30cbc48ecefc2ba9095 Mon Sep 17 00:00:00 2001 From: Julien Date: Sat, 22 Jul 2023 00:54:58 +0200 Subject: [PATCH 033/112] chore(release): 7.0.0-1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1fa1153..405025b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@adonisjs/session", "description": "Session provider for AdonisJS", - "version": "7.0.0-0", + "version": "7.0.0-1", "engines": { "node": ">=18.16.0" }, From baab1f930876b1df90c7715cca1b45ecb016c250 Mon Sep 17 00:00:00 2001 From: Julien Date: Sat, 22 Jul 2023 01:07:34 +0200 Subject: [PATCH 034/112] refactor: use container binding for creating middleware --- providers/session_provider.ts | 6 ++++++ src/session_middleware.ts | 5 +++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/providers/session_provider.ts b/providers/session_provider.ts index 7b1d201..7479851 100644 --- a/providers/session_provider.ts +++ b/providers/session_provider.ts @@ -10,6 +10,7 @@ import { ApplicationService } from '@adonisjs/core/types' import { extendHttpContext } from '../src/bindings/http_context.js' import { extendApiClient } from '../src/bindings/api_client.js' +import SessionMiddleware from '../src/session_middleware.js' export default class SessionProvider { constructor(protected app: ApplicationService) {} @@ -27,6 +28,11 @@ export default class SessionProvider { return new SessionManager(config, encryption, redis) }) + + this.app.container.bind(SessionMiddleware, async () => { + const session = await this.app.container.make('session') + return new SessionMiddleware(session) + }) } /** diff --git a/src/session_middleware.ts b/src/session_middleware.ts index 1397689..3415391 100644 --- a/src/session_middleware.ts +++ b/src/session_middleware.ts @@ -3,9 +3,10 @@ import type { NextFn } from '@adonisjs/core/types/http' import { SessionManager } from './session_manager.js' export default class SessionMiddleware { + constructor(protected session: SessionManager) {} + async handle(ctx: HttpContext, next: NextFn) { - const sessionManager = (await ctx.containerResolver.make('session')) as SessionManager - if (!sessionManager.isEnabled()) { + if (!this.session.isEnabled()) { return } From 7b339e87412c556644142c68b84acbb37a22f282 Mon Sep 17 00:00:00 2001 From: Julien Date: Tue, 25 Jul 2023 00:04:43 +0200 Subject: [PATCH 035/112] refactor: extend japa api client in test env only --- providers/session_provider.ts | 23 +++++++++++++++------- test/session_middleware.spec.ts | 3 ++- test/session_provider.spec.ts | 34 ++++++++++++++++++++------------- test_helpers/index.ts | 4 ++-- 4 files changed, 41 insertions(+), 23 deletions(-) diff --git a/providers/session_provider.ts b/providers/session_provider.ts index 7479851..4b7be0c 100644 --- a/providers/session_provider.ts +++ b/providers/session_provider.ts @@ -7,9 +7,9 @@ * file that was distributed with this source code. */ -import { ApplicationService } from '@adonisjs/core/types' +import type { SessionManager } from '../src/session_manager.js' +import type { ApplicationService } from '@adonisjs/core/types' import { extendHttpContext } from '../src/bindings/http_context.js' -import { extendApiClient } from '../src/bindings/api_client.js' import SessionMiddleware from '../src/session_middleware.js' export default class SessionProvider { @@ -35,21 +35,30 @@ export default class SessionProvider { }) } + /** + * Register Japa API Client bindings + */ + async #registerApiClientBindings(session: SessionManager) { + if (this.app.getEnvironment() === 'test') { + const { extendApiClient } = await import('../src/bindings/api_client.js') + extendApiClient(session) + } + } + /** * Register bindings */ async boot() { - const sessionManager = await this.app.container.make('session') + const session = await this.app.container.make('session') /** * Add `session` getter to the HttpContext class */ - extendHttpContext(sessionManager) + extendHttpContext(session) /** - * Add some macros and getter to japa/api-client classes for - * easier testing + * Extend Japa API Client */ - extendApiClient(sessionManager) + await this.#registerApiClientBindings(session) } } diff --git a/test/session_middleware.spec.ts b/test/session_middleware.spec.ts index 2a12930..4fb3a88 100644 --- a/test/session_middleware.spec.ts +++ b/test/session_middleware.spec.ts @@ -28,7 +28,8 @@ test.group('Session', () => { }) const server = createServer(async (req, res) => { - const middleware = new SessionMiddleware() + const session = await app.container.make('session') + const middleware = new SessionMiddleware(session) const ctx = await createHttpContext(app, req, res) await middleware.handle(ctx, () => { assert.isTrue(ctx.session.initiated) diff --git a/test/session_provider.spec.ts b/test/session_provider.spec.ts index 724b6af..ee51978 100644 --- a/test/session_provider.spec.ts +++ b/test/session_provider.spec.ts @@ -29,7 +29,7 @@ test.group('Session Provider', (group) => { }) test('register test api request methods', async ({ assert, fs }) => { - await setup(fs) + await setup(fs, {}, 'test') assert.isTrue(ApiRequest.prototype.hasOwnProperty('session')) assert.isTrue(ApiRequest.prototype.hasOwnProperty('flashMessages')) @@ -37,9 +37,11 @@ test.group('Session Provider', (group) => { }) test('set session before making the api request', async ({ fs, assert }) => { - const { app } = await setup(fs, { - session: { driver: 'memory', cookieName: 'adonis-session' }, - }) + const { app } = await setup( + fs, + { session: { driver: 'memory', cookieName: 'adonis-session' } }, + 'test' + ) const server = createServer(async (req, res) => { const ctx = await createHttpContext(app, req, res) @@ -65,9 +67,11 @@ test.group('Session Provider', (group) => { }) test('get session data from the response', async ({ fs, assert }) => { - const { app } = await setup(fs, { - session: { driver: 'memory', cookieName: 'adonis-session' }, - }) + const { app } = await setup( + fs, + { session: { driver: 'memory', cookieName: 'adonis-session' } }, + 'test' + ) const server = createServer(async (req, res) => { const ctx = await createHttpContext(app, req, res) @@ -92,9 +96,11 @@ test.group('Session Provider', (group) => { }) test('get flash messages from the response', async ({ fs, assert }) => { - const { app } = await setup(fs, { - session: { driver: 'memory', cookieName: 'adonis-session' }, - }) + const { app } = await setup( + fs, + { session: { driver: 'memory', cookieName: 'adonis-session' } }, + 'test' + ) const server = createServer(async (req, res) => { const ctx = await createHttpContext(app, req, res) @@ -119,9 +125,11 @@ test.group('Session Provider', (group) => { }) test('destroy session when request fails', async ({ fs, assert }) => { - const { app } = await setup(fs, { - session: { driver: 'memory', cookieName: 'adonis-session' }, - }) + const { app } = await setup( + fs, + { session: { driver: 'memory', cookieName: 'adonis-session' } }, + 'test' + ) const server = createServer(async (req, res) => { const ctx = await createHttpContext(app, req, res) diff --git a/test_helpers/index.ts b/test_helpers/index.ts index e86ab30..658807e 100644 --- a/test_helpers/index.ts +++ b/test_helpers/index.ts @@ -35,7 +35,7 @@ export const sessionConfig: SessionConfig = { export const BASE_URL = new URL('./tmp/', import.meta.url) -export async function setup(fs: FileSystem, config?: any) { +export async function setup(fs: FileSystem, config?: any, environment: 'web' | 'test' = 'web') { const IMPORTER = (filePath: string) => { if (filePath.startsWith('./') || filePath.startsWith('../')) { return import(new URL(filePath, BASE_URL).href) @@ -54,7 +54,7 @@ export async function setup(fs: FileSystem, config?: any) { }) .create(fs.baseUrl, { importer: IMPORTER }) - const app = ignitor.createApp('web') + const app = ignitor.createApp(environment) await app.init() await app.boot() From 97a5fc3e3789656aecb825e2c27c7812beaf1616 Mon Sep 17 00:00:00 2001 From: Julien Date: Tue, 25 Jul 2023 09:25:52 +0200 Subject: [PATCH 036/112] refactor: remove fs-extra and use node:fs directly --- package.json | 4 +--- src/drivers/file.ts | 50 +++++++++++++++++++++++++++++++++++----- test/file_driver.spec.ts | 43 ++++++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 405025b..0d295f0 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,6 @@ "@japa/runner": "3.0.0-6", "@poppinss/dev-utils": "^2.0.3", "@swc/core": "^1.3.70", - "@types/fs-extra": "^11.0.1", "@types/node": "^20.4.2", "@types/set-cookie-parser": "^2.4.3", "@types/supertest": "^2.0.12", @@ -71,8 +70,7 @@ "typescript": "^5.1.6" }, "dependencies": { - "@poppinss/utils": "6.5.0-3", - "fs-extra": "^11.1.1" + "@poppinss/utils": "6.5.0-3" }, "peerDependencies": { "@adonisjs/core": "6.1.5-10", diff --git a/src/drivers/file.ts b/src/drivers/file.ts index 0a55763..4583212 100644 --- a/src/drivers/file.ts +++ b/src/drivers/file.ts @@ -7,11 +7,10 @@ * file that was distributed with this source code. */ -import { join } from 'node:path' +import { dirname, join } from 'node:path' +import { access, mkdir, readFile, rm, writeFile } from 'node:fs/promises' import { Exception } from '@poppinss/utils' import { MessageBuilder } from '@poppinss/utils' -import { ensureFile, outputFile, remove } from 'fs-extra/esm' -import { readFile } from 'node:fs/promises' import { SessionConfig, SessionDriverContract } from '../types.js' /** @@ -38,13 +37,52 @@ export class FileDriver implements SessionDriverContract { return join(this.#config.file!.location, `${sessionId}.txt`) } + /** + * Check if the given path exists or not + */ + async #pathExists(path: string) { + try { + await access(path) + return true + } catch { + return false + } + } + + /** + * Output file with contents to the given path + */ + async #outputFile(path: string, content: string) { + const pathDirname = dirname(path) + const dirExists = await this.#pathExists(pathDirname) + + if (!dirExists) { + await mkdir(pathDirname, { recursive: true }) + } + + await writeFile(path, content, 'utf-8') + } + + /** + * Ensure the file exists. Create it if missing + */ + async #ensureFile(path: string) { + const pathDirname = dirname(path) + const dirExists = await this.#pathExists(pathDirname) + + if (!dirExists) { + await mkdir(pathDirname, { recursive: true }) + await writeFile(path, '', 'utf-8') + } + } + /** * Returns file contents. A new file will be created if it's * missing. */ async read(sessionId: string): Promise<{ [key: string]: any } | null> { const filePath = this.#getFilePath(sessionId) - await ensureFile(filePath) + await this.#ensureFile(filePath) const contents = await readFile(filePath, 'utf-8') if (!contents.trim()) { @@ -71,14 +109,14 @@ export class FileDriver implements SessionDriverContract { } const message = new MessageBuilder().build(values, undefined, sessionId) - await outputFile(this.#getFilePath(sessionId), message) + await this.#outputFile(this.#getFilePath(sessionId), message) } /** * Cleanup session file by removing it */ async destroy(sessionId: string): Promise { - await remove(this.#getFilePath(sessionId)) + await rm(this.#getFilePath(sessionId), { force: true }) } /** diff --git a/test/file_driver.spec.ts b/test/file_driver.spec.ts index 3027f50..68591b1 100644 --- a/test/file_driver.spec.ts +++ b/test/file_driver.spec.ts @@ -28,6 +28,40 @@ test.group('File driver', () => { ) }) + test('read() should create file when missing', async ({ assert }) => { + const sessionId = '1234' + const session = new FileDriver(config) + const value = await session.read(sessionId) + + assert.isNull(value) + await assert.fileExists('1234.txt') + }) + + test('should create intermediate directories when missing', async ({ assert }) => { + const sessionId = '1234' + const session = new FileDriver({ + driver: 'file', + file: { location: join(fileURLToPath(BASE_URL), 'foo/bar') }, + age: 1000, + clearWithBrowser: false, + cookieName: 'adonis-session', + enabled: true, + cookie: {}, + }) + + const value = await session.read(sessionId) + assert.isNull(value) + await assert.fileExists('foo/bar/1234.txt') + + await session.write(sessionId, { message: 'hello-world' }) + await assert.fileExists('foo/bar/1234.txt') + + await assert.fileEquals( + 'foo/bar/1234.txt', + JSON.stringify({ message: { message: 'hello-world' }, purpose: '1234' }) + ) + }) + test('return null when file is missing', async ({ assert }) => { const sessionId = '1234' const session = new FileDriver(config) @@ -63,6 +97,15 @@ test.group('File driver', () => { await assert.fileNotExists('1234.txt') }) + test('shouldnt file when trying to remove non-existing file', async ({ assert }) => { + const sessionId = '1234' + const session = new FileDriver(config) + + await assert.fileNotExists('1234.txt') + await session.destroy(sessionId) + await assert.fileNotExists('1234.txt') + }) + test('update session expiry', async ({ assert, fs }) => { const sessionId = '1234' From f127f43819ef107e7f73fc482af05490828c2024 Mon Sep 17 00:00:00 2001 From: Julien Date: Tue, 25 Jul 2023 09:27:58 +0200 Subject: [PATCH 037/112] chore: add japa/api-client as peer dependency --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index 0d295f0..403f48a 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,9 @@ "peerDependenciesMeta": { "@adonisjs/redis": { "optional": true + }, + "@japa/api-client": { + "optional": true } }, "author": "virk,adonisjs", From dc0d6f4d9cf503678d56a8bad3eb348192d18262 Mon Sep 17 00:00:00 2001 From: Julien Date: Tue, 25 Jul 2023 09:28:52 +0200 Subject: [PATCH 038/112] chore(release): 7.0.0-2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 403f48a..88ee671 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@adonisjs/session", "description": "Session provider for AdonisJS", - "version": "7.0.0-1", + "version": "7.0.0-2", "engines": { "node": ">=18.16.0" }, From 8cd3c69df99a79fcb71d818c835b33c6d2df45ba Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Sat, 29 Jul 2023 11:06:48 +0530 Subject: [PATCH 039/112] refactor: cleanup cookie driver --- bin/test.ts | 11 +- instructions.md | 10 -- package.json | 25 ++-- src/drivers/cookie.ts | 21 ++- src/types.ts | 13 +- test/cookie_driver.spec.ts | 167 +++++++++++++--------- test_helpers/index.ts | 281 +++++++++++++++++++------------------ 7 files changed, 286 insertions(+), 242 deletions(-) delete mode 100644 instructions.md diff --git a/bin/test.ts b/bin/test.ts index 156fc01..ffdc824 100644 --- a/bin/test.ts +++ b/bin/test.ts @@ -1,7 +1,6 @@ import { assert } from '@japa/assert' -import { processCLIArgs, configure, run } from '@japa/runner' import { fileSystem } from '@japa/file-system' -import { BASE_URL } from '../test_helpers/index.js' +import { processCLIArgs, configure, run } from '@japa/runner' /* |-------------------------------------------------------------------------- @@ -19,13 +18,7 @@ import { BASE_URL } from '../test_helpers/index.js' processCLIArgs(process.argv.slice(2)) configure({ files: ['test/**/*.spec.ts'], - plugins: [ - assert(), - fileSystem({ - autoClean: true, - basePath: BASE_URL, - }), - ], + plugins: [assert(), fileSystem()], forceExit: true, }) diff --git a/instructions.md b/instructions.md deleted file mode 100644 index 98742b8..0000000 --- a/instructions.md +++ /dev/null @@ -1,10 +0,0 @@ -The package has been configured successfully. The session configuration stored inside `config/session.ts` file relies on the following environment variables and hence we recommend validating them. - -Open the `env.ts` file and paste the following code inside the `Env.rules` object. - -```ts -SESSION_DRIVER: Env.schema.string() -``` - -- Here we expect the `SESSION_DRIVER` environment variable to be always present -- And should be a valid string diff --git a/package.json b/package.json index 88ee671..f409c27 100644 --- a/package.json +++ b/package.json @@ -35,23 +35,20 @@ "format": "prettier --write .", "release": "np", "version": "npm run build", - "sync-labels": "github-label-sync --labels .github/labels.json adonisjs/ally", + "sync-labels": "github-label-sync --labels .github/labels.json adonisjs/session", "quick:test": "node --enable-source-maps --loader=ts-node/esm bin/test.ts" }, "devDependencies": { - "@adonisjs/application": "7.1.2-8", - "@adonisjs/core": "6.1.5-10", + "@adonisjs/core": "^6.1.5-12", "@adonisjs/eslint-config": "^1.1.8", "@adonisjs/prettier-config": "^1.1.8", - "@adonisjs/redis": "8.0.0-3", + "@adonisjs/redis": "^8.0.0-7", "@adonisjs/tsconfig": "^1.1.8", - "@adonisjs/view": "7.0.0-4", - "@japa/api-client": "2.0.0-0", - "@japa/assert": "2.0.0-1", - "@japa/file-system": "2.0.0-1", - "@japa/plugin-adonisjs": "2.0.0-1", - "@japa/runner": "3.0.0-6", - "@poppinss/dev-utils": "^2.0.3", + "@japa/api-client": "^2.0.0-0", + "@japa/assert": "^2.0.0-1", + "@japa/file-system": "^2.0.0-1", + "@japa/plugin-adonisjs": "^2.0.0-1", + "@japa/runner": "^3.0.0-6", "@swc/core": "^1.3.70", "@types/node": "^20.4.2", "@types/set-cookie-parser": "^2.4.3", @@ -70,11 +67,11 @@ "typescript": "^5.1.6" }, "dependencies": { - "@poppinss/utils": "6.5.0-3" + "@poppinss/utils": "^6.5.0-5" }, "peerDependencies": { - "@adonisjs/core": "6.1.5-10", - "@adonisjs/redis": "8.0.0-3" + "@adonisjs/core": "^6.1.5-12", + "@adonisjs/redis": "^8.0.0-7" }, "peerDependenciesMeta": { "@adonisjs/redis": { diff --git a/src/drivers/cookie.ts b/src/drivers/cookie.ts index d1b60f8..91d611a 100644 --- a/src/drivers/cookie.ts +++ b/src/drivers/cookie.ts @@ -8,16 +8,18 @@ */ import type { HttpContext } from '@adonisjs/core/http' -import type { SessionConfig, SessionDriverContract } from '../types.js' +import { CookieOptions } from '@adonisjs/core/types/http' +import type { SessionData, SessionDriverContract } from '../types.js' /** - * Cookie driver utilizes the encrypted HTTP cookies to write session value. + * Cookie driver stores the session data inside an encrypted + * cookie. */ export class CookieDriver implements SessionDriverContract { - #config: SessionConfig #ctx: HttpContext + #config: Partial - constructor(config: SessionConfig, ctx: HttpContext) { + constructor(config: Partial, ctx: HttpContext) { this.#config = config this.#ctx = ctx } @@ -25,23 +27,20 @@ export class CookieDriver implements SessionDriverContract { /** * Read session value from the cookie */ - read(sessionId: string): { [key: string]: any } | null { + read(sessionId: string): SessionData | null { const cookieValue = this.#ctx.request.encryptedCookie(sessionId) if (typeof cookieValue !== 'object') { return null } + return cookieValue } /** * Write session values to the cookie */ - write(sessionId: string, values: { [key: string]: any }): void { - if (typeof values !== 'object') { - throw new Error('Session cookie driver expects an object of values') - } - - this.#ctx.response.encryptedCookie(sessionId, values, this.#config.cookie) + write(sessionId: string, values: SessionData): void { + this.#ctx.response.encryptedCookie(sessionId, values, this.#config) } /** diff --git a/src/types.ts b/src/types.ts index 2b978ba..135c4a2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,16 @@ +/* + * @adonisjs/session + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import type { HttpContext } from '@adonisjs/core/http' import type { CookieOptions } from '@adonisjs/core/types/http' + import type { SessionManager } from './session_manager.js' -import type { HttpContext } from '@adonisjs/core/http' /** * The callback to be passed to the `extend` method. It is invoked @@ -74,3 +84,4 @@ export interface SessionConfig { * The values allowed by the `session.put` method */ export type AllowedSessionValues = string | boolean | number | object | Date | Array +export type SessionData = Record diff --git a/test/cookie_driver.spec.ts b/test/cookie_driver.spec.ts index 13606af..ecffc26 100644 --- a/test/cookie_driver.spec.ts +++ b/test/cookie_driver.spec.ts @@ -7,114 +7,153 @@ * file that was distributed with this source code. */ -import { test } from '@japa/runner' import supertest from 'supertest' -import { createServer } from 'node:http' +import { test } from '@japa/runner' +import setCookieParser from 'set-cookie-parser' +import { CookieClient } from '@adonisjs/core/http' +import type { CookieOptions } from '@adonisjs/core/types/http' +import { EncryptionFactory } from '@adonisjs/core/factories/encryption' +import { HttpContextFactory, RequestFactory, ResponseFactory } from '@adonisjs/core/factories/http' +import { httpServer } from '../test_helpers/index.js' import { CookieDriver } from '../src/drivers/cookie.js' -import { - setup, - sessionConfig, - encryptCookie, - decryptCookie, - createHttpContext, -} from '../test_helpers/index.js' -test.group('Cookie driver', () => { - test('return null object when cookie is missing', async ({ fs, assert }) => { - assert.plan(1) +const encryption = new EncryptionFactory().create() +const cookieClient = new CookieClient(encryption) +const cookieConfig: Partial = { + sameSite: 'strict', + maxAge: '5mins', +} - const { app } = await setup(fs) +test.group('Cookie driver', () => { + test('return null when session data cookie does not exists', async ({ assert }) => { const sessionId = '1234' - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() - const session = new CookieDriver(sessionConfig, ctx) + const session = new CookieDriver(cookieConfig, ctx) const value = session.read(sessionId) - assert.isNull(value) - res.end() + response.json(value) + response.finish() }) - await supertest(server).get('/') + const { body, text } = await supertest(server).get('/') + assert.deepEqual(body, {}) + assert.equal(text, '') }) - test('return empty object when cookie value is invalid', async ({ fs, assert }) => { - assert.plan(1) - - const { app } = await setup(fs) + test('return session data from the cookie', async ({ assert }) => { const sessionId = '1234' - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() - const session = new CookieDriver(sessionConfig, ctx) + const session = new CookieDriver(cookieConfig, ctx) const value = session.read(sessionId) - assert.isNull(value) - res.end() + response.json(value) + response.finish() }) - await supertest(server).get('/').set('cookie', '1234=hello-world') + const { body } = await supertest(server) + .get('/') + .set('cookie', `${sessionId}=${cookieClient.encrypt(sessionId, { visits: 1 })}`) + + assert.deepEqual(body, { visits: 1 }) }) - test('return cookie values as an object', async ({ fs, assert }) => { - const { app } = await setup(fs) + test('persist session data inside a cookie', async ({ assert }) => { const sessionId = '1234' - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() - const session = new CookieDriver(sessionConfig, ctx) - const value = session.read(sessionId) + const session = new CookieDriver(cookieConfig, ctx) + session.write(sessionId, { visits: 0 }) + response.finish() + }) - res.writeHead(200, { 'content-type': 'application/json' }) - res.write(JSON.stringify(value)) - res.end() + const { headers } = await supertest(server).get('/') + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) + assert.deepEqual(cookieClient.decrypt(sessionId, cookies[sessionId].value), { + visits: 0, }) + }) - const { body } = await supertest(server) + test('touch cookie by re-updating its attributes', async ({ assert }) => { + const sessionId = '1234' + + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() + + const session = new CookieDriver(cookieConfig, ctx) + session.touch(sessionId) + response.finish() + }) + + const { headers } = await supertest(server) .get('/') - .set('cookie', await encryptCookie(app, { message: 'hello-world' }, sessionId)) + .set('Cookie', `${sessionId}=${cookieClient.encrypt(sessionId, { visits: 1 })}`) - assert.deepEqual(body, { message: 'hello-world' }) + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) + assert.deepEqual(cookieClient.decrypt(sessionId, cookies[sessionId].value), { + visits: 1, + }) }) - test('write cookie value', async ({ fs, assert }) => { - const { app } = await setup(fs) + test('do not write cookie to response unless touch or write methods are called', async ({ + assert, + }) => { const sessionId = '1234' - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) - - const session = new CookieDriver(sessionConfig, ctx) - session.write(sessionId, { message: 'hello-world' }) + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() - ctx.response.send('') - ctx.response.finish() + const session = new CookieDriver(cookieConfig, ctx) + response.json(session.read(sessionId)) + response.finish() }) - const { header } = await supertest(server).get('/') - assert.deepEqual(await decryptCookie(app, header, sessionId), { message: 'hello-world' }) + const { headers, body } = await supertest(server) + .get('/') + .set('Cookie', `${sessionId}=${cookieClient.encrypt(sessionId, { visits: 1 })}`) + + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) + assert.deepEqual(cookies, {}) + assert.deepEqual(body, { visits: 1 }) }) - test('update cookie with existing value', async ({ fs, assert }) => { - const { app } = await setup(fs) + test('delete session data cookie', async ({ assert }) => { const sessionId = '1234' - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) - - const session = new CookieDriver(sessionConfig, ctx) - session.touch(sessionId) + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() - ctx.response.send('') - ctx.response.finish() + const session = new CookieDriver(cookieConfig, ctx) + response.json(session.read(sessionId)) + session.destroy(sessionId) + response.finish() }) - const { header } = await supertest(server) + const { headers, body } = await supertest(server) .get('/') - .set('cookie', await encryptCookie(app, { message: 'hello-world' }, sessionId)) + .set('Cookie', `${sessionId}=${cookieClient.encrypt(sessionId, { visits: 1 })}`) - assert.deepEqual(await decryptCookie(app, header, sessionId), { message: 'hello-world' }) + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) + assert.equal(cookies[sessionId].maxAge, -1) + assert.equal(cookies[sessionId].expires, new Date('1970-01-01').toString()) + assert.deepEqual(body, { visits: 1 }) }) }) diff --git a/test_helpers/index.ts b/test_helpers/index.ts index 658807e..cd2c13e 100644 --- a/test_helpers/index.ts +++ b/test_helpers/index.ts @@ -7,139 +7,154 @@ * file that was distributed with this source code. */ -import { IgnitorFactory } from '@adonisjs/core/factories' -import { FileSystem } from '@japa/file-system' -import { SessionConfig } from '../src/types.js' -import { ApplicationService } from '@adonisjs/core/types' -import { RedisService } from '@adonisjs/redis/types' -import { RedisManagerFactory } from '@adonisjs/redis/factories' -import { Application } from '@adonisjs/core/app' -import { IncomingMessage, ServerResponse } from 'node:http' -import { Server } from '@adonisjs/core/http' -import { pluginAdonisJS } from '@japa/plugin-adonisjs' -import { Runner } from '@japa/runner/core' - -/** - * Session default config - */ -export const sessionConfig: SessionConfig = { - enabled: true, - driver: 'cookie', - cookieName: 'adonis-session', - clearWithBrowser: false, - age: 3000, - cookie: { - path: '/', - }, -} - -export const BASE_URL = new URL('./tmp/', import.meta.url) - -export async function setup(fs: FileSystem, config?: any, environment: 'web' | 'test' = 'web') { - const IMPORTER = (filePath: string) => { - if (filePath.startsWith('./') || filePath.startsWith('../')) { - return import(new URL(filePath, BASE_URL).href) - } - return import(filePath) - } - - const ignitor = new IgnitorFactory() - .withCoreConfig() - .withCoreProviders() - .merge({ - rcFileContents: { - providers: ['../../providers/session_provider.js', '@adonisjs/redis/redis_provider'], - }, - config: config || { session: sessionConfig }, +import { getActiveTest } from '@japa/runner' +import { IncomingMessage, ServerResponse, createServer } from 'node:http' + +export const httpServer = { + create(callback: (req: IncomingMessage, res: ServerResponse) => any) { + const server = createServer(callback) + getActiveTest()?.cleanup(async () => { + await new Promise((resolve) => { + server.close(() => resolve()) + }) }) - .create(fs.baseUrl, { importer: IMPORTER }) - - const app = ignitor.createApp(environment) - - await app.init() - await app.boot() - - // @ts-ignore - await pluginAdonisJS(app)({ - runner: new Runner({} as any), - }) - - return { app, ignitor } -} - -/** - * Sleep for a while - */ -export function sleep(time: number): Promise { - return new Promise((resolve) => setTimeout(resolve, time)) -} - -/** - * Signs value to be set as cookie header - */ -export async function signCookie(app: ApplicationService, value: any, name: string) { - const encryption = await app.container.make('encryption') - return `${name}=s:${encryption.verifier.sign(value, undefined, name)}` -} - -/** - * Encrypt value to be set as cookie header - */ -export async function encryptCookie(app: ApplicationService, value: any, name: string) { - const encryption = await app.container.make('encryption') - return `${name}=e:${encryption.encrypt(value, undefined, name)}` -} - -/** - * Decrypt cookie - */ -export async function decryptCookie(app: ApplicationService, header: any, name: string) { - const encryption = await app.container.make('encryption') - const cookieValue = decodeURIComponent(header['set-cookie'][0].split(';')[0]) - .replace(`${name}=`, '') - .slice(2) - - return encryption.decrypt(cookieValue, name) -} - -/** - * Unsign cookie - */ -export async function unsignCookie(app: ApplicationService, header: any, name: string) { - const encryption = await app.container.make('encryption') - - const cookieValue = decodeURIComponent(header['set-cookie'][0].split(';')[0]) - .replace(`${name}=`, '') - .slice(2) - - return encryption.verifier.unsign(cookieValue, name) -} - -/** - * Reference to the redis manager - */ -export function getRedisManager(application: ApplicationService) { - return new RedisManagerFactory({ - connection: 'session', - connections: { - session: { - host: process.env.REDIS_HOST || '0.0.0.0', - port: process.env.REDIS_PORT || 6379, - }, - }, - }).create(application) as RedisService + return server + }, } -export async function createHttpContext( - app: Application, - req: IncomingMessage, - res: ServerResponse -) { - const adonisServer = (await app.container.make('server')) as Server - - const request = adonisServer.createRequest(req, res) - const response = adonisServer.createResponse(req, res) - const ctx = adonisServer.createHttpContext(request, response, app.container.createResolver()) - - return ctx -} +// import { IgnitorFactory } from '@adonisjs/core/factories' +// import { FileSystem } from '@japa/file-system' +// import { SessionConfig } from '../src/types.js' +// import { ApplicationService } from '@adonisjs/core/types' +// import { RedisService } from '@adonisjs/redis/types' +// import { RedisManagerFactory } from '@adonisjs/redis/factories' +// import { Application } from '@adonisjs/core/app' +// import { IncomingMessage, ServerResponse } from 'node:http' +// import { Server } from '@adonisjs/core/http' +// import { pluginAdonisJS } from '@japa/plugin-adonisjs' +// import { Runner } from '@japa/runner/core' + +// /** +// * Session default config +// */ +// export const sessionConfig: SessionConfig = { +// enabled: true, +// driver: 'cookie', +// cookieName: 'adonis-session', +// clearWithBrowser: false, +// age: 3000, +// cookie: { +// path: '/', +// }, +// } + +// export const BASE_URL = new URL('./tmp/', import.meta.url) + +// export async function setup(fs: FileSystem, config?: any, environment: 'web' | 'test' = 'web') { +// const IMPORTER = (filePath: string) => { +// if (filePath.startsWith('./') || filePath.startsWith('../')) { +// return import(new URL(filePath, BASE_URL).href) +// } +// return import(filePath) +// } + +// const ignitor = new IgnitorFactory() +// .withCoreConfig() +// .withCoreProviders() +// .merge({ +// rcFileContents: { +// providers: ['../../providers/session_provider.js', '@adonisjs/redis/redis_provider'], +// }, +// config: config || { session: sessionConfig }, +// }) +// .create(fs.baseUrl, { importer: IMPORTER }) + +// const app = ignitor.createApp(environment) + +// await app.init() +// await app.boot() + +// // @ts-ignore +// await pluginAdonisJS(app)({ +// runner: new Runner({} as any), +// }) + +// return { app, ignitor } +// } + +// /** +// * Sleep for a while +// */ +// export function sleep(time: number): Promise { +// return new Promise((resolve) => setTimeout(resolve, time)) +// } + +// /** +// * Signs value to be set as cookie header +// */ +// export async function signCookie(app: ApplicationService, value: any, name: string) { +// const encryption = await app.container.make('encryption') +// return `${name}=s:${encryption.verifier.sign(value, undefined, name)}` +// } + +// /** +// * Encrypt value to be set as cookie header +// */ +// export async function encryptCookie(app: ApplicationService, value: any, name: string) { +// const encryption = await app.container.make('encryption') +// return `${name}=e:${encryption.encrypt(value, undefined, name)}` +// } + +// /** +// * Decrypt cookie +// */ +// export async function decryptCookie(app: ApplicationService, header: any, name: string) { +// const encryption = await app.container.make('encryption') +// const cookieValue = decodeURIComponent(header['set-cookie'][0].split(';')[0]) +// .replace(`${name}=`, '') +// .slice(2) + +// return encryption.decrypt(cookieValue, name) +// } + +// /** +// * Unsign cookie +// */ +// export async function unsignCookie(app: ApplicationService, header: any, name: string) { +// const encryption = await app.container.make('encryption') + +// const cookieValue = decodeURIComponent(header['set-cookie'][0].split(';')[0]) +// .replace(`${name}=`, '') +// .slice(2) + +// return encryption.verifier.unsign(cookieValue, name) +// } + +// /** +// * Reference to the redis manager +// */ +// export function getRedisManager(application: ApplicationService) { +// return new RedisManagerFactory({ +// connection: 'session', +// connections: { +// session: { +// host: process.env.REDIS_HOST || '0.0.0.0', +// port: process.env.REDIS_PORT || 6379, +// }, +// }, +// }).create(application) as RedisService +// } + +// export async function createHttpContext( +// app: Application, +// req: IncomingMessage, +// res: ServerResponse +// ) { +// const adonisServer = (await app.container.make('server')) as Server + +// const request = adonisServer.createRequest(req, res) +// const response = adonisServer.createResponse(req, res) +// const ctx = adonisServer.createHttpContext(request, response, app.container.createResolver()) + +// return ctx +// } From 5e3d4152ff3c157fd2a1c713283cd9e128aebbb6 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Sat, 29 Jul 2023 14:22:42 +0530 Subject: [PATCH 040/112] refactor: migrate file driver --- src/drivers/file.ts | 124 ++++++++++++++++++++++----------------- src/types.ts | 65 ++++++++++++++------ test/file_driver.spec.ts | 123 +++++++++++++++++++------------------- 3 files changed, 180 insertions(+), 132 deletions(-) diff --git a/src/drivers/file.ts b/src/drivers/file.ts index 4583212..7cb8c8d 100644 --- a/src/drivers/file.ts +++ b/src/drivers/file.ts @@ -8,37 +8,35 @@ */ import { dirname, join } from 'node:path' -import { access, mkdir, readFile, rm, writeFile } from 'node:fs/promises' -import { Exception } from '@poppinss/utils' +import string from '@poppinss/utils/string' import { MessageBuilder } from '@poppinss/utils' -import { SessionConfig, SessionDriverContract } from '../types.js' +import { access, mkdir, readFile, rm, writeFile, utimes, stat } from 'node:fs/promises' + +import type { SessionData, SessionDriverContract } from '../types.js' +import { Stats } from 'node:fs' /** - * File driver to read/write session to filesystem + * File driver writes the session data on the file system as. Each session + * id gets its own file. */ export class FileDriver implements SessionDriverContract { - #config: SessionConfig + #config: { location: string } + #age: string | number - constructor(config: SessionConfig) { + constructor(config: { location: string }, age: string | number) { this.#config = config - - if (!this.#config.file || !this.#config.file.location) { - throw new Exception( - 'Missing "file.location" for session file driver inside "config/session" file', - { code: 'E_INVALID_SESSION_DRIVER_CONFIG', status: 500 } - ) - } + this.#age = age } /** - * Returns complete path to the session file + * Returns an absolute path to the session id file */ #getFilePath(sessionId: string): string { - return join(this.#config.file!.location, `${sessionId}.txt`) + return join(this.#config.location, `${sessionId}.txt`) } /** - * Check if the given path exists or not + * Check if a file exists at a given path or not */ async #pathExists(path: string) { try { @@ -50,84 +48,102 @@ export class FileDriver implements SessionDriverContract { } /** - * Output file with contents to the given path + * Returns stats for a file and ignoring missing + * files. */ - async #outputFile(path: string, content: string) { - const pathDirname = dirname(path) - const dirExists = await this.#pathExists(pathDirname) - - if (!dirExists) { - await mkdir(pathDirname, { recursive: true }) + async #stats(path: string): Promise { + try { + const stats = await stat(path) + return stats + } catch { + return null } - - await writeFile(path, content, 'utf-8') } /** - * Ensure the file exists. Create it if missing + * Output file with contents to the given path */ - async #ensureFile(path: string) { + async #outputFile(path: string, contents: string) { const pathDirname = dirname(path) - const dirExists = await this.#pathExists(pathDirname) + const dirExists = await this.#pathExists(pathDirname) if (!dirExists) { await mkdir(pathDirname, { recursive: true }) - await writeFile(path, '', 'utf-8') } + + await writeFile(path, contents, 'utf-8') } /** - * Returns file contents. A new file will be created if it's - * missing. + * Reads the session data from the disk. */ - async read(sessionId: string): Promise<{ [key: string]: any } | null> { + async read(sessionId: string): Promise { const filePath = this.#getFilePath(sessionId) - await this.#ensureFile(filePath) - const contents = await readFile(filePath, 'utf-8') - if (!contents.trim()) { + /** + * Return null when no session id file exists in first + * place + */ + const stats = await this.#stats(filePath) + if (!stats) { return null } /** - * Verify contents with the session id and return them as an object. + * Check if the file has been expired and return null (if expired) */ - const verifiedContents = new MessageBuilder().verify(contents.trim(), sessionId) - if (typeof verifiedContents !== 'object') { + const sessionWillExpireAt = stats.mtimeMs + string.milliseconds.parse(this.#age) + if (Date.now() > sessionWillExpireAt) { return null } - return verifiedContents + /** + * Reading the file contents if the file exists + */ + let contents = await readFile(filePath, 'utf-8') + contents = contents.trim() + if (!contents) { + return null + } + + /** + * Verify contents with the session id and return them as an object. The verify + * method can fail when the contents is not JSON> + */ + try { + const verifiedContents = new MessageBuilder().verify(contents, sessionId) + if (typeof verifiedContents !== 'object') { + return null + } + + return verifiedContents + } catch { + return null + } } /** - * Write session values to a file + * Writes the session data to the disk as a string */ - async write(sessionId: string, values: { [key: string]: any }): Promise { - if (typeof values !== 'object') { - throw new Error('Session file driver expects an object of values') - } - + async write(sessionId: string, values: SessionData): Promise { + const filePath = this.#getFilePath(sessionId) const message = new MessageBuilder().build(values, undefined, sessionId) - await this.#outputFile(this.#getFilePath(sessionId), message) + + await this.#outputFile(filePath, message) } /** - * Cleanup session file by removing it + * Removes the session file from the disk */ async destroy(sessionId: string): Promise { await rm(this.#getFilePath(sessionId), { force: true }) } /** - * Writes the value by reading it from the store + * Updates the session expiry by rewriting it to the + * persistence store */ async touch(sessionId: string): Promise { - const value = await this.read(sessionId) - if (!value) { - return - } - - await this.write(sessionId, value) + await utimes(this.#getFilePath(sessionId), Date.now(), Date.now()) } } diff --git a/src/types.ts b/src/types.ts index 135c4a2..2694e39 100644 --- a/src/types.ts +++ b/src/types.ts @@ -9,7 +9,6 @@ import type { HttpContext } from '@adonisjs/core/http' import type { CookieOptions } from '@adonisjs/core/types/http' - import type { SessionManager } from './session_manager.js' /** @@ -23,12 +22,37 @@ export type ExtendCallback = ( ) => SessionDriverContract /** - * Shape of a driver that every session driver must have + * The values allowed by the `session.put` method + */ +export type AllowedSessionValues = string | boolean | number | object | Date | Array +export type SessionData = Record + +/** + * Session drivers must implement the session driver contract. */ export interface SessionDriverContract { - read(sessionId: string): Promise | null> | Record | null - write(sessionId: string, values: Record): Promise | void + /** + * The read method is used to read the data from the persistence + * store and return it back as an object + */ + read(sessionId: string): Promise | SessionData | null + + /** + * The write method is used to write the session data into the + * persistence store. + */ + write(sessionId: string, data: SessionData): Promise | void + + /** + * The destroy method is used to destroy the session by removing + * its data from the persistence store + */ destroy(sessionId: string): Promise | void + + /** + * The touch method should update the lifetime of session id without + * making changes to the session data. + */ touch(sessionId: string): Promise | void } @@ -37,51 +61,54 @@ export interface SessionDriverContract { */ export interface SessionConfig { /** - * Enable/disable session for the entire application lifecycle + * Enable/disable sessions temporarily */ enabled: boolean /** - * The driver in play + * The drivers to use */ driver: string /** - * Cookie name. + * The name of the cookie for storing the session id. */ cookieName: string /** - * Clear session when browser closes + * When set to true, the session id cookie will be removed + * when the user closes the browser. + * + * However, the persisted data will continue to exist until + * it gets expired. */ clearWithBrowser: boolean /** - * Age of session cookie + * How long the session data should be kept alive without any + * activity. + * + * The session id cookie will also live for the same duration, unless + * "clearWithBrowser" is enabled */ age: string | number /** - * Config for the cookie driver and also the session id - * cookie + * Configuration used by the cookie driver and for storing the + * session id cookie. */ cookie: Omit, 'maxAge' | 'expires'> /** - * Config for the file driver + * Configuration used by the file driver. */ file?: { location: string } /** - * The redis connection to use from the `config/redis` file + * Reference to the redis connection name to use for + * storing the session data. */ redisConnection?: string } - -/** - * The values allowed by the `session.put` method - */ -export type AllowedSessionValues = string | boolean | number | object | Date | Array -export type SessionData = Record diff --git a/test/file_driver.spec.ts b/test/file_driver.spec.ts index 68591b1..c60e988 100644 --- a/test/file_driver.spec.ts +++ b/test/file_driver.spec.ts @@ -7,117 +7,122 @@ * file that was distributed with this source code. */ -import { test } from '@japa/runner' import { join } from 'node:path' -import { FileDriver } from '../src/drivers/file.js' -import { sleep, sessionConfig, BASE_URL } from '../test_helpers/index.js' -import { fileURLToPath } from 'node:url' +import { test } from '@japa/runner' +import { setTimeout } from 'node:timers/promises' -const config = Object.assign({}, sessionConfig, { - driver: 'file', - file: { location: fileURLToPath(BASE_URL) }, -}) +import { FileDriver } from '../src/drivers/file.js' test.group('File driver', () => { - test('throws if location is missing', ({ assert }) => { - // @ts-ignore - const session = () => new FileDriver({ driver: 'file', file: {} }) - assert.throws( - session, - 'Missing "file.location" for session file driver inside "config/session" file' - ) - }) - - test('read() should create file when missing', async ({ assert }) => { + test('do not create file for a new session', async ({ fs, assert }) => { const sessionId = '1234' - const session = new FileDriver(config) - const value = await session.read(sessionId) + const session = new FileDriver({ location: fs.basePath }, '2 hours') + const value = await session.read(sessionId) assert.isNull(value) - await assert.fileExists('1234.txt') + + await assert.fileNotExists('1234.txt') }) - test('should create intermediate directories when missing', async ({ assert }) => { + test('create intermediate directories when missing', async ({ fs, assert }) => { const sessionId = '1234' - const session = new FileDriver({ - driver: 'file', - file: { location: join(fileURLToPath(BASE_URL), 'foo/bar') }, - age: 1000, - clearWithBrowser: false, - cookieName: 'adonis-session', - enabled: true, - cookie: {}, - }) - - const value = await session.read(sessionId) - assert.isNull(value) - await assert.fileExists('foo/bar/1234.txt') + const session = new FileDriver( + { + location: join(fs.basePath, 'app/sessions'), + }, + '2 hours' + ) await session.write(sessionId, { message: 'hello-world' }) - await assert.fileExists('foo/bar/1234.txt') + await assert.fileExists('app/sessions/1234.txt') await assert.fileEquals( - 'foo/bar/1234.txt', + 'app/sessions/1234.txt', JSON.stringify({ message: { message: 'hello-world' }, purpose: '1234' }) ) }) - test('return null when file is missing', async ({ assert }) => { + test('get session existing value', async ({ assert, fs }) => { const sessionId = '1234' - const session = new FileDriver(config) + const session = new FileDriver({ location: fs.basePath }, '2 hours') + await session.write(sessionId, { message: 'hello-world' }) + const value = await session.read(sessionId) - assert.isNull(value) + assert.deepEqual(value, { message: 'hello-world' }) }) - test('write session value to the file', async ({ assert }) => { + test('return null when session data is expired', async ({ assert, fs }) => { const sessionId = '1234' - const session = new FileDriver(config) + const session = new FileDriver({ location: fs.basePath }, 1000) await session.write(sessionId, { message: 'hello-world' }) - await assert.fileEquals( - '1234.txt', - JSON.stringify({ message: { message: 'hello-world' }, purpose: '1234' }) - ) - }) + await setTimeout(2000) - test('get session existing value', async ({ assert }) => { + const value = await session.read(sessionId) + assert.isNull(value) + }).disableTimeout() + + test('ignore malformed file contents', async ({ fs, assert }) => { const sessionId = '1234' - const session = new FileDriver(config) - await session.write(sessionId, { message: 'hello-world' }) + const session = new FileDriver({ location: fs.basePath }, '2 hours') + await fs.create('1234.txt', 'foo') + const value = await session.read(sessionId) - assert.deepEqual(value, { message: 'hello-world' }) + assert.isNull(value) }) - test('remove session file', async ({ assert }) => { + test('remove file on destroy', async ({ assert, fs }) => { const sessionId = '1234' - const session = new FileDriver(config) + + const session = new FileDriver({ location: fs.basePath }, '2 hours') await session.write(sessionId, { message: 'hello-world' }) await session.destroy(sessionId) await assert.fileNotExists('1234.txt') }) - test('shouldnt file when trying to remove non-existing file', async ({ assert }) => { + test('do not fail when destroying a non-existing session', async ({ assert, fs }) => { const sessionId = '1234' - const session = new FileDriver(config) await assert.fileNotExists('1234.txt') + + const session = new FileDriver({ location: fs.basePath }, '2 hours') await session.destroy(sessionId) + await assert.fileNotExists('1234.txt') }) - test('update session expiry', async ({ assert, fs }) => { + test('update session expiry on touch', async ({ assert, fs }) => { const sessionId = '1234' + const now = Date.now() - const session = new FileDriver(config) + const session = new FileDriver({ location: fs.basePath }, '2 hours') await session.write(sessionId, { message: 'hello-world' }) - await sleep(1000) + /** + * Waiting a bit + */ + await setTimeout(2000) + + /** + * Making sure the original mTime of the file was smaller + * than the current time after wait + */ const { mtimeMs } = await fs.adapter.stat(join(fs.basePath, '1234.txt')) assert.isBelow(mtimeMs, Date.now()) + assert.isAbove(mtimeMs, now) await session.touch(sessionId) + + /** + * Ensuring the new mTime is greater than the old mTime + */ let { mtimeMs: newMtimeMs } = await fs.adapter.stat(join(fs.basePath, '1234.txt')) assert.isAbove(newMtimeMs, mtimeMs) - }).timeout(0) + + await assert.fileEquals( + '1234.txt', + JSON.stringify({ message: { message: 'hello-world' }, purpose: '1234' }) + ) + }).disableTimeout() }) From a51eb2c96e8b8e05c618084db6b56c7a09bb2746 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Sat, 29 Jul 2023 14:24:25 +0530 Subject: [PATCH 041/112] ci: run tests on windows --- .github/workflows/test.yml | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 63ac71d..3938df8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ jobs: typecheck: uses: adonisjs/.github/.github/workflows/typecheck.yml@main - linux: + test_linux: runs-on: ubuntu-latest strategy: matrix: @@ -37,3 +37,32 @@ jobs: env: REDIS_HOST: 0.0.0.0 REDIS_PORT: 6379 + + test_windows: + runs-on: windows-latest + strategy: + matrix: + node-version: [18.16.0, 20.x] + services: + redis: + image: redis + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 6379:6379 + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - name: Install + run: npm install + - name: Run tests + run: npm test + env: + REDIS_HOST: 0.0.0.0 + REDIS_PORT: 6379 From 3332c23e22a1956f65495c8cb4a3c883e8f740d4 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Sat, 29 Jul 2023 14:26:11 +0530 Subject: [PATCH 042/112] ci: only run file driver tests --- .github/workflows/test.yml | 13 +------------ bin/test.ts | 2 +- {test => tests}/configure.spec.ts | 0 {test => tests}/cookie_driver.spec.ts | 0 {test => tests}/define_config.spec.ts | 0 {test => tests}/file_driver.spec.ts | 0 {test => tests}/redis_driver.spec.ts | 0 {test => tests}/session.spec.ts | 0 {test => tests}/session_client.spec.ts | 0 {test => tests}/session_manager.spec.ts | 0 {test => tests}/session_middleware.spec.ts | 0 {test => tests}/session_provider.spec.ts | 0 {test => tests}/store.spec.ts | 0 13 files changed, 2 insertions(+), 13 deletions(-) rename {test => tests}/configure.spec.ts (100%) rename {test => tests}/cookie_driver.spec.ts (100%) rename {test => tests}/define_config.spec.ts (100%) rename {test => tests}/file_driver.spec.ts (100%) rename {test => tests}/redis_driver.spec.ts (100%) rename {test => tests}/session.spec.ts (100%) rename {test => tests}/session_client.spec.ts (100%) rename {test => tests}/session_manager.spec.ts (100%) rename {test => tests}/session_middleware.spec.ts (100%) rename {test => tests}/session_provider.spec.ts (100%) rename {test => tests}/store.spec.ts (100%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3938df8..e90709d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -43,16 +43,6 @@ jobs: strategy: matrix: node-version: [18.16.0, 20.x] - services: - redis: - image: redis - options: >- - --health-cmd "redis-cli ping" - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - - 6379:6379 steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} @@ -64,5 +54,4 @@ jobs: - name: Run tests run: npm test env: - REDIS_HOST: 0.0.0.0 - REDIS_PORT: 6379 + NO_REDIS: true diff --git a/bin/test.ts b/bin/test.ts index ffdc824..697c352 100644 --- a/bin/test.ts +++ b/bin/test.ts @@ -17,7 +17,7 @@ import { processCLIArgs, configure, run } from '@japa/runner' */ processCLIArgs(process.argv.slice(2)) configure({ - files: ['test/**/*.spec.ts'], + files: ['tests/file_driver.spec.ts'], plugins: [assert(), fileSystem()], forceExit: true, }) diff --git a/test/configure.spec.ts b/tests/configure.spec.ts similarity index 100% rename from test/configure.spec.ts rename to tests/configure.spec.ts diff --git a/test/cookie_driver.spec.ts b/tests/cookie_driver.spec.ts similarity index 100% rename from test/cookie_driver.spec.ts rename to tests/cookie_driver.spec.ts diff --git a/test/define_config.spec.ts b/tests/define_config.spec.ts similarity index 100% rename from test/define_config.spec.ts rename to tests/define_config.spec.ts diff --git a/test/file_driver.spec.ts b/tests/file_driver.spec.ts similarity index 100% rename from test/file_driver.spec.ts rename to tests/file_driver.spec.ts diff --git a/test/redis_driver.spec.ts b/tests/redis_driver.spec.ts similarity index 100% rename from test/redis_driver.spec.ts rename to tests/redis_driver.spec.ts diff --git a/test/session.spec.ts b/tests/session.spec.ts similarity index 100% rename from test/session.spec.ts rename to tests/session.spec.ts diff --git a/test/session_client.spec.ts b/tests/session_client.spec.ts similarity index 100% rename from test/session_client.spec.ts rename to tests/session_client.spec.ts diff --git a/test/session_manager.spec.ts b/tests/session_manager.spec.ts similarity index 100% rename from test/session_manager.spec.ts rename to tests/session_manager.spec.ts diff --git a/test/session_middleware.spec.ts b/tests/session_middleware.spec.ts similarity index 100% rename from test/session_middleware.spec.ts rename to tests/session_middleware.spec.ts diff --git a/test/session_provider.spec.ts b/tests/session_provider.spec.ts similarity index 100% rename from test/session_provider.spec.ts rename to tests/session_provider.spec.ts diff --git a/test/store.spec.ts b/tests/store.spec.ts similarity index 100% rename from test/store.spec.ts rename to tests/store.spec.ts From 5b4186fb5240415048b233781e0024b269d6b81b Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Sat, 29 Jul 2023 14:31:31 +0530 Subject: [PATCH 043/112] test: remove flaky assertion --- tests/file_driver.spec.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/file_driver.spec.ts b/tests/file_driver.spec.ts index c60e988..109018b 100644 --- a/tests/file_driver.spec.ts +++ b/tests/file_driver.spec.ts @@ -94,7 +94,6 @@ test.group('File driver', () => { test('update session expiry on touch', async ({ assert, fs }) => { const sessionId = '1234' - const now = Date.now() const session = new FileDriver({ location: fs.basePath }, '2 hours') await session.write(sessionId, { message: 'hello-world' }) @@ -110,7 +109,6 @@ test.group('File driver', () => { */ const { mtimeMs } = await fs.adapter.stat(join(fs.basePath, '1234.txt')) assert.isBelow(mtimeMs, Date.now()) - assert.isAbove(mtimeMs, now) await session.touch(sessionId) From 8e5caeb03a1d6c772d2b9679d0e6920ef590ba73 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Sat, 29 Jul 2023 14:34:41 +0530 Subject: [PATCH 044/112] fix: pass instance of date to utimes method --- src/drivers/file.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/drivers/file.ts b/src/drivers/file.ts index 7cb8c8d..fab7ba6 100644 --- a/src/drivers/file.ts +++ b/src/drivers/file.ts @@ -7,13 +7,13 @@ * file that was distributed with this source code. */ +import type { Stats } from 'node:fs' import { dirname, join } from 'node:path' import string from '@poppinss/utils/string' import { MessageBuilder } from '@poppinss/utils' import { access, mkdir, readFile, rm, writeFile, utimes, stat } from 'node:fs/promises' import type { SessionData, SessionDriverContract } from '../types.js' -import { Stats } from 'node:fs' /** * File driver writes the session data on the file system as. Each session @@ -144,6 +144,6 @@ export class FileDriver implements SessionDriverContract { * persistence store */ async touch(sessionId: string): Promise { - await utimes(this.#getFilePath(sessionId), Date.now(), Date.now()) + await utimes(this.#getFilePath(sessionId), new Date(), new Date()) } } From 475f16a06d3d7987eb058cee09ed864d999bd69a Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Sat, 29 Jul 2023 17:56:15 +0530 Subject: [PATCH 045/112] test: improve store tests coverage --- src/store.ts | 60 ++++++++++++++++++++++------------ tests/store.spec.ts | 78 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 21 deletions(-) diff --git a/src/store.ts b/src/store.ts index 079ae57..5691fb7 100644 --- a/src/store.ts +++ b/src/store.ts @@ -7,21 +7,28 @@ * file that was distributed with this source code. */ -import { Exception } from '@poppinss/utils' import lodash from '@poppinss/utils/lodash' +import { RuntimeException } from '@poppinss/utils' -import type { AllowedSessionValues } from './types.js' +import type { AllowedSessionValues, SessionData } from './types.js' /** - * Session store to mutate and access values from the session object + * Session store encapsulates the session data and offers a + * declarative API to mutate it. */ export class Store { /** * Underlying store values */ - #values: { [key: string]: any } + #values: SessionData - constructor(values: { [key: string]: any } | null) { + /** + * A boolean to know if store has been + * modified + */ + #modified: boolean = false + + constructor(values: SessionData | null) { this.#values = values || {} } @@ -33,33 +40,28 @@ export class Store { } /** - * Set key/value pair + * Find if the store has been modified. */ - set(key: string, value: AllowedSessionValues): void { - lodash.set(this.#values, key, value) + get hasBeenModified(): boolean { + return this.#modified } /** - * Get value for a given key + * Set key/value pair */ - get(key: string, defaultValue?: any): any { - return lodash.get(this.#values, key, defaultValue) + set(key: string, value: AllowedSessionValues): void { + this.#modified = true + lodash.set(this.#values, key, value) } /** * Remove key */ unset(key: string): void { + this.#modified = true lodash.unset(this.#values, key) } - /** - * Reset store by clearing it's values. - */ - clear(): void { - this.update({}) - } - /** * Pull value from the store. It is same as calling * store.get and then store.unset @@ -78,7 +80,7 @@ export class Store { increment(key: string, steps: number = 1): void { const value = this.get(key, 0) if (typeof value !== 'number') { - throw new Exception(`Cannot increment "${key}", since original value is not a number`) + throw new RuntimeException(`Cannot increment "${key}". Existing value is not a number`) } this.set(key, value + steps) @@ -91,16 +93,17 @@ export class Store { decrement(key: string, steps: number = 1): void { const value = this.get(key, 0) if (typeof value !== 'number') { - throw new Exception(`Cannot increment "${key}", since original value is not a number`) + throw new RuntimeException(`Cannot decrement "${key}". Existing value is not a number`) } this.set(key, value - steps) } /** - * Overwrite the underlying values object + * Overwrite existing store data with new values. */ update(values: { [key: string]: any }): void { + this.#modified = true this.#values = values } @@ -108,9 +111,24 @@ export class Store { * Update to merge values */ merge(values: { [key: string]: any }): any { + this.#modified = true lodash.merge(this.#values, values) } + /** + * Reset store by clearing it's values. + */ + clear(): void { + this.update({}) + } + + /** + * Get value for a given key + */ + get(key: string, defaultValue?: any): any { + return lodash.get(this.#values, key, defaultValue) + } + /** * A boolean to know if value exists. Extra guards to check * arrays for it's length as well. diff --git a/tests/store.spec.ts b/tests/store.spec.ts index 092848e..cdf7d2b 100644 --- a/tests/store.spec.ts +++ b/tests/store.spec.ts @@ -14,17 +14,25 @@ test.group('Store', () => { test('return empty object for empty store', ({ assert }) => { const store = new Store(null) assert.deepEqual(store.toJSON(), {}) + assert.isTrue(store.isEmpty) + assert.isFalse(store.hasBeenModified) }) test('mutate values inside store', ({ assert }) => { const store = new Store({}) store.set('username', 'virk') + + assert.isFalse(store.isEmpty) + assert.isTrue(store.hasBeenModified) assert.deepEqual(store.toJSON(), { username: 'virk' }) }) test('mutate nested values inside store', ({ assert }) => { const store = new Store({}) store.set('user.username', 'virk') + + assert.isFalse(store.isEmpty) + assert.isTrue(store.hasBeenModified) assert.deepEqual(store.toJSON(), { user: { username: 'virk' } }) }) @@ -32,6 +40,9 @@ test.group('Store', () => { const store = new Store(null) store.set('user.username', 'virk') store.unset('user.username') + + assert.isFalse(store.isEmpty) + assert.isTrue(store.hasBeenModified) assert.deepEqual(store.toJSON(), { user: {} }) }) @@ -39,22 +50,43 @@ test.group('Store', () => { const store = new Store(null) store.set('user.age', 22) store.increment('user.age') + + assert.isFalse(store.isEmpty) + assert.isTrue(store.hasBeenModified) assert.deepEqual(store.toJSON(), { user: { age: 23 } }) }) + test('throw when incrementing a non integer value', () => { + const store = new Store(null) + store.set('user.age', 'foo') + store.increment('user.age') + }).throws('Cannot increment "user.age". Existing value is not a number') + test('decrement value inside store', ({ assert }) => { const store = new Store(null) store.set('user.age', 22) store.decrement('user.age') + + assert.isFalse(store.isEmpty) + assert.isTrue(store.hasBeenModified) assert.deepEqual(store.toJSON(), { user: { age: 21 } }) }) + test('throw when decrementing a non integer value', () => { + const store = new Store(null) + store.set('user.age', 'foo') + store.decrement('user.age') + }).throws('Cannot decrement "user.age". Existing value is not a number') + test('find if value exists in the store', ({ assert }) => { const store = new Store({}) assert.isFalse(store.has('username')) store.update({ username: 'virk' }) + assert.isTrue(store.has('username')) + assert.isFalse(store.isEmpty) + assert.isTrue(store.hasBeenModified) }) test('check for arrays length', ({ assert }) => { @@ -78,4 +110,50 @@ test.group('Store', () => { store.update({ users: ['virk'] }) assert.isTrue(store.has('users')) }) + + test('pull key from the store', ({ assert }) => { + const store = new Store({}) + store.set('username', 'virk') + + assert.equal(store.pull('username'), 'virk') + + assert.isTrue(store.isEmpty) + assert.isTrue(store.hasBeenModified) + assert.deepEqual(store.toJSON(), {}) + }) + + test('deep merge with existing values', ({ assert }) => { + const store = new Store({}) + store.set('user', { profile: { username: 'virk' }, id: 1 }) + store.merge({ user: { profile: { age: 32 } } }) + + assert.isFalse(store.isEmpty) + assert.isTrue(store.hasBeenModified) + + assert.deepEqual(store.toJSON(), { user: { id: 1, profile: { age: 32, username: 'virk' } } }) + }) + + test('clear store', ({ assert }) => { + const store = new Store({}) + store.set('user', { profile: { username: 'virk' }, id: 1 }) + store.clear() + + assert.isTrue(store.isEmpty) + assert.isTrue(store.hasBeenModified) + assert.deepEqual(store.toObject(), {}) + }) + + test('stringify store data object', ({ assert }) => { + const store = new Store({}) + store.set('user', { profile: { username: 'virk' }, id: 1 }) + store.merge({ user: { profile: { age: 32 } } }) + + assert.isFalse(store.isEmpty) + assert.isTrue(store.hasBeenModified) + + assert.equal( + store.toString(), + JSON.stringify({ user: { profile: { username: 'virk', age: 32 }, id: 1 } }) + ) + }) }) From 6b9a5530326ae08ae9132a6abe0d6306ea01983a Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 31 Jul 2023 17:49:51 +0530 Subject: [PATCH 046/112] refactor: finalize all drivers --- src/drivers/file.ts | 13 +-- src/drivers/memory.ts | 12 +-- src/drivers/redis.ts | 70 ++++-------- tests/drivers/cookie_driver.spec.ts | 159 ++++++++++++++++++++++++++++ tests/drivers/file_driver.spec.ts | 153 ++++++++++++++++++++++++++ tests/drivers/memory.spec.ts | 75 +++++++++++++ tests/drivers/redis_driver.spec.ts | 110 +++++++++++++++++++ 7 files changed, 528 insertions(+), 64 deletions(-) create mode 100644 tests/drivers/cookie_driver.spec.ts create mode 100644 tests/drivers/file_driver.spec.ts create mode 100644 tests/drivers/memory.spec.ts create mode 100644 tests/drivers/redis_driver.spec.ts diff --git a/src/drivers/file.ts b/src/drivers/file.ts index fab7ba6..a41bf01 100644 --- a/src/drivers/file.ts +++ b/src/drivers/file.ts @@ -13,17 +13,17 @@ import string from '@poppinss/utils/string' import { MessageBuilder } from '@poppinss/utils' import { access, mkdir, readFile, rm, writeFile, utimes, stat } from 'node:fs/promises' -import type { SessionData, SessionDriverContract } from '../types.js' +import type { FileDriverConfig, SessionData, SessionDriverContract } from '../types.js' /** * File driver writes the session data on the file system as. Each session * id gets its own file. */ export class FileDriver implements SessionDriverContract { - #config: { location: string } + #config: FileDriverConfig #age: string | number - constructor(config: { location: string }, age: string | number) { + constructor(config: FileDriverConfig, age: string | number) { this.#config = config this.#age = age } @@ -111,12 +111,7 @@ export class FileDriver implements SessionDriverContract { * method can fail when the contents is not JSON> */ try { - const verifiedContents = new MessageBuilder().verify(contents, sessionId) - if (typeof verifiedContents !== 'object') { - return null - } - - return verifiedContents + return new MessageBuilder().verify(contents, sessionId) } catch { return null } diff --git a/src/drivers/memory.ts b/src/drivers/memory.ts index 3209d0b..6976da3 100644 --- a/src/drivers/memory.ts +++ b/src/drivers/memory.ts @@ -7,29 +7,25 @@ * file that was distributed with this source code. */ -import { SessionDriverContract } from '../types.js' +import type { SessionData, SessionDriverContract } from '../types.js' /** * Memory driver is meant to be used for writing tests. */ export class MemoryDriver implements SessionDriverContract { - static sessions: Map = new Map() + static sessions: Map = new Map() /** * Read session id value from the memory */ - read(sessionId: string): { [key: string]: any } | null { + read(sessionId: string): SessionData | null { return MemoryDriver.sessions.get(sessionId) || null } /** * Save in memory value for a given session id */ - write(sessionId: string, values: { [key: string]: any }): void { - if (typeof values !== 'object') { - throw new Error('Session memory driver expects an object of values') - } - + write(sessionId: string, values: SessionData): void { MemoryDriver.sessions.set(sessionId, values) } diff --git a/src/drivers/redis.ts b/src/drivers/redis.ts index a4f481d..e23d7ca 100644 --- a/src/drivers/redis.ts +++ b/src/drivers/redis.ts @@ -7,92 +7,68 @@ * file that was distributed with this source code. */ -import { Exception } from '@poppinss/utils' import string from '@poppinss/utils/string' import { MessageBuilder } from '@poppinss/utils' -import type { RedisConnectionContract, RedisManagerContract } from '@adonisjs/redis/types' -import type { SessionDriverContract, SessionConfig } from '../types.js' +import type { RedisService } from '@adonisjs/redis/types' + +import type { SessionDriverContract, RedisDriverConfig, SessionData } from '../types.js' /** * File driver to read/write session to filesystem */ export class RedisDriver implements SessionDriverContract { - #config: SessionConfig - #redis: RedisManagerContract - #ttl: number + #config: RedisDriverConfig + #redis: RedisService + #ttlSeconds: number - constructor(config: SessionConfig, redis: RedisManagerContract) { + constructor(redis: RedisService, config: RedisDriverConfig, age: string | number) { this.#config = config this.#redis = redis - - /** - * Convert milliseconds to seconds - */ - this.#ttl = Math.round( - (typeof this.#config.age === 'string' - ? string.milliseconds.parse(this.#config.age) - : this.#config.age) / 1000 - ) - - if (!this.#config.redisConnection) { - throw new Exception( - 'Missing redisConnection for session redis driver inside "config/session" file', - { code: 'E_INVALID_SESSION_DRIVER_CONFIG', status: 500 } - ) - } - } - - /** - * Returns instance of the redis connection - */ - #getRedisConnection(): RedisConnectionContract { - return (this.#redis.connection as any)(this.#config.redisConnection) + this.#ttlSeconds = string.seconds.parse(age) } /** * Returns file contents. A new file will be created if it's * missing. */ - async read(sessionId: string): Promise<{ [key: string]: any } | null> { - const contents = await this.#getRedisConnection().get(sessionId) + async read(sessionId: string): Promise { + const contents = await this.#redis.connection(this.#config.connection).get(sessionId) if (!contents) { return null } - const verifiedContents = new MessageBuilder().verify(contents, sessionId) - if (typeof verifiedContents !== 'object') { + /** + * Verify contents with the session id and return them as an object. The verify + * method can fail when the contents is not JSON> + */ + try { + return new MessageBuilder().verify(contents, sessionId) + } catch { return null } - - return verifiedContents } /** * Write session values to a file */ async write(sessionId: string, values: Object): Promise { - if (typeof values !== 'object') { - throw new Error('Session file driver expects an object of values') - } - - await this.#getRedisConnection().setex( - sessionId, - this.#ttl, - new MessageBuilder().build(values, undefined, sessionId) - ) + const message = new MessageBuilder().build(values, undefined, sessionId) + await this.#redis + .connection(this.#config.connection) + .setex(sessionId, this.#ttlSeconds, message) } /** * Cleanup session file by removing it */ async destroy(sessionId: string): Promise { - await this.#getRedisConnection().del(sessionId) + await this.#redis.connection(this.#config.connection).del(sessionId) } /** * Updates the value expiry */ async touch(sessionId: string): Promise { - await this.#getRedisConnection().expire(sessionId, this.#ttl) + await this.#redis.connection(this.#config.connection).expire(sessionId, this.#ttlSeconds) } } diff --git a/tests/drivers/cookie_driver.spec.ts b/tests/drivers/cookie_driver.spec.ts new file mode 100644 index 0000000..ada21b1 --- /dev/null +++ b/tests/drivers/cookie_driver.spec.ts @@ -0,0 +1,159 @@ +/* + * @adonisjs/session + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import supertest from 'supertest' +import { test } from '@japa/runner' +import setCookieParser from 'set-cookie-parser' +import { CookieClient } from '@adonisjs/core/http' +import type { CookieOptions } from '@adonisjs/core/types/http' +import { EncryptionFactory } from '@adonisjs/core/factories/encryption' +import { HttpContextFactory, RequestFactory, ResponseFactory } from '@adonisjs/core/factories/http' + +import { httpServer } from '../../test_helpers/index.js' +import { CookieDriver } from '../../src/drivers/cookie.js' + +const encryption = new EncryptionFactory().create() +const cookieClient = new CookieClient(encryption) +const cookieConfig: Partial = { + sameSite: 'strict', + maxAge: '5mins', +} + +test.group('Cookie driver', () => { + test('return null when session data cookie does not exists', async ({ assert }) => { + const sessionId = '1234' + + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() + + const session = new CookieDriver(cookieConfig, ctx) + const value = session.read(sessionId) + response.json(value) + response.finish() + }) + + const { body, text } = await supertest(server).get('/') + assert.deepEqual(body, {}) + assert.equal(text, '') + }) + + test('return session data from the cookie', async ({ assert }) => { + const sessionId = '1234' + + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() + + const session = new CookieDriver(cookieConfig, ctx) + const value = session.read(sessionId) + response.json(value) + response.finish() + }) + + const { body } = await supertest(server) + .get('/') + .set('cookie', `${sessionId}=${cookieClient.encrypt(sessionId, { visits: 1 })}`) + + assert.deepEqual(body, { visits: 1 }) + }) + + test('persist session data inside a cookie', async ({ assert }) => { + const sessionId = '1234' + + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() + + const session = new CookieDriver(cookieConfig, ctx) + session.write(sessionId, { visits: 0 }) + response.finish() + }) + + const { headers } = await supertest(server).get('/') + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) + assert.deepEqual(cookieClient.decrypt(sessionId, cookies[sessionId].value), { + visits: 0, + }) + }) + + test('touch cookie by re-updating its attributes', async ({ assert }) => { + const sessionId = '1234' + + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() + + const session = new CookieDriver(cookieConfig, ctx) + session.touch(sessionId) + response.finish() + }) + + const { headers } = await supertest(server) + .get('/') + .set('Cookie', `${sessionId}=${cookieClient.encrypt(sessionId, { visits: 1 })}`) + + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) + assert.deepEqual(cookieClient.decrypt(sessionId, cookies[sessionId].value), { + visits: 1, + }) + }) + + test('do not write cookie to response unless touch or write methods are called', async ({ + assert, + }) => { + const sessionId = '1234' + + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() + + const session = new CookieDriver(cookieConfig, ctx) + response.json(session.read(sessionId)) + response.finish() + }) + + const { headers, body } = await supertest(server) + .get('/') + .set('Cookie', `${sessionId}=${cookieClient.encrypt(sessionId, { visits: 1 })}`) + + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) + assert.deepEqual(cookies, {}) + assert.deepEqual(body, { visits: 1 }) + }) + + test('delete session data cookie', async ({ assert }) => { + const sessionId = '1234' + + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() + + const session = new CookieDriver(cookieConfig, ctx) + response.json(session.read(sessionId)) + session.destroy(sessionId) + response.finish() + }) + + const { headers, body } = await supertest(server) + .get('/') + .set('Cookie', `${sessionId}=${cookieClient.encrypt(sessionId, { visits: 1 })}`) + + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) + assert.equal(cookies[sessionId].maxAge, -1) + assert.equal(cookies[sessionId].expires, new Date('1970-01-01').toString()) + assert.deepEqual(body, { visits: 1 }) + }) +}) diff --git a/tests/drivers/file_driver.spec.ts b/tests/drivers/file_driver.spec.ts new file mode 100644 index 0000000..8bc5246 --- /dev/null +++ b/tests/drivers/file_driver.spec.ts @@ -0,0 +1,153 @@ +/* + * @adonisjs/session + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { join } from 'node:path' +import { test } from '@japa/runner' +import { setTimeout } from 'node:timers/promises' + +import { FileDriver } from '../../src/drivers/file.js' + +test.group('File driver', () => { + test('do not create file for a new session', async ({ fs, assert }) => { + const sessionId = '1234' + const session = new FileDriver({ location: fs.basePath }, '2 hours') + + const value = await session.read(sessionId) + assert.isNull(value) + + await assert.fileNotExists('1234.txt') + }) + + test('create intermediate directories when missing', async ({ fs, assert }) => { + const sessionId = '1234' + const session = new FileDriver( + { + location: join(fs.basePath, 'app/sessions'), + }, + '2 hours' + ) + + await session.write(sessionId, { message: 'hello-world' }) + + await assert.fileExists('app/sessions/1234.txt') + await assert.fileEquals( + 'app/sessions/1234.txt', + JSON.stringify({ message: { message: 'hello-world' }, purpose: '1234' }) + ) + }) + + test('update existing session', async ({ fs, assert }) => { + const sessionId = '1234' + const session = new FileDriver( + { + location: fs.basePath, + }, + '2 hours' + ) + + await session.write(sessionId, { message: 'hello-world' }) + await assert.fileEquals( + '1234.txt', + JSON.stringify({ message: { message: 'hello-world' }, purpose: '1234' }) + ) + + await session.write(sessionId, { message: 'hi-world' }) + await assert.fileEquals( + '1234.txt', + JSON.stringify({ message: { message: 'hi-world' }, purpose: '1234' }) + ) + }) + + test('get session existing value', async ({ assert, fs }) => { + const sessionId = '1234' + const session = new FileDriver({ location: fs.basePath }, '2 hours') + await session.write(sessionId, { message: 'hello-world' }) + + const value = await session.read(sessionId) + assert.deepEqual(value, { message: 'hello-world' }) + }) + + test('return null when session data is expired', async ({ assert, fs }) => { + const sessionId = '1234' + const session = new FileDriver({ location: fs.basePath }, 1000) + await session.write(sessionId, { message: 'hello-world' }) + + await setTimeout(2000) + + const value = await session.read(sessionId) + assert.isNull(value) + }).disableTimeout() + + test('ignore malformed file contents', async ({ fs, assert }) => { + const sessionId = '1234' + const session = new FileDriver({ location: fs.basePath }, '2 hours') + + await fs.create('1234.txt', '') + assert.isNull(await session.read(sessionId)) + + await fs.create('1234.txt', 'foo') + assert.isNull(await session.read(sessionId)) + + await fs.create('1234.txt', JSON.stringify({ foo: 'bar' })) + assert.isNull(await session.read(sessionId)) + }) + + test('remove file on destroy', async ({ assert, fs }) => { + const sessionId = '1234' + + const session = new FileDriver({ location: fs.basePath }, '2 hours') + await session.write(sessionId, { message: 'hello-world' }) + await session.destroy(sessionId) + + await assert.fileNotExists('1234.txt') + }) + + test('do not fail when destroying a non-existing session', async ({ assert, fs }) => { + const sessionId = '1234' + + await assert.fileNotExists('1234.txt') + + const session = new FileDriver({ location: fs.basePath }, '2 hours') + await session.destroy(sessionId) + + await assert.fileNotExists('1234.txt') + }) + + test('update session expiry on touch', async ({ assert, fs }) => { + const sessionId = '1234' + + const session = new FileDriver({ location: fs.basePath }, '2 hours') + await session.write(sessionId, { message: 'hello-world' }) + + /** + * Waiting a bit + */ + await setTimeout(2000) + + /** + * Making sure the original mTime of the file was smaller + * than the current time after wait + */ + const { mtimeMs } = await fs.adapter.stat(join(fs.basePath, '1234.txt')) + assert.isBelow(mtimeMs, Date.now()) + + await session.touch(sessionId) + + /** + * Ensuring the new mTime is greater than the old mTime + */ + let { mtimeMs: newMtimeMs } = await fs.adapter.stat(join(fs.basePath, '1234.txt')) + assert.isAbove(newMtimeMs, mtimeMs) + + await assert.fileEquals( + '1234.txt', + JSON.stringify({ message: { message: 'hello-world' }, purpose: '1234' }) + ) + }).disableTimeout() +}) diff --git a/tests/drivers/memory.spec.ts b/tests/drivers/memory.spec.ts new file mode 100644 index 0000000..fb1281a --- /dev/null +++ b/tests/drivers/memory.spec.ts @@ -0,0 +1,75 @@ +/* + * @adonisjs/session + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { MemoryDriver } from '../../src/drivers/memory.js' + +test.group('Memory driver', (group) => { + group.each.setup(() => { + MemoryDriver.sessions.clear() + }) + + test('return null when session does not exists', async ({ assert }) => { + const sessionId = '1234' + const session = new MemoryDriver() + + assert.isNull(session.read(sessionId)) + }) + + test('write to session store', async ({ assert }) => { + const sessionId = '1234' + const session = new MemoryDriver() + session.write(sessionId, { message: 'hello-world' }) + + assert.isTrue(MemoryDriver.sessions.has(sessionId)) + assert.deepEqual(MemoryDriver.sessions.get(sessionId), { message: 'hello-world' }) + }) + + test('update existing session', async ({ assert }) => { + const sessionId = '1234' + const session = new MemoryDriver() + + session.write(sessionId, { message: 'hello-world' }) + assert.isTrue(MemoryDriver.sessions.has(sessionId)) + assert.deepEqual(MemoryDriver.sessions.get(sessionId), { message: 'hello-world' }) + + session.write(sessionId, { foo: 'bar' }) + assert.isTrue(MemoryDriver.sessions.has(sessionId)) + assert.deepEqual(MemoryDriver.sessions.get(sessionId), { foo: 'bar' }) + }) + + test('get session existing value', async ({ assert }) => { + const sessionId = '1234' + const session = new MemoryDriver() + + session.write(sessionId, { message: 'hello-world' }) + assert.isTrue(MemoryDriver.sessions.has(sessionId)) + assert.deepEqual(session.read(sessionId), { message: 'hello-world' }) + }) + + test('remove session on destroy', async ({ assert }) => { + const sessionId = '1234' + const session = new MemoryDriver() + + session.write(sessionId, { message: 'hello-world' }) + session.destroy(sessionId) + + assert.isFalse(MemoryDriver.sessions.has(sessionId)) + }) + + test('noop on touch', async ({ assert }) => { + const sessionId = '1234' + const session = new MemoryDriver() + + session.write(sessionId, { message: 'hello-world' }) + session.touch() + + assert.deepEqual(MemoryDriver.sessions.get(sessionId), { message: 'hello-world' }) + }) +}) diff --git a/tests/drivers/redis_driver.spec.ts b/tests/drivers/redis_driver.spec.ts new file mode 100644 index 0000000..d3f790c --- /dev/null +++ b/tests/drivers/redis_driver.spec.ts @@ -0,0 +1,110 @@ +/* + * @adonisjs/session + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { defineConfig } from '@adonisjs/redis' +import { setTimeout } from 'node:timers/promises' +import { RedisService } from '@adonisjs/redis/types' +import { RedisManagerFactory } from '@adonisjs/redis/factories' + +import { RedisDriver } from '../../src/drivers/redis.js' + +const sessionId = '1234' +const redisConfig = defineConfig({ + connection: 'main', + connections: { + main: { + host: process.env.REDIS_HOST || '0.0.0.0', + port: process.env.REDIS_PORT || 6379, + }, + }, +}) +const redis = new RedisManagerFactory(redisConfig).create() as RedisService +declare module '@adonisjs/redis/types' { + interface RedisConnections extends InferConnections {} +} + +test.group('Redis driver', (group) => { + group.each.setup(() => { + return async () => { + await redis.del(sessionId) + } + }) + + test('return null when value is missing', async ({ assert }) => { + const session = new RedisDriver(redis, { connection: 'main' }, '2 hours') + const value = await session.read(sessionId) + assert.isNull(value) + }) + + test('save session data in a set', async ({ assert }) => { + const session = new RedisDriver(redis, { connection: 'main' }, '2 hours') + await session.write(sessionId, { message: 'hello-world' }) + + assert.equal( + await redis.get(sessionId), + JSON.stringify({ + message: { message: 'hello-world' }, + purpose: sessionId, + }) + ) + }) + + test('return null when session data is expired', async ({ assert }) => { + const session = new RedisDriver(redis, { connection: 'main' }, 1) + await session.write(sessionId, { message: 'hello-world' }) + + await setTimeout(2000) + + const value = await session.read(sessionId) + assert.isNull(value) + }).disableTimeout() + + test('ignore malformed contents', async ({ assert }) => { + const session = new RedisDriver(redis, { connection: 'main' }, 1) + await redis.set(sessionId, 'foo') + + const value = await session.read(sessionId) + assert.isNull(value) + }) + + test('delete key on destroy', async ({ assert }) => { + const session = new RedisDriver(redis, { connection: 'main' }, '2 hours') + + await session.write(sessionId, { message: 'hello-world' }) + await session.destroy(sessionId) + + assert.isNull(await redis.get(sessionId)) + }) + + test('update session expiry on touch', async ({ assert }) => { + const session = new RedisDriver(redis, { connection: 'main' }, 10) + await session.write(sessionId, { message: 'hello-world' }) + + /** + * Waiting a bit + */ + await setTimeout(2000) + + /** + * Making sure the original mTime of the file was smaller + * than the current time after wait + */ + const expiry = await redis.ttl(sessionId) + assert.isBelow(expiry, 9) + + await session.touch(sessionId) + + /** + * Ensuring the new mTime is greater than the old mTime + */ + const expiryPostTouch = await redis.ttl(sessionId) + assert.isAtLeast(expiryPostTouch, 9) + }).disableTimeout() +}) From 26065d13e3c3f1882a4f0c81bd2e26e6a0d7f487 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 31 Jul 2023 17:50:37 +0530 Subject: [PATCH 047/112] refactor: finalize define_config implementation --- src/define_config.ts | 49 +++++++++++++++++++++++++++++++------ tests/define_config.spec.ts | 43 ++++++++++++++++++++++++-------- 2 files changed, 74 insertions(+), 18 deletions(-) diff --git a/src/define_config.ts b/src/define_config.ts index ed0cdde..44bed7d 100644 --- a/src/define_config.ts +++ b/src/define_config.ts @@ -1,17 +1,50 @@ +/* + * @adonisjs/session + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import string from '@poppinss/utils/string' import { InvalidArgumentsException } from '@poppinss/utils' -import { SessionConfig } from './types.js' +import type { CookieOptions } from '@adonisjs/core/types/http' + +import type { SessionConfig } from './types.js' /** - * Helper to define session config + * Helper to normalize session config */ -export function defineConfig(config: SessionConfig) { - if (!config.cookieName) { - throw new InvalidArgumentsException('Missing "cookieName" property inside the session config') - } - +export function defineConfig( + config: Partial +): SessionConfig & { cookie: Partial } { + /** + * Make sure a driver is defined + */ if (!config.driver) { throw new InvalidArgumentsException('Missing "driver" property inside the session config') } - return config + const age = config.age || '2h' + const clearWithBrowser = config.clearWithBrowser ?? false + const cookieOptions: Partial = { ...config.cookie } + + /** + * Define maxAge property when session id cookie is + * not a session cookie. + */ + if (!clearWithBrowser) { + cookieOptions.maxAge = string.seconds.parse(config.age || age) + } + + return { + enabled: true, + age, + clearWithBrowser, + cookieName: 'adonis_session', + cookie: cookieOptions, + driver: config.driver!, + ...config, + } } diff --git a/tests/define_config.spec.ts b/tests/define_config.spec.ts index d717f0b..b992674 100644 --- a/tests/define_config.spec.ts +++ b/tests/define_config.spec.ts @@ -1,18 +1,41 @@ +/* + * @adonisjs/redis + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import { test } from '@japa/runner' import { defineConfig } from '../src/define_config.js' test.group('Define config', () => { - test('should throws if no driver defined', ({ assert }) => { - assert.throws( - () => defineConfig({ cookieName: 'hey' } as any), - 'Missing "driver" property inside the session config' - ) + test('throw error when driver is not defined', () => { + defineConfig({}) + }).throws('Missing "driver" property inside the session config') + + test('define maxAge when clearWithBrowser is not enabled', ({ assert }) => { + assert.equal(defineConfig({ driver: 'cookie' }).cookie.maxAge, 7200) + assert.equal(defineConfig({ driver: 'cookie', clearWithBrowser: false }).cookie.maxAge, 7200) + }) + + test('do not define maxAge when clearWithBrowser is true', ({ assert }) => { + assert.isUndefined(defineConfig({ driver: 'cookie', clearWithBrowser: true }).cookie.maxAge) }) - test('should throws if no cookieName defined', ({ assert }) => { - assert.throws( - () => defineConfig({ driver: 'cookie' } as any), - 'Missing "cookieName" property inside the session config' - ) + test('normalize config', ({ assert }) => { + assert.snapshot(defineConfig({ driver: 'cookie' })).matchInline(` + { + "age": "2h", + "clearWithBrowser": false, + "cookie": { + "maxAge": 7200, + }, + "cookieName": "adonis_session", + "driver": "cookie", + "enabled": true, + } + `) }) }) From 4dd39bef11d22beb273695f8c523ce0e717d8966 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 31 Jul 2023 17:50:56 +0530 Subject: [PATCH 048/112] feat: add drivers collection --- src/drivers_collection.ts | 56 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/drivers_collection.ts diff --git a/src/drivers_collection.ts b/src/drivers_collection.ts new file mode 100644 index 0000000..b79e57f --- /dev/null +++ b/src/drivers_collection.ts @@ -0,0 +1,56 @@ +/* + * @adonisjs/redis + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { RuntimeException } from '@poppinss/utils' +import type { HttpContext } from '@adonisjs/core/http' + +import type { SessionDriversList } from './types.js' + +/** + * A global collection of session drivers + */ +class SessionDriversCollection { + /** + * List of registered drivers + */ + list: Partial = {} + + /** + * Extend drivers collection and add a custom + * driver to it. + */ + extend( + driverName: Name, + factoryCallback: SessionDriversList[Name] + ): this { + this.list[driverName] = factoryCallback + return this + } + + /** + * Creates the driver instance with config + */ + create( + name: Name, + config: Parameters[0], + ctx: HttpContext + ): ReturnType { + const driverFactory = this.list[name] + if (!driverFactory) { + throw new RuntimeException( + `Unknown redis driver "${String(name)}". Make sure the driver is registered` + ) + } + + return driverFactory(config as any, ctx) as ReturnType + } +} + +const sessionDriversList = new SessionDriversCollection() +export default sessionDriversList From e25c9070540ba45f39f42d7b772c031dad8d2b00 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 31 Jul 2023 17:51:14 +0530 Subject: [PATCH 049/112] feat: abstract errors to their own file --- src/errors.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/errors.ts diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000..21ca81d --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,28 @@ +/* + * @adonisjs/session + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { createError } from '@poppinss/utils' + +/** + * Raised when session store is not mutable + */ +export const E_SESSION_NOT_MUTABLE = createError( + 'Session store is in readonly mode and cannot be mutated', + 'E_SESSION_NOT_MUTABLE', + 500 +) + +/** + * Raised when session store has been initiated + */ +export const E_SESSION_NOT_READY = createError( + 'Session store has not been initiated. Make sure you have registered the session middleware', + 'E_SESSION_NOT_READY', + 500 +) From 29f0a02aeaa1f3523f60527673f8d4208f0e62a8 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 31 Jul 2023 19:14:00 +0530 Subject: [PATCH 050/112] refactor: cleaning things --- bin/test.ts | 5 +- factories/main.ts | 10 + factories/session_manager_factory.ts | 71 -- factories/session_middleware_factory.ts | 58 ++ index.ts | 8 +- package.json | 14 +- providers/session_provider.ts | 56 +- src/bindings/api_client.ts | 169 ---- src/bindings/http_context.ts | 24 - src/bindings/types.ts | 98 -- src/client.ts | 84 +- src/define_config.ts | 2 +- src/drivers/cookie.ts | 2 +- src/drivers/file.ts | 2 +- src/drivers/memory.ts | 2 +- src/drivers/redis.ts | 2 +- src/drivers_collection.ts | 2 +- src/helpers.ts | 49 + src/session.ts | 484 +++++---- src/session_manager.ts | 187 ---- src/session_middleware.ts | 52 +- src/store.ts | 2 +- src/types/extended.ts | 21 + src/{types.ts => types/main.ts} | 61 +- test_helpers/index.ts | 137 --- tests/concurrent_session.spec.ts | 371 +++++++ tests/cookie_driver.spec.ts | 159 --- tests/file_driver.spec.ts | 126 --- tests/redis_driver.spec.ts | 128 --- tests/session.spec.ts | 1192 ++++++++++++----------- tests/session_client.spec.ts | 200 +--- tests/session_manager.spec.ts | 169 ---- tests/session_middleware.spec.ts | 99 +- tests/session_provider.spec.ts | 300 +++--- tsconfig.json | 3 - 35 files changed, 1807 insertions(+), 2542 deletions(-) create mode 100644 factories/main.ts delete mode 100644 factories/session_manager_factory.ts create mode 100644 factories/session_middleware_factory.ts delete mode 100644 src/bindings/api_client.ts delete mode 100644 src/bindings/http_context.ts delete mode 100644 src/bindings/types.ts create mode 100644 src/helpers.ts delete mode 100644 src/session_manager.ts create mode 100644 src/types/extended.ts rename src/{types.ts => types/main.ts} (65%) create mode 100644 tests/concurrent_session.spec.ts delete mode 100644 tests/cookie_driver.spec.ts delete mode 100644 tests/file_driver.spec.ts delete mode 100644 tests/redis_driver.spec.ts delete mode 100644 tests/session_manager.spec.ts diff --git a/bin/test.ts b/bin/test.ts index 697c352..1777bb0 100644 --- a/bin/test.ts +++ b/bin/test.ts @@ -1,4 +1,5 @@ import { assert } from '@japa/assert' +import { snapshot } from '@japa/snapshot' import { fileSystem } from '@japa/file-system' import { processCLIArgs, configure, run } from '@japa/runner' @@ -17,8 +18,8 @@ import { processCLIArgs, configure, run } from '@japa/runner' */ processCLIArgs(process.argv.slice(2)) configure({ - files: ['tests/file_driver.spec.ts'], - plugins: [assert(), fileSystem()], + files: ['tests/**/*.spec.ts'], + plugins: [assert(), fileSystem(), snapshot()], forceExit: true, }) diff --git a/factories/main.ts b/factories/main.ts new file mode 100644 index 0000000..556c554 --- /dev/null +++ b/factories/main.ts @@ -0,0 +1,10 @@ +/* + * @adonisjs/session + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export { SessionMiddlewareFactory } from './session_middleware_factory.js' diff --git a/factories/session_manager_factory.ts b/factories/session_manager_factory.ts deleted file mode 100644 index 688ddeb..0000000 --- a/factories/session_manager_factory.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * @adonisjs/session - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import type { SessionConfig } from '../src/types.js' -import type { Application } from '@adonisjs/application' -import type { RedisConnectionConfig } from '@adonisjs/redis/types' -import { EncryptionFactory } from '@adonisjs/core/factories/encryption' -import { SessionManager } from '../src/session_manager.js' -import { RedisManagerFactory } from '@adonisjs/redis/factories' - -/** - * Session Manager Factory is used to create an instance of - * session manager for testing - */ -export class SessionManagerFactory { - /** - * Default configuration for the session manager - */ - #options: SessionConfig = { - enabled: true, - driver: 'cookie', - cookieName: 'adonis-session', - clearWithBrowser: false, - age: 3000, - cookie: { path: '/' }, - } - - /** - * Configuration for the redis manager - */ - #redisManagerOptions = { - connection: 'local', - connections: { local: { host: '127.0.0.1', port: 6379 } }, - } as const - - /** - * Merge factory parameters - */ - merge(options: SessionConfig) { - this.#options = Object.assign(this.#options, options) - return this - } - - /** - * Merge redis manager parameters - */ - mergeRedisManagerOptions>(options: { - connection: keyof Connections - connections: Connections - }) { - this.#redisManagerOptions = Object.assign(this.#redisManagerOptions, options) - return this - } - - /** - * Create Session manager instance - */ - create(app: Application) { - return new SessionManager( - this.#options, - new EncryptionFactory().create(), - new RedisManagerFactory(this.#redisManagerOptions).create(app) - ) - } -} diff --git a/factories/session_middleware_factory.ts b/factories/session_middleware_factory.ts new file mode 100644 index 0000000..faf8053 --- /dev/null +++ b/factories/session_middleware_factory.ts @@ -0,0 +1,58 @@ +/* + * @adonisjs/session + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Emitter } from '@adonisjs/core/events' +import { ApplicationService, EventsList } from '@adonisjs/core/types' +import { AppFactory } from '@adonisjs/core/factories/app' + +import { defineConfig } from '../index.js' +import { SessionConfig } from '../src/types/main.js' +import { registerSessionDriver } from '../src/helpers.js' +import SessionMiddleware from '../src/session_middleware.js' + +/** + * Exposes the API to create an instance of the session middleware + * without additional plumbing + */ +export class SessionMiddlewareFactory { + #config: Partial = { driver: 'memory' } + #emitter?: Emitter + + #getApp() { + return new AppFactory().create(new URL('./', import.meta.url), () => {}) as ApplicationService + } + + #getEmitter() { + return this.#emitter || new Emitter(this.#getApp()) + } + + /** + * Merge custom options + */ + merge(options: { config?: Partial; emitter?: Emitter }) { + if (options.config) { + this.#config = options.config + } + + if (options.emitter) { + this.#emitter = options.emitter + } + + return this + } + + /** + * Creates an instance of the session middleware + */ + async create() { + const config = defineConfig(this.#config) + await registerSessionDriver(this.#getApp(), config.driver) + return new SessionMiddleware(config, this.#getEmitter()) + } +} diff --git a/index.ts b/index.ts index f421a82..eacea28 100644 --- a/index.ts +++ b/index.ts @@ -7,8 +7,10 @@ * file that was distributed with this source code. */ -import './src/bindings/types.js' +import './src/types/extended.js' -export { defineConfig } from './src/define_config.js' -export { stubsRoot } from './stubs/main.js' +export * as errors from './src/errors.js' export { configure } from './configure.js' +export { stubsRoot } from './stubs/main.js' +export { defineConfig } from './src/define_config.js' +export { default as sessionDriversList } from './src/drivers_collection.js' diff --git a/package.json b/package.json index f409c27..a5cbdea 100644 --- a/package.json +++ b/package.json @@ -11,15 +11,16 @@ "build/src", "build/stubs", "build/providers", - "build/index.d.ts", - "build/index.js", "build/configure.d.ts", - "build/configure.js" + "build/configure.js", + "build/index.d.ts", + "build/index.js" ], "exports": { ".": "./build/index.js", "./session_provider": "./build/providers/session_provider.js", "./session_middleware": "./build/src/session_middleware.js", + "./client": "./build/src/client.js", "./types": "./build/src/types.js" }, "scripts": { @@ -42,13 +43,14 @@ "@adonisjs/core": "^6.1.5-12", "@adonisjs/eslint-config": "^1.1.8", "@adonisjs/prettier-config": "^1.1.8", - "@adonisjs/redis": "^8.0.0-7", + "@adonisjs/redis": "^8.0.0-8", "@adonisjs/tsconfig": "^1.1.8", "@japa/api-client": "^2.0.0-0", "@japa/assert": "^2.0.0-1", "@japa/file-system": "^2.0.0-1", "@japa/plugin-adonisjs": "^2.0.0-1", "@japa/runner": "^3.0.0-6", + "@japa/snapshot": "^2.0.0-1", "@swc/core": "^1.3.70", "@types/node": "^20.4.2", "@types/set-cookie-parser": "^2.4.3", @@ -121,7 +123,9 @@ ], "exclude": [ "tests/**", - "stubs/**" + "stubs/**", + "factories/**", + "bin/**" ] } } diff --git a/providers/session_provider.ts b/providers/session_provider.ts index 4b7be0c..f72d013 100644 --- a/providers/session_provider.ts +++ b/providers/session_provider.ts @@ -7,58 +7,30 @@ * file that was distributed with this source code. */ -import type { SessionManager } from '../src/session_manager.js' import type { ApplicationService } from '@adonisjs/core/types' -import { extendHttpContext } from '../src/bindings/http_context.js' + +import { registerSessionDriver } from '../src/helpers.js' import SessionMiddleware from '../src/session_middleware.js' +/** + * Session provider configures the session management inside an + * AdonisJS application + */ export default class SessionProvider { constructor(protected app: ApplicationService) {} - /** - * Register Session Manager in the container - */ - async register() { - this.app.container.singleton('session', async () => { - const { SessionManager } = await import('../src/session_manager.js') - - const encryption = await this.app.container.make('encryption') - const redis = await this.app.container.make('redis').catch(() => undefined) + register() { + this.app.container.bind(SessionMiddleware, async (resolver) => { const config = this.app.config.get('session', {}) - - return new SessionManager(config, encryption, redis) - }) - - this.app.container.bind(SessionMiddleware, async () => { - const session = await this.app.container.make('session') - return new SessionMiddleware(session) + const emitter = await resolver.make('emitter') + return new SessionMiddleware(config, emitter) }) } - /** - * Register Japa API Client bindings - */ - async #registerApiClientBindings(session: SessionManager) { - if (this.app.getEnvironment() === 'test') { - const { extendApiClient } = await import('../src/bindings/api_client.js') - extendApiClient(session) - } - } - - /** - * Register bindings - */ async boot() { - const session = await this.app.container.make('session') - - /** - * Add `session` getter to the HttpContext class - */ - extendHttpContext(session) - - /** - * Extend Japa API Client - */ - await this.#registerApiClientBindings(session) + this.app.container.resolving(SessionMiddleware, async () => { + const config = this.app.config.get('session') + await registerSessionDriver(this.app, config.driver) + }) } } diff --git a/src/bindings/api_client.ts b/src/bindings/api_client.ts deleted file mode 100644 index 28c1d49..0000000 --- a/src/bindings/api_client.ts +++ /dev/null @@ -1,169 +0,0 @@ -/* - * @adonisjs/session - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { ApiRequest, ApiResponse, ApiClient } from '@japa/api-client' -import { InspectOptions, inspect } from 'node:util' -import { SessionManager } from '../session_manager.js' -import { AllowedSessionValues } from '../types.js' - -export function extendApiClient(sessionManager: SessionManager) { - /** - * Set "sessionClient" on the api request - */ - ApiRequest.getter( - 'sessionClient', - function () { - return sessionManager.client() - }, - true - ) - - /** - * Send session values in the request - */ - ApiRequest.macro( - 'session', - function (this: ApiRequest, session: Record) { - if (!this.sessionClient.isEnabled()) { - throw new Error('Cannot set session. Make sure to enable it inside "config/session" file') - } - - this.sessionClient.merge(session) - - return this - } - ) - - /** - * Send flash messages in the request - */ - ApiRequest.macro( - 'flashMessages', - function (this: ApiRequest, messages: Record) { - if (!this.sessionClient.isEnabled()) { - throw new Error( - 'Cannot set flash messages. Make sure to enable the session inside "config/session" file' - ) - } - - this.sessionClient.flashMessages.merge(messages) - return this - } - ) - - /** - * Returns reference to the session data from the session - * jar - */ - ApiResponse.macro('session', function (this: ApiResponse) { - return this.sessionJar.session - }) - - /** - * Returns reference to the flash messages from the session - * jar - */ - ApiResponse.macro('flashMessages', function (this: ApiResponse) { - return this.sessionJar.flashMessages || {} - }) - - /** - * Assert response to contain a given session and optionally - * has the expected value - */ - ApiResponse.macro('assertSession', function (this: ApiResponse, name: string, value?: any) { - this.ensureHasAssert() - this.assert!.property(this.session(), name) - - if (value !== undefined) { - this.assert!.deepEqual(this.session()[name], value) - } - }) - - /** - * Assert response to not contain a given session - */ - ApiResponse.macro('assertSessionMissing', function (this: ApiResponse, name: string) { - this.ensureHasAssert() - this.assert!.notProperty(this.session(), name) - }) - - /** - * Assert response to contain a given flash message and optionally - * has the expected value - */ - ApiResponse.macro('assertFlashMessage', function (this: ApiResponse, name: string, value?: any) { - this.ensureHasAssert() - this.assert!.property(this.flashMessages(), name) - - if (value !== undefined) { - this.assert!.deepEqual(this.flashMessages()[name], value) - } - }) - - /** - * Assert response to not contain a given session - */ - ApiResponse.macro('assertFlashMissing', function (this: ApiResponse, name: string) { - this.ensureHasAssert() - this.assert!.notProperty(this.flashMessages(), name) - }) - - /** - * Dump session to the console - */ - ApiResponse.macro('dumpSession', function (this: ApiResponse, options?: InspectOptions) { - const inspectOptions = { depth: 2, showHidden: false, colors: true, ...options } - console.log(`"session" => ${inspect(this.session(), inspectOptions)}`) - console.log(`"flashMessages" => ${inspect(this.flashMessages(), inspectOptions)}`) - - return this - }) - - /** - * Adding hooks directly on the request object moves the hooks to - * the end of the queue (basically after the globally hooks) - */ - ApiClient.onRequest((req) => { - /** - * Hook into request and persist session data to be available - * on the server during the request. - */ - req.setup(async (request) => { - /** - * Persist session data and set the session id within the - * cookie - */ - const { cookieName, sessionId } = await request.sessionClient.commit() - request.withCookie(cookieName, sessionId) - - /** - * Cleanup if request has error. Otherwise the teardown - * hook will clear - */ - return async (error: any) => { - if (error) { - await request.sessionClient.forget() - } - } - }) - - /** - * Load messages from the session store and keep a reference to it - * inside the response object. - * - * We also destroy the session after getting a copy of the session - * data - */ - req.teardown(async (response) => { - response.sessionJar = await response.request.sessionClient.load(response.cookies()) - await response.request.sessionClient.forget() - }) - }) -} diff --git a/src/bindings/http_context.ts b/src/bindings/http_context.ts deleted file mode 100644 index bb59691..0000000 --- a/src/bindings/http_context.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * @adonisjs/session - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { HttpContext } from '@adonisjs/core/http' -import { SessionManager } from '../session_manager.js' - -/** - * Extends HttpContext class with the ally getter - */ -export function extendHttpContext(session: SessionManager) { - HttpContext.getter( - 'session', - function (this: HttpContext) { - return session.create(this) - }, - true - ) -} diff --git a/src/bindings/types.ts b/src/bindings/types.ts deleted file mode 100644 index 3d84e7e..0000000 --- a/src/bindings/types.ts +++ /dev/null @@ -1,98 +0,0 @@ -/* - * @adonisjs/session - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { InspectOptions } from 'node:util' -import type { SessionClient } from '../client.js' -import type { Session } from '../session.js' -import type { SessionManager } from '../session_manager.js' -import type { AllowedSessionValues } from '../types.js' - -/** - * HttpContext augmentations - */ -declare module '@adonisjs/core/http' { - interface HttpContext { - session: Session - } -} - -/** - * Container augmentations - */ -declare module '@adonisjs/core/types' { - interface ContainerBindings { - session: SessionManager - } -} - -/** - * Japa api client augmentations - */ -declare module '@japa/api-client' { - export interface ApiRequest { - sessionClient: SessionClient - - /** - * Send session values in the request - */ - session(session: Record): this - - /** - * Send flash messages in the request - */ - flashMessages(messages: Record): this - } - - export interface ApiResponse { - /** - * A copy of session data loaded from the driver - */ - sessionJar: { - session: Record - flashMessages: Record | null - } - - /** - * Get session data - */ - session(): Record - - /** - * Dump session - */ - dumpSession(options?: InspectOptions): this - - /** - * Get flash messages set by the server - */ - flashMessages(): Record - - /** - * Assert response to contain a given session and optionally - * has the expected value - */ - assertSession(key: string, value?: any): void - - /** - * Assert response to not contain a given session - */ - assertSessionMissing(key: string): void - - /** - * Assert response to contain a given flash message and optionally - * has the expected value - */ - assertFlashMessage(key: string, value?: any): void - - /** - * Assert response to not contain a given session - */ - assertFlashMissing(key: string): void - } -} diff --git a/src/client.ts b/src/client.ts index 54ffdcf..ae8abe0 100644 --- a/src/client.ts +++ b/src/client.ts @@ -8,21 +8,22 @@ */ import { cuid } from '@adonisjs/core/helpers' -import { Store } from './store.js' -import type { SessionConfig, SessionDriverContract } from './types.js' import type { CookieClient } from '@adonisjs/core/http' +import { Store } from './store.js' +import type { SessionConfig, SessionData, SessionDriverContract } from './types/main.js' + /** - * SessionClient exposes the API to set session data as a client + * Session client exposes the API to set session data as a client */ -export class SessionClient extends Store { +export class SessionClient { /** * Session configuration */ #config: SessionConfig /** - * The session driver used to read and write session data + * The session driver to use for reading and writing session data */ #driver: SessionDriverContract @@ -32,54 +33,38 @@ export class SessionClient extends Store { #cookieClient: CookieClient /** - * Each instance of client works on a single session id. Generate - * multiple client instances for a different session id + * Session to use when no explicit session id is + * defined */ - sessionId = cuid() + #sessionId = cuid() /** * Session key for setting flash messages */ - #flashMessagesKey = '__flash__' - - /** - * Flash messages store. They are merged with the session data during - * commit - */ - flashMessages = new Store({}) - - constructor( - config: SessionConfig, - driver: SessionDriverContract, - cookieClient: CookieClient, - values: { [key: string]: any } | null - ) { - super(values) + flashKey = '__flash__' + constructor(config: SessionConfig, driver: SessionDriverContract, cookieClient: CookieClient) { this.#config = config this.#driver = driver this.#cookieClient = cookieClient } /** - * Find if the sessions are enabled + * Load session data from the driver */ - isEnabled() { - return this.#config.enabled - } - - /** - * Load session from the driver - */ - async load(cookies: Record) { + async load( + cookies: Record, + sessionId?: string + ): Promise<{ sessionId: string; session: SessionData; flashMessages: SessionData }> { const sessionIdCookie = cookies[this.#config.cookieName] - const sessionId = sessionIdCookie ? sessionIdCookie.value : this.sessionId + const sessId = sessionId || sessionIdCookie ? sessionIdCookie.value : this.#sessionId - const contents = await this.#driver.read(sessionId) + const contents = await this.#driver.read(sessId) const store = new Store(contents) - const flashMessages = store.pull(this.#flashMessagesKey, null) + const flashMessages = store.pull(this.flashKey, null) return { + sessionId: sessId, session: store.all(), flashMessages, } @@ -90,19 +75,19 @@ export class SessionClient extends Store { * the session id and cookie name for it to be accessible * by the server */ - async commit() { - this.set(this.#flashMessagesKey, this.flashMessages.all()) - await this.#driver.write(this.sessionId, this.toJSON()) + async commit(values: SessionData | null, flashMessages: SessionData | null, sessionId?: string) { + const sessId = sessionId || this.#sessionId /** - * Clear from the session client memory + * Persist session data to the store, alongside flash messages */ - this.clear() - this.flashMessages.clear() + if (values || flashMessages) { + await this.#driver.write(sessId, Object.assign({ [this.flashKey]: flashMessages }, values)) + } return { - sessionId: this.sessionId!, - signedSessionId: this.#cookieClient.sign(this.#config.cookieName, this.sessionId)!, + sessionId: sessId, + signedSessionId: this.#cookieClient.sign(this.#config.cookieName, sessId)!, cookieName: this.#config.cookieName, } } @@ -110,16 +95,7 @@ export class SessionClient extends Store { /** * Clear the session store */ - async forget() { - /** - * Clear from the session client memory - */ - this.clear() - this.flashMessages.clear() - - /** - * Clear with the driver - */ - await this.#driver.destroy(this.sessionId) + async forget(sessionId?: string) { + await this.#driver.destroy(sessionId || this.#sessionId) } } diff --git a/src/define_config.ts b/src/define_config.ts index 44bed7d..d22a8a4 100644 --- a/src/define_config.ts +++ b/src/define_config.ts @@ -11,7 +11,7 @@ import string from '@poppinss/utils/string' import { InvalidArgumentsException } from '@poppinss/utils' import type { CookieOptions } from '@adonisjs/core/types/http' -import type { SessionConfig } from './types.js' +import type { SessionConfig } from './types/main.js' /** * Helper to normalize session config diff --git a/src/drivers/cookie.ts b/src/drivers/cookie.ts index 91d611a..fda29ed 100644 --- a/src/drivers/cookie.ts +++ b/src/drivers/cookie.ts @@ -9,7 +9,7 @@ import type { HttpContext } from '@adonisjs/core/http' import { CookieOptions } from '@adonisjs/core/types/http' -import type { SessionData, SessionDriverContract } from '../types.js' +import type { SessionData, SessionDriverContract } from '../types/main.js' /** * Cookie driver stores the session data inside an encrypted diff --git a/src/drivers/file.ts b/src/drivers/file.ts index a41bf01..682c53c 100644 --- a/src/drivers/file.ts +++ b/src/drivers/file.ts @@ -13,7 +13,7 @@ import string from '@poppinss/utils/string' import { MessageBuilder } from '@poppinss/utils' import { access, mkdir, readFile, rm, writeFile, utimes, stat } from 'node:fs/promises' -import type { FileDriverConfig, SessionData, SessionDriverContract } from '../types.js' +import type { FileDriverConfig, SessionData, SessionDriverContract } from '../types/main.js' /** * File driver writes the session data on the file system as. Each session diff --git a/src/drivers/memory.ts b/src/drivers/memory.ts index 6976da3..e9605dc 100644 --- a/src/drivers/memory.ts +++ b/src/drivers/memory.ts @@ -7,7 +7,7 @@ * file that was distributed with this source code. */ -import type { SessionData, SessionDriverContract } from '../types.js' +import type { SessionData, SessionDriverContract } from '../types/main.js' /** * Memory driver is meant to be used for writing tests. diff --git a/src/drivers/redis.ts b/src/drivers/redis.ts index e23d7ca..d60d8ba 100644 --- a/src/drivers/redis.ts +++ b/src/drivers/redis.ts @@ -11,7 +11,7 @@ import string from '@poppinss/utils/string' import { MessageBuilder } from '@poppinss/utils' import type { RedisService } from '@adonisjs/redis/types' -import type { SessionDriverContract, RedisDriverConfig, SessionData } from '../types.js' +import type { SessionDriverContract, RedisDriverConfig, SessionData } from '../types/main.js' /** * File driver to read/write session to filesystem diff --git a/src/drivers_collection.ts b/src/drivers_collection.ts index b79e57f..2387e72 100644 --- a/src/drivers_collection.ts +++ b/src/drivers_collection.ts @@ -10,7 +10,7 @@ import { RuntimeException } from '@poppinss/utils' import type { HttpContext } from '@adonisjs/core/http' -import type { SessionDriversList } from './types.js' +import type { SessionDriversList } from './types/main.js' /** * A global collection of session drivers diff --git a/src/helpers.ts b/src/helpers.ts new file mode 100644 index 0000000..50d56ad --- /dev/null +++ b/src/helpers.ts @@ -0,0 +1,49 @@ +/* + * @adonisjs/session + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import type { ApplicationService } from '@adonisjs/core/types' + +import sessionDriversList from './drivers_collection.js' +import type { SessionDriversList } from './types/main.js' + +/** + * Lazily imports and registers a driver with the sessionDriversList + */ +export async function registerSessionDriver( + app: ApplicationService, + driverInUse: keyof SessionDriversList +) { + if (driverInUse === 'cookie') { + const { CookieDriver } = await import('../src/drivers/cookie.js') + sessionDriversList.extend('cookie', (config, ctx) => new CookieDriver(config.cookie, ctx)) + return + } + + if (driverInUse === 'memory') { + const { MemoryDriver } = await import('../src/drivers/memory.js') + sessionDriversList.extend('memory', () => new MemoryDriver()) + return + } + + if (driverInUse === 'file') { + const { FileDriver } = await import('../src/drivers/file.js') + sessionDriversList.extend('file', (config) => new FileDriver(config.file!, config.age)) + return + } + + if (driverInUse === 'redis') { + const { RedisDriver } = await import('../src/drivers/redis.js') + const redis = await app.container.make('redis') + sessionDriversList.extend( + 'redis', + (config) => new RedisDriver(redis, config.redis!, config.age) + ) + return + } +} diff --git a/src/session.ts b/src/session.ts index a948d60..d6e58ba 100644 --- a/src/session.ts +++ b/src/session.ts @@ -7,424 +7,408 @@ * file that was distributed with this source code. */ -import type { SessionConfig, SessionDriverContract, AllowedSessionValues } from './types.js' -import type { HttpContext } from '@adonisjs/core/http' -import { Exception } from '@poppinss/utils' import lodash from '@poppinss/utils/lodash' import { cuid } from '@adonisjs/core/helpers' +import { EmitterService } from '@adonisjs/core/types' +import type { HttpContext } from '@adonisjs/core/http' import { Store } from './store.js' +import * as errors from './errors.js' +import type { + SessionData, + SessionConfig, + AllowedSessionValues, + SessionDriverContract, +} from './types/main.js' /** - * Session class exposes the API to read/write values to the session for - * a given request. + * The session class exposes the API to read and write values to + * the session store. + * + * A session instance is isolated between requests but + * uses a centralized persistence store and */ export class Session { - /** - * Session id for the current request. It will be different - * from the "this.sessionId" when regenerate is called. - */ - #currentSessionId: string - - /** - * A instance of store with values read from the driver. The store - * in initiated inside the [[initiate]] method - */ - #store!: Store - - /** - * Whether or not to re-generate the session id before committing - * session values. - */ - #regeneratedSessionId = false - - /** - * Session key for setting flash messages - */ - #flashMessagesKey = '__flash__' - - /** - * The HTTP context for the current request. - */ - #ctx: HttpContext - - /** - * Configuration for the session - */ #config: SessionConfig - - /** - * The session driver instance used to read and write session data. - */ #driver: SessionDriverContract + #emitter: EmitterService + #ctx: HttpContext + #readonly: boolean = false + #store?: Store /** - * Set to true inside the `initiate` method + * Session id refers to the session id that will be committed + * as a cookie during the response. */ - initiated = false + #sessionId: string /** - * A boolean to know if it's a fresh session or not. Fresh - * sessions are those, whose session id is not present - * in cookie + * Session id from cookie refers to the value we read from the + * cookie during the HTTP request. + * + * This only might not exist during the first request. Also during + * session id re-generation, this value will be different from + * the session id. */ - fresh = false + #sessionIdFromCookie?: string /** - * A boolean to know if store is initiated in readonly mode - * or not. This is done during Websocket requests + * Store of flash messages that be written during the + * HTTP request */ - readonly = false + responseFlashMessages = new Store({}) /** - * Session id for the given request. A new session id is only - * generated when the cookie for the session id is missing + * Store of flash messages for the current HTTP request. */ - sessionId: string + flashMessages = new Store({}) /** - * A copy of previously set flash messages + * The key to use for storing flash messages inside + * the session store. */ - flashMessages = new Store({}) + flashKey: string = '__flash__' /** - * A copy of flash messages. The `input` messages - * are overwritten when any of the input related - * methods are used. - * - * The `others` object is expanded with each call. + * Session id for the current HTTP request */ - responseFlashMessages = new Store({}) - - constructor(ctx: HttpContext, config: SessionConfig, driver: SessionDriverContract) { - this.#ctx = ctx - this.#config = config - this.#driver = driver - - this.sessionId = this.#getSessionId() - this.#currentSessionId = this.sessionId + get sessionId() { + return this.#sessionId } /** - * Returns a merged copy of flash messages or null - * when nothing is set + * A boolean to know if a fresh session is created during + * the request */ - #setFlashMessages(): void { - if (this.responseFlashMessages.isEmpty) { - return - } - - const { input, ...others } = this.responseFlashMessages.all() - this.put(this.#flashMessagesKey, { ...input, ...others }) + get fresh(): boolean { + return this.#sessionIdFromCookie === undefined } /** - * Returns the existing session id or creates one. + * A boolean to know if session is in readonly + * state */ - #getSessionId(): string { - const sessionId = this.#ctx.request.cookie(this.#config.cookieName) - if (sessionId) { - return sessionId - } - - this.fresh = true - return cuid() + get readonly() { + return this.#readonly } /** - * Ensures the session store is initialized + * A boolean to know if session store has been initiated */ - #ensureIsReady(): void { - if (!this.initiated) { - throw new Exception( - 'Session store is not initiated yet. Make sure you are using the session hook', - { code: 'E_RUNTIME_EXCEPTION', status: 500 } - ) - } + get initiated() { + return !!this.#store } /** - * Raises exception when session store is in readonly mode + * A boolean to know if the session id has been re-generated + * during the current request */ - #ensureIsMutable() { - if (this.readonly) { - throw new Exception('Session store is in readonly mode and cannot be mutated', { - status: 500, - code: 'E_RUNTIME_EXCEPTION', - }) - } + get hasRegeneratedSession() { + return !!(this.#sessionIdFromCookie && this.#sessionIdFromCookie !== this.#sessionId) } /** - * Touches the session cookie + * A boolean to know if the session store is empty */ - #touchSessionCookie(): void { - this.#ctx.logger.trace('touching session cookie') - this.#ctx.response.cookie(this.#config.cookieName, this.sessionId, this.#config.cookie!) + get isEmpty() { + return this.#store?.isEmpty ?? true } /** - * Commits the session value to the store + * A boolean to know if the session store has been + * modified */ - async #commitValuesToStore(): Promise { - this.#ctx.logger.trace('persist session store with driver') - await this.#driver.write(this.sessionId, this.#store.toJSON()) + get hasBeenModified() { + return this.#store?.hasBeenModified ?? false } - /** - * Touches the driver to make sure the session values doesn't expire - */ - async #touchDriver(): Promise { - this.#ctx.logger.trace('touch driver for liveliness') - await this.#driver.touch(this.sessionId) + constructor( + config: SessionConfig, + driver: SessionDriverContract, + emitter: EmitterService, + ctx: HttpContext + ) { + this.#ctx = ctx + this.#config = config + this.#driver = driver + this.#emitter = emitter + this.#sessionIdFromCookie = ctx.request.cookie(config.cookieName, undefined) + this.#sessionId = this.#sessionIdFromCookie || cuid() } /** - * Reading flash messages from the last HTTP request and - * updating the flash messages bag + * Returns the flash messages store for a given + * mode */ - #readLastRequestFlashMessage() { - if (this.readonly) { - return + #getFlashStore(mode: 'write' | 'read'): Store { + if (!this.#store) { + throw new errors.E_SESSION_NOT_READY() } - this.flashMessages.update(this.pull(this.#flashMessagesKey, null)) + if (mode === 'write' && this.readonly) { + throw new errors.E_SESSION_NOT_MUTABLE() + } + + return this.responseFlashMessages } /** - * Share flash messages & read only session's functions with views - * (only when view property exists) + * Returns the store instance for a given mode */ - #shareLocalsWithView() { - if (!this.#ctx['view'] || typeof this.#ctx['view'].share !== 'function') { - return + #getStore(mode: 'write' | 'read'): Store { + if (!this.#store) { + throw new errors.E_SESSION_NOT_READY() + } + + if (mode === 'write' && this.readonly) { + throw new errors.E_SESSION_NOT_MUTABLE() } - this.#ctx['view'].share({ - flashMessages: this.flashMessages, - session: { - get: this.get.bind(this), - has: this.has.bind(this), - all: this.all.bind(this), - }, - }) + return this.#store } /** - * Initiating the session by reading it's value from the - * driver and feeding it to a store. - * - * Multiple calls to `initiate` results in a noop. + * Initiates the session store. The method results in a noop + * when called multiple times */ async initiate(readonly: boolean): Promise { - if (this.initiated) { + if (this.#store) { return } - this.readonly = readonly - - const contents = await this.#driver.read(this.sessionId) + this.#readonly = readonly + const contents = await this.#driver.read(this.#sessionId) this.#store = new Store(contents) - this.initiated = true - this.#readLastRequestFlashMessage() - this.#shareLocalsWithView() - } + /** + * Extract flash messages from the store and keep a local + * copy of it. + */ + if (this.has(this.flashKey)) { + if (this.#readonly) { + this.flashMessages.update(this.get(this.flashKey, null)) + } else { + this.flashMessages.update(this.pull(this.flashKey, null)) + } + } - /** - * Re-generates the session id. This can is used to avoid - * session fixation attacks. - */ - regenerate(): void { - this.#ctx.logger.trace('explicitly re-generating session id') - this.sessionId = cuid() - this.#regeneratedSessionId = true + this.#emitter.emit('session:initiated', { session: this }) } /** - * Set/update session value + * Put a key-value pair to the session data store */ - put(key: string, value: AllowedSessionValues): void { - this.#ensureIsReady() - this.#ensureIsMutable() - this.#store.set(key, value) + put(key: string, value: AllowedSessionValues) { + this.#getStore('write').set(key, value) } /** - * Find if the value exists in the session + * Check if a key exists inside the datastore */ has(key: string): boolean { - this.#ensureIsReady() - return this.#store.has(key) + return this.#getStore('read').has(key) } /** - * Get value from the session. The default value is returned - * when actual value is `undefined` + * Get the value of a key from the session datastore. + * You can specify a default value to use, when key + * does not exists or has undefined value. */ - get(key: string, defaultValue?: any): any { - this.#ensureIsReady() - return this.#store.get(key, defaultValue) + get(key: string, defaultValue?: any) { + return this.#getStore('read').get(key, defaultValue) } /** - * Returns everything from the session + * Get everything from the session store */ - all(): any { - this.#ensureIsReady() - return this.#store.all() + all() { + return this.#getStore('read').all() } /** - * Remove value for a given key from the session + * Remove a key from the session datastore */ - forget(key: string): void { - this.#ensureIsReady() - this.#ensureIsMutable() - this.#store.unset(key) + forget(key: string) { + return this.#getStore('write').unset(key) } /** - * The method is equivalent to calling `session.get` followed - * by `session.forget` + * Read value for a key from the session datastore + * and remove it simultaneously. */ - pull(key: string, defaultValue?: any): any { - this.#ensureIsReady() - this.#ensureIsMutable() - return this.#store.pull(key, defaultValue) + pull(key: string, defaultValue?: any) { + return this.#getStore('write').pull(key, defaultValue) } /** - * Increment value for a number inside the session store. The - * method raises an error when underlying value is not - * a number + * Increment the value of a key inside the session + * store. + * + * A new key will be defined if does not exists already. + * The value of a new key will be 1 */ - increment(key: string, steps: number = 1): void { - this.#ensureIsReady() - this.#ensureIsMutable() - this.#store.increment(key, steps) + increment(key: string, steps: number = 1) { + return this.#getStore('write').increment(key, steps) } /** - * Decrement value for a number inside the session store. The - * method raises an error when underlying value is not - * a number + * Increment the value of a key inside the session + * store. + * + * A new key will be defined if does not exists already. + * The value of a new key will be -1 */ - decrement(key: string, steps: number = 1): void { - this.#ensureIsReady() - this.#ensureIsMutable() - this.#store.decrement(key, steps) + decrement(key: string, steps: number = 1) { + return this.#getStore('write').decrement(key, steps) } /** - * Remove everything from the session + * Empty the session store */ - clear(): void { - this.#ensureIsReady() - this.#ensureIsMutable() - this.#store.clear() + clear() { + return this.#getStore('write').clear() } /** - * Add a new flash message + * Add a key-value pair to flash messages */ - flash(key: string | { [key: string]: AllowedSessionValues }, value?: AllowedSessionValues): void { - this.#ensureIsReady() - this.#ensureIsMutable() - - /** - * Update value - */ + flash(key: string, value: AllowedSessionValues): void + flash(keyValue: SessionData): void + flash(key: string | SessionData, value?: AllowedSessionValues): void { if (typeof key === 'string') { if (value) { - this.responseFlashMessages.set(key, value) + this.#getFlashStore('write').set(key, value) } } else { - this.responseFlashMessages.merge(key) + this.#getFlashStore('write').merge(key) } } /** - * Flash all form values + * Flash form input data to the flash messages store */ - flashAll(): void { - this.#ensureIsReady() - this.#ensureIsMutable() - this.responseFlashMessages.set('input', this.#ctx.request.original()) + flashAll() { + return this.#getFlashStore('write').set('input', this.#ctx.request.original()) } /** - * Flash all form values except mentioned keys + * Flash form input data (except some keys) to the flash messages store */ flashExcept(keys: string[]): void { - this.#ensureIsReady() - this.#ensureIsMutable() - this.responseFlashMessages.set('input', lodash.omit(this.#ctx.request.original(), keys)) + this.#getFlashStore('write').set('input', lodash.omit(this.#ctx.request.original(), keys)) } /** - * Flash only defined keys from the form values + * Flash form input data (only some keys) to the flash messages store */ flashOnly(keys: string[]): void { - this.#ensureIsReady() - this.#ensureIsMutable() - this.responseFlashMessages.set('input', lodash.pick(this.#ctx.request.original(), keys)) + this.#getFlashStore('write').set('input', lodash.pick(this.#ctx.request.original(), keys)) } /** - * Reflash existing flash messages + * Reflash messages from the last request in the current response */ - reflash() { - this.flash(this.flashMessages.all()) + reflash(): void { + this.#getFlashStore('write').set('reflashed', this.flashMessages.all()) } /** - * Reflash selected keys from the existing flash messages + * Reflash messages (only some keys) from the last + * request in the current response */ reflashOnly(keys: string[]) { - this.flash(lodash.pick(this.flashMessages.all(), keys)) + this.#getFlashStore('write').set('reflashed', lodash.pick(this.flashMessages.all(), keys)) } /** - * Omit selected keys from the existing flash messages - * and flash the rest of values + * Reflash messages (except some keys) from the last + * request in the current response */ reflashExcept(keys: string[]) { - this.flash(lodash.omit(this.flashMessages.all(), keys)) + this.#getFlashStore('write').set('reflashed', lodash.omit(this.flashMessages.all(), keys)) } /** - * Writes value to the underlying session driver. + * Re-generate the session id and migrate data to it. */ - async commit(): Promise { - if (!this.initiated) { - this.#touchSessionCookie() - await this.#touchDriver() + regenerate() { + this.#sessionId = cuid() + } + + /** + * Commit session changes. No more mutations will be + * allowed after commit. + */ + async commit() { + if (!this.#store || this.readonly) { return } /** - * Cleanup old session and re-generate new session + * If the flash messages store is not empty, we should put + * its messages inside main session store. */ - if (this.#regeneratedSessionId) { - await this.#driver.destroy(this.#currentSessionId) + if (!this.responseFlashMessages.isEmpty) { + const { input, reflashed, ...others } = this.responseFlashMessages.all() + this.put(this.flashKey, { ...reflashed, ...input, ...others }) } /** - * Touch the session cookie to keep it alive. + * Touch the session id cookie to stay alive */ - this.#touchSessionCookie() - this.#setFlashMessages() + this.#ctx.response.cookie(this.#config.cookieName, this.#sessionId, this.#config.cookie!) /** - * Commit values to the store if not empty. - * Otherwise delete the session store to cleanup - * the storage space. + * Delete the session data when the session store + * is empty. + * + * Also we only destroy the session id we read from the cookie. + * If there was no session id in the cookie, there won't be + * any data inside the store either. */ - if (!this.#store.isEmpty) { - await this.#commitValuesToStore() + if (this.isEmpty) { + if (this.#sessionIdFromCookie) { + await this.#driver.destroy(this.#sessionIdFromCookie) + } + this.#emitter.emit('session:committed', { session: this }) + return + } + + /** + * Touch the store expiry when the session store was + * not modified. + */ + if (!this.hasBeenModified) { + if (this.#sessionIdFromCookie && this.#sessionIdFromCookie !== this.#sessionId) { + await this.#driver.destroy(this.#sessionIdFromCookie) + await this.#driver.write(this.#sessionId, this.#store.toJSON()) + this.#emitter.emit('session:migrated', { + fromSessionId: this.#sessionIdFromCookie, + toSessionId: this.sessionId, + session: this, + }) + } else { + await this.#driver.touch(this.#sessionId) + } + this.#emitter.emit('session:committed', { session: this }) + return + } + + /** + * Otherwise commit to the session store + */ + if (this.#sessionIdFromCookie && this.#sessionIdFromCookie !== this.#sessionId) { + await this.#driver.destroy(this.#sessionIdFromCookie) + await this.#driver.write(this.#sessionId, this.#store.toJSON()) + this.#emitter.emit('session:migrated', { + fromSessionId: this.#sessionIdFromCookie, + toSessionId: this.sessionId, + session: this, + }) } else { - await this.#driver.destroy(this.sessionId) + await this.#driver.write(this.#sessionId, this.#store.toJSON()) } + + this.#emitter.emit('session:committed', { session: this }) } } diff --git a/src/session_manager.ts b/src/session_manager.ts deleted file mode 100644 index 5aea9cf..0000000 --- a/src/session_manager.ts +++ /dev/null @@ -1,187 +0,0 @@ -/** - * @adonisjs/session - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import string from '@poppinss/utils/string' -import { Exception } from '@poppinss/utils' - -import { Session } from './session.js' -import { CookieClient, HttpContext } from '@adonisjs/core/http' -import { CookieDriver } from './drivers/cookie.js' -import { MemoryDriver } from './drivers/memory.js' -import { FileDriver } from './drivers/file.js' -import { RedisDriver } from './drivers/redis.js' -import { ExtendCallback, SessionConfig, SessionDriverContract } from './types.js' -import { RedisManagerContract } from '@adonisjs/redis/types' -import { Encryption } from '@adonisjs/core/encryption' -import { SessionClient } from './client.js' - -type SessionManagerConfig = SessionConfig & { - cookie: { - expires: undefined - maxAge: number | undefined - } -} - -/** - * Session manager exposes the API to create session instance for a given - * request and also add new drivers. - */ -export class SessionManager { - /** - * A private map of drivers added from outside in. - */ - #extendedDrivers: Map = new Map() - - /** - * Reference to session config - */ - #config!: SessionManagerConfig - - /** - * Reference to the encryption instance - */ - #encryption: Encryption - - /** - * Reference to the redis manager - */ - #redis?: RedisManagerContract - - constructor(config: SessionConfig, encryption: Encryption, redis?: RedisManagerContract) { - this.#encryption = encryption - this.#redis = redis - - this.#processConfig(config) - } - - /** - * Processes the config and decides the `expires` option for the cookie - */ - #processConfig(config: SessionConfig): void { - /** - * Explicitly overwriting `cookie.expires` and `cookie.maxAge` from - * the user defined config - */ - const processedConfig: SessionManagerConfig = Object.assign({ enabled: true }, config, { - cookie: { - ...config.cookie, - expires: undefined, - maxAge: undefined, - }, - }) - - /** - * Set the max age when `clearWithBrowser = false`. Otherwise cookie - * is a session cookie - */ - if (!processedConfig.clearWithBrowser) { - const age = - typeof processedConfig.age === 'string' - ? Math.round(string.milliseconds.parse(processedConfig.age) / 1000) - : processedConfig.age - - processedConfig.cookie.maxAge = age - } - - this.#config = processedConfig - } - - /** - * Returns an instance of cookie driver - */ - #createCookieDriver(ctx: HttpContext) { - return new CookieDriver(this.#config, ctx) - } - - /** - * Returns an instance of the memory driver - */ - #createMemoryDriver() { - return new MemoryDriver() - } - - /** - * Returns an instance of file driver - */ - #createFileDriver() { - return new FileDriver(this.#config) - } - - /** - * Returns an instance of redis driver - */ - #createRedisDriver() { - if (!this.#redis) { - throw new Error( - 'Install "@adonisjs/redis" in order to use the redis driver for storing sessions' - ) - } - - return new RedisDriver(this.#config, this.#redis) - } - - /** - * Creates an instance of extended driver - */ - #createExtendedDriver(ctx: HttpContext): any { - if (!this.#extendedDrivers.has(this.#config.driver)) { - throw new Exception(`"${this.#config.driver}" is not a valid session driver`, { - code: 'E_INVALID_SESSION_DRIVER', - status: 500, - }) - } - - return this.#extendedDrivers.get(this.#config.driver)!(this, this.#config, ctx) - } - - #createDriver(ctx: HttpContext): SessionDriverContract { - switch (this.#config.driver) { - case 'cookie': - return this.#createCookieDriver(ctx) - case 'file': - return this.#createFileDriver() - case 'redis': - return this.#createRedisDriver() - case 'memory': - return this.#createMemoryDriver() - default: - return this.#createExtendedDriver(ctx) - } - } - - /** - * Find if the sessions are enabled - */ - isEnabled() { - return this.#config.enabled - } - - /** - * Creates an instance of the session client - */ - client() { - const cookieClient = new CookieClient(this.#encryption) - - return new SessionClient(this.#config, this.#createMemoryDriver(), cookieClient, {}) - } - - /** - * Creates a new session instance for a given HTTP request - */ - create(ctx: HttpContext) { - return new Session(ctx, this.#config, this.#createDriver(ctx)) - } - - /** - * Extend the drivers list by adding a new one. - */ - extend(driver: string, callback: ExtendCallback): void { - this.#extendedDrivers.set(driver, callback) - } -} diff --git a/src/session_middleware.ts b/src/session_middleware.ts index 3415391..68beddc 100644 --- a/src/session_middleware.ts +++ b/src/session_middleware.ts @@ -1,15 +1,50 @@ -import type { HttpContext } from '@adonisjs/core/http' +/* + * @adonisjs/session + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { HttpContext } from '@adonisjs/core/http' +import { EmitterService } from '@adonisjs/core/types' import type { NextFn } from '@adonisjs/core/types/http' -import { SessionManager } from './session_manager.js' +import { Session } from './session.js' +import type { SessionConfig } from './types/main.js' +import sessionDriversList from './drivers_collection.js' + +/** + * HttpContext augmentations + */ +declare module '@adonisjs/core/http' { + interface HttpContext { + session: Session + } +} + +/** + * Session middleware is used to initiate the session store + * and commit its values during an HTTP request + */ export default class SessionMiddleware { - constructor(protected session: SessionManager) {} + #config: SessionConfig + #emitter: EmitterService + + constructor(config: SessionConfig, emitter: EmitterService) { + this.#config = config + this.#emitter = emitter + } async handle(ctx: HttpContext, next: NextFn) { - if (!this.session.isEnabled()) { - return + if (!this.#config.enabled) { + return next() } + const driver = sessionDriversList.create(this.#config.driver, this.#config, ctx) + ctx.session = new Session(this.#config, driver, this.#emitter, ctx) + /** * Initiate session store */ @@ -18,11 +53,16 @@ export default class SessionMiddleware { /** * Call next middlewares or route handler */ - await next() + const response = await next() /** * Commit store mutations */ await ctx.session.commit() + + /** + * Return response + */ + return response } } diff --git a/src/store.ts b/src/store.ts index 5691fb7..f7164db 100644 --- a/src/store.ts +++ b/src/store.ts @@ -10,7 +10,7 @@ import lodash from '@poppinss/utils/lodash' import { RuntimeException } from '@poppinss/utils' -import type { AllowedSessionValues, SessionData } from './types.js' +import type { AllowedSessionValues, SessionData } from './types/main.js' /** * Session store encapsulates the session data and offers a diff --git a/src/types/extended.ts b/src/types/extended.ts new file mode 100644 index 0000000..02e7a58 --- /dev/null +++ b/src/types/extended.ts @@ -0,0 +1,21 @@ +/* + * @adonisjs/session + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import type { Session } from '../session.js' + +/** + * Events emitted by the session class + */ +declare module '@adonisjs/core/types' { + interface EventsList { + 'session:initiated': { session: Session } + 'session:committed': { session: Session } + 'session:migrated': { fromSessionId: string; toSessionId: string; session: Session } + } +} diff --git a/src/types.ts b/src/types/main.ts similarity index 65% rename from src/types.ts rename to src/types/main.ts index 2694e39..a5adb86 100644 --- a/src/types.ts +++ b/src/types/main.ts @@ -8,18 +8,13 @@ */ import type { HttpContext } from '@adonisjs/core/http' +import { RedisConnections } from '@adonisjs/redis/types' import type { CookieOptions } from '@adonisjs/core/types/http' -import type { SessionManager } from './session_manager.js' -/** - * The callback to be passed to the `extend` method. It is invoked - * for each request (if extended driver is in use). - */ -export type ExtendCallback = ( - manager: SessionManager, - config: SessionConfig, - ctx: HttpContext -) => SessionDriverContract +import type { FileDriver } from '../drivers/file.js' +import type { RedisDriver } from '../drivers/redis.js' +import type { MemoryDriver } from '../drivers/memory.js' +import type { CookieDriver } from '../drivers/cookie.js' /** * The values allowed by the `session.put` method @@ -68,7 +63,7 @@ export interface SessionConfig { /** * The drivers to use */ - driver: string + driver: keyof SessionDriversList /** * The name of the cookie for storing the session id. @@ -90,6 +85,8 @@ export interface SessionConfig { * * The session id cookie will also live for the same duration, unless * "clearWithBrowser" is enabled + * + * The value should be a time expression or a number in seconds */ age: string | number @@ -98,17 +95,37 @@ export interface SessionConfig { * session id cookie. */ cookie: Omit, 'maxAge' | 'expires'> +} - /** - * Configuration used by the file driver. - */ - file?: { - location: string - } +/** + * Configuration used by the file driver. + */ +export type FileDriverConfig = { + location: string +} - /** - * Reference to the redis connection name to use for - * storing the session data. - */ - redisConnection?: string +/** + * Configuration used by the redis driver. + */ +export type RedisDriverConfig = { + connection: keyof RedisConnections +} + +/** + * Extending session config with the drivers config + */ +export interface SessionConfig { + file?: FileDriverConfig + redis?: RedisDriverConfig +} + +/** + * List of the session drivers. The list can be extended using + * declaration merging + */ +export interface SessionDriversList { + file: (config: SessionConfig, ctx: HttpContext) => FileDriver + cookie: (config: SessionConfig, ctx: HttpContext) => CookieDriver + redis: (config: SessionConfig, ctx: HttpContext) => RedisDriver + memory: (config: SessionConfig, ctx: HttpContext) => MemoryDriver } diff --git a/test_helpers/index.ts b/test_helpers/index.ts index cd2c13e..2a25bf6 100644 --- a/test_helpers/index.ts +++ b/test_helpers/index.ts @@ -21,140 +21,3 @@ export const httpServer = { return server }, } - -// import { IgnitorFactory } from '@adonisjs/core/factories' -// import { FileSystem } from '@japa/file-system' -// import { SessionConfig } from '../src/types.js' -// import { ApplicationService } from '@adonisjs/core/types' -// import { RedisService } from '@adonisjs/redis/types' -// import { RedisManagerFactory } from '@adonisjs/redis/factories' -// import { Application } from '@adonisjs/core/app' -// import { IncomingMessage, ServerResponse } from 'node:http' -// import { Server } from '@adonisjs/core/http' -// import { pluginAdonisJS } from '@japa/plugin-adonisjs' -// import { Runner } from '@japa/runner/core' - -// /** -// * Session default config -// */ -// export const sessionConfig: SessionConfig = { -// enabled: true, -// driver: 'cookie', -// cookieName: 'adonis-session', -// clearWithBrowser: false, -// age: 3000, -// cookie: { -// path: '/', -// }, -// } - -// export const BASE_URL = new URL('./tmp/', import.meta.url) - -// export async function setup(fs: FileSystem, config?: any, environment: 'web' | 'test' = 'web') { -// const IMPORTER = (filePath: string) => { -// if (filePath.startsWith('./') || filePath.startsWith('../')) { -// return import(new URL(filePath, BASE_URL).href) -// } -// return import(filePath) -// } - -// const ignitor = new IgnitorFactory() -// .withCoreConfig() -// .withCoreProviders() -// .merge({ -// rcFileContents: { -// providers: ['../../providers/session_provider.js', '@adonisjs/redis/redis_provider'], -// }, -// config: config || { session: sessionConfig }, -// }) -// .create(fs.baseUrl, { importer: IMPORTER }) - -// const app = ignitor.createApp(environment) - -// await app.init() -// await app.boot() - -// // @ts-ignore -// await pluginAdonisJS(app)({ -// runner: new Runner({} as any), -// }) - -// return { app, ignitor } -// } - -// /** -// * Sleep for a while -// */ -// export function sleep(time: number): Promise { -// return new Promise((resolve) => setTimeout(resolve, time)) -// } - -// /** -// * Signs value to be set as cookie header -// */ -// export async function signCookie(app: ApplicationService, value: any, name: string) { -// const encryption = await app.container.make('encryption') -// return `${name}=s:${encryption.verifier.sign(value, undefined, name)}` -// } - -// /** -// * Encrypt value to be set as cookie header -// */ -// export async function encryptCookie(app: ApplicationService, value: any, name: string) { -// const encryption = await app.container.make('encryption') -// return `${name}=e:${encryption.encrypt(value, undefined, name)}` -// } - -// /** -// * Decrypt cookie -// */ -// export async function decryptCookie(app: ApplicationService, header: any, name: string) { -// const encryption = await app.container.make('encryption') -// const cookieValue = decodeURIComponent(header['set-cookie'][0].split(';')[0]) -// .replace(`${name}=`, '') -// .slice(2) - -// return encryption.decrypt(cookieValue, name) -// } - -// /** -// * Unsign cookie -// */ -// export async function unsignCookie(app: ApplicationService, header: any, name: string) { -// const encryption = await app.container.make('encryption') - -// const cookieValue = decodeURIComponent(header['set-cookie'][0].split(';')[0]) -// .replace(`${name}=`, '') -// .slice(2) - -// return encryption.verifier.unsign(cookieValue, name) -// } - -// /** -// * Reference to the redis manager -// */ -// export function getRedisManager(application: ApplicationService) { -// return new RedisManagerFactory({ -// connection: 'session', -// connections: { -// session: { -// host: process.env.REDIS_HOST || '0.0.0.0', -// port: process.env.REDIS_PORT || 6379, -// }, -// }, -// }).create(application) as RedisService -// } - -// export async function createHttpContext( -// app: Application, -// req: IncomingMessage, -// res: ServerResponse -// ) { -// const adonisServer = (await app.container.make('server')) as Server - -// const request = adonisServer.createRequest(req, res) -// const response = adonisServer.createResponse(req, res) -// const ctx = adonisServer.createHttpContext(request, response, app.container.createResolver()) - -// return ctx -// } diff --git a/tests/concurrent_session.spec.ts b/tests/concurrent_session.spec.ts new file mode 100644 index 0000000..d032e8b --- /dev/null +++ b/tests/concurrent_session.spec.ts @@ -0,0 +1,371 @@ +/* + * @adonisjs/session + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import supertest from 'supertest' +import { test } from '@japa/runner' +import { cuid } from '@adonisjs/core/helpers' +import { defineConfig } from '@adonisjs/redis' +import setCookieParser from 'set-cookie-parser' +import { Emitter } from '@adonisjs/core/events' +import { setTimeout } from 'node:timers/promises' +import { EventsList } from '@adonisjs/core/types' +import { RedisService } from '@adonisjs/redis/types' +import { AppFactory } from '@adonisjs/core/factories/app' +import { IncomingMessage, ServerResponse } from 'node:http' +import { RedisManagerFactory } from '@adonisjs/redis/factories' +import { CookieClient, HttpContext } from '@adonisjs/core/http' +import { EncryptionFactory } from '@adonisjs/core/factories/encryption' +import { HttpContextFactory, RequestFactory, ResponseFactory } from '@adonisjs/core/factories/http' + +import { Session } from '../src/session.js' +import { FileDriver } from '../src/drivers/file.js' +import { httpServer } from '../test_helpers/index.js' +import { CookieDriver } from '../src/drivers/cookie.js' +import type { SessionConfig, SessionDriverContract } from '../src/types/main.js' +import { RedisDriver } from '../src/drivers/redis.js' + +const app = new AppFactory().create(new URL('./', import.meta.url), () => {}) +const emitter = new Emitter(app) +const encryption = new EncryptionFactory().create() +const cookieClient = new CookieClient(encryption) +const sessionConfig: SessionConfig = { + enabled: true, + age: '2 hours', + clearWithBrowser: false, + cookieName: 'adonis_session', + driver: 'cookie', + cookie: {}, +} + +const redisConfig = defineConfig({ + connection: 'main', + connections: { + main: { + host: process.env.REDIS_HOST || '0.0.0.0', + port: process.env.REDIS_PORT || 6379, + }, + }, +}) +const redis = new RedisManagerFactory(redisConfig).create() as RedisService +declare module '@adonisjs/redis/types' { + interface RedisConnections extends InferConnections {} +} + +/** + * Re-usable request handler that creates different session scanerios + * based upon the request URL. + */ +async function requestHandler( + req: IncomingMessage, + res: ServerResponse, + driver: (ctx: HttpContext) => SessionDriverContract +) { + try { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() + + const session = new Session(sessionConfig, driver(ctx), emitter, ctx) + await session.initiate(false) + + if (req.url === '/read-data') { + await session.commit() + + response.json(session.all()) + return response.finish() + } + + if (req.url === '/read-data-slowly') { + await setTimeout(2000) + await session.commit() + + response.json(session.all()) + return response.finish() + } + + if (req.url === '/write-data') { + session.put('username', 'virk') + await session.commit() + + response.json(session.all()) + return response.finish() + } + + if (req.url === '/write-data-slowly') { + await setTimeout(2000) + + session.put('email', 'foo@bar.com') + await session.commit() + + response.json(session.all()) + return response.finish() + } + } catch (error) { + res.writeHead(500) + res.write(error.stack) + res.end() + } +} + +test.group('Concurrency | cookie driver', () => { + test('concurrently read and read slowly', async ({ assert }) => { + let sessionId = cuid() + + const server = httpServer.create((req, res) => + requestHandler(req, res, (ctx) => new CookieDriver(sessionConfig.cookie, ctx)) + ) + + const sessionIdCookie = `adonis_session=${cookieClient.sign('adonis_session', sessionId)}` + const sessionStoreCookie = `${sessionId}=${cookieClient.encrypt(sessionId, { age: 22 })}` + + const responses = await Promise.all([ + supertest(server) + .get('/read-data') + .set('Cookie', `${sessionIdCookie}; ${sessionStoreCookie}`), + + supertest(server) + .get('/read-data-slowly') + .set('Cookie', `${sessionIdCookie}; ${sessionStoreCookie}`), + ]) + + /** + * Asserting store data when using cookie driver + */ + const cookies = setCookieParser.parse(responses[0].headers['set-cookie'], { map: true }) + const cookies1 = setCookieParser.parse(responses[1].headers['set-cookie'], { map: true }) + assert.deepEqual(cookieClient.decrypt(sessionId!, cookies[sessionId!].value), { + age: 22, + }) + assert.deepEqual(cookieClient.decrypt(sessionId!, cookies1[sessionId!].value), { + age: 22, + }) + }).timeout(6000) + + test('HAS RACE CONDITION: concurrently write and read slowly', async ({ assert }) => { + let sessionId = cuid() + + const server = httpServer.create((req, res) => + requestHandler(req, res, (ctx) => new CookieDriver(sessionConfig.cookie, ctx)) + ) + + const sessionIdCookie = `adonis_session=${cookieClient.sign('adonis_session', sessionId)}` + const sessionStoreCookie = `${sessionId}=${cookieClient.encrypt(sessionId, { age: 22 })}` + + const responses = await Promise.all([ + supertest(server) + .get('/read-data-slowly') + .set('Cookie', `${sessionIdCookie}; ${sessionStoreCookie}`), + + supertest(server) + .get('/write-data') + .set('Cookie', `${sessionIdCookie}; ${sessionStoreCookie}`), + ]) + + const cookies = setCookieParser.parse(responses[0].headers['set-cookie'], { map: true }) + const cookies1 = setCookieParser.parse(responses[1].headers['set-cookie'], { map: true }) + + /** + * Since this request finishes afterwards, it will overwrite the mutations + * from the /write-data endpoint. THIS IS A CONCURRENCY CONCERN + */ + assert.deepEqual(cookieClient.decrypt(sessionId!, cookies[sessionId!].value), { + age: 22, + }) + + assert.deepEqual(cookieClient.decrypt(sessionId!, cookies1[sessionId!].value), { + age: 22, + username: 'virk', + }) + }).timeout(6000) + + test('HAS RACE CONDITION: concurrently write and write slowly', async ({ assert }) => { + let sessionId = cuid() + + const server = httpServer.create((req, res) => + requestHandler(req, res, (ctx) => new CookieDriver(sessionConfig.cookie, ctx)) + ) + + const sessionIdCookie = `adonis_session=${cookieClient.sign('adonis_session', sessionId)}` + const sessionStoreCookie = `${sessionId}=${cookieClient.encrypt(sessionId, { age: 22 })}` + + const responses = await Promise.all([ + supertest(server) + .get('/write-data-slowly') + .set('Cookie', `${sessionIdCookie}; ${sessionStoreCookie}`), + + supertest(server) + .get('/write-data') + .set('Cookie', `${sessionIdCookie}; ${sessionStoreCookie}`), + ]) + + const cookies = setCookieParser.parse(responses[0].headers['set-cookie'], { map: true }) + const cookies1 = setCookieParser.parse(responses[1].headers['set-cookie'], { map: true }) + + /** + * Since this request finishes afterwards, it will overwrite the mutations + * from the /write-data endpoint. THIS IS A CONCURRENCY CONCERN + */ + assert.deepEqual(cookieClient.decrypt(sessionId!, cookies[sessionId!].value), { + age: 22, + email: 'foo@bar.com', + }) + + /** + * Same applies here. In short two concurrent write requests will mess up + * all the time + */ + assert.deepEqual(cookieClient.decrypt(sessionId!, cookies1[sessionId!].value), { + age: 22, + username: 'virk', + }) + }).timeout(6000) +}) + +test.group('Concurrency | file driver', () => { + test('concurrently read and read slowly', async ({ fs, assert }) => { + let sessionId = cuid() + + const fileDriver = new FileDriver({ location: fs.basePath }, sessionConfig.age) + await fileDriver.write(sessionId, { age: 22 }) + + const server = httpServer.create((req, res) => requestHandler(req, res, () => fileDriver)) + + const sessionIdCookie = `adonis_session=${cookieClient.sign('adonis_session', sessionId)}` + await Promise.all([ + supertest(server).get('/read-data').set('Cookie', `${sessionIdCookie};`), + supertest(server).get('/read-data-slowly').set('Cookie', `${sessionIdCookie};`), + ]) + + /** + * Asserting store data when using file driver + */ + await assert.fileEquals( + `${sessionId}.txt`, + JSON.stringify({ + message: { age: 22 }, + purpose: sessionId, + }) + ) + }).timeout(6000) + + test('concurrently write and read slowly', async ({ fs, assert }) => { + let sessionId = cuid() + + const fileDriver = new FileDriver({ location: fs.basePath }, sessionConfig.age) + await fileDriver.write(sessionId, { age: 22 }) + + const server = httpServer.create((req, res) => requestHandler(req, res, () => fileDriver)) + const sessionIdCookie = `adonis_session=${cookieClient.sign('adonis_session', sessionId)}` + + await Promise.all([ + supertest(server).get('/read-data-slowly').set('Cookie', `${sessionIdCookie}`), + supertest(server).get('/write-data').set('Cookie', `${sessionIdCookie}`), + ]) + + await assert.fileEquals( + `${sessionId}.txt`, + JSON.stringify({ + message: { age: 22, username: 'virk' }, + purpose: sessionId, + }) + ) + }).timeout(6000) + + test('HAS RACE CONDITON: concurrently write and write slowly', async ({ fs, assert }) => { + let sessionId = cuid() + + const fileDriver = new FileDriver({ location: fs.basePath }, sessionConfig.age) + await fileDriver.write(sessionId, { age: 22 }) + + const server = httpServer.create((req, res) => requestHandler(req, res, () => fileDriver)) + + const sessionIdCookie = `adonis_session=${cookieClient.sign('adonis_session', sessionId)}` + + await Promise.all([ + supertest(server).get('/write-data-slowly').set('Cookie', `${sessionIdCookie}`), + supertest(server).get('/write-data').set('Cookie', `${sessionIdCookie}`), + ]) + + await assert.fileEquals( + `${sessionId}.txt`, + JSON.stringify({ + message: { age: 22, email: 'foo@bar.com' }, + purpose: sessionId, + }) + ) + }).timeout(6000) +}) + +test.group('Concurrency | redis driver', () => { + test('concurrently read and read slowly', async ({ assert, cleanup }) => { + let sessionId = cuid() + cleanup(async () => { + await redisDriver.destroy(sessionId) + }) + + const redisDriver = new RedisDriver(redis, { connection: 'main' }, sessionConfig.age) + await redisDriver.write(sessionId, { age: 22 }) + + const server = httpServer.create((req, res) => requestHandler(req, res, () => redisDriver)) + + const sessionIdCookie = `adonis_session=${cookieClient.sign('adonis_session', sessionId)}` + await Promise.all([ + supertest(server).get('/read-data').set('Cookie', `${sessionIdCookie};`), + supertest(server).get('/read-data-slowly').set('Cookie', `${sessionIdCookie};`), + ]) + + /** + * Asserting store data when using file driver + */ + assert.deepEqual(await redisDriver.read(sessionId), { + age: 22, + }) + }).timeout(6000) + + test('concurrently write and read slowly', async ({ assert, cleanup }) => { + let sessionId = cuid() + cleanup(async () => { + await redisDriver.destroy(sessionId) + }) + + const redisDriver = new RedisDriver(redis, { connection: 'main' }, sessionConfig.age) + await redisDriver.write(sessionId, { age: 22 }) + + const server = httpServer.create((req, res) => requestHandler(req, res, () => redisDriver)) + const sessionIdCookie = `adonis_session=${cookieClient.sign('adonis_session', sessionId)}` + + await Promise.all([ + supertest(server).get('/read-data-slowly').set('Cookie', `${sessionIdCookie}`), + supertest(server).get('/write-data').set('Cookie', `${sessionIdCookie}`), + ]) + + assert.deepEqual(await redisDriver.read(sessionId), { age: 22, username: 'virk' }) + }).timeout(6000) + + test('HAS RACE CONDITON: concurrently write and write slowly', async ({ assert, cleanup }) => { + let sessionId = cuid() + cleanup(async () => { + await redisDriver.destroy(sessionId) + }) + + const redisDriver = new RedisDriver(redis, { connection: 'main' }, sessionConfig.age) + await redisDriver.write(sessionId, { age: 22 }) + + const server = httpServer.create((req, res) => requestHandler(req, res, () => redisDriver)) + + const sessionIdCookie = `adonis_session=${cookieClient.sign('adonis_session', sessionId)}` + + await Promise.all([ + supertest(server).get('/write-data-slowly').set('Cookie', `${sessionIdCookie}`), + supertest(server).get('/write-data').set('Cookie', `${sessionIdCookie}`), + ]) + + assert.deepEqual(await redisDriver.read(sessionId), { age: 22, email: 'foo@bar.com' }) + }).timeout(6000) +}) diff --git a/tests/cookie_driver.spec.ts b/tests/cookie_driver.spec.ts deleted file mode 100644 index ecffc26..0000000 --- a/tests/cookie_driver.spec.ts +++ /dev/null @@ -1,159 +0,0 @@ -/* - * @adonisjs/session - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import supertest from 'supertest' -import { test } from '@japa/runner' -import setCookieParser from 'set-cookie-parser' -import { CookieClient } from '@adonisjs/core/http' -import type { CookieOptions } from '@adonisjs/core/types/http' -import { EncryptionFactory } from '@adonisjs/core/factories/encryption' -import { HttpContextFactory, RequestFactory, ResponseFactory } from '@adonisjs/core/factories/http' - -import { httpServer } from '../test_helpers/index.js' -import { CookieDriver } from '../src/drivers/cookie.js' - -const encryption = new EncryptionFactory().create() -const cookieClient = new CookieClient(encryption) -const cookieConfig: Partial = { - sameSite: 'strict', - maxAge: '5mins', -} - -test.group('Cookie driver', () => { - test('return null when session data cookie does not exists', async ({ assert }) => { - const sessionId = '1234' - - const server = httpServer.create(async (req, res) => { - const request = new RequestFactory().merge({ req, res, encryption }).create() - const response = new ResponseFactory().merge({ req, res, encryption }).create() - const ctx = new HttpContextFactory().merge({ request, response }).create() - - const session = new CookieDriver(cookieConfig, ctx) - const value = session.read(sessionId) - response.json(value) - response.finish() - }) - - const { body, text } = await supertest(server).get('/') - assert.deepEqual(body, {}) - assert.equal(text, '') - }) - - test('return session data from the cookie', async ({ assert }) => { - const sessionId = '1234' - - const server = httpServer.create(async (req, res) => { - const request = new RequestFactory().merge({ req, res, encryption }).create() - const response = new ResponseFactory().merge({ req, res, encryption }).create() - const ctx = new HttpContextFactory().merge({ request, response }).create() - - const session = new CookieDriver(cookieConfig, ctx) - const value = session.read(sessionId) - response.json(value) - response.finish() - }) - - const { body } = await supertest(server) - .get('/') - .set('cookie', `${sessionId}=${cookieClient.encrypt(sessionId, { visits: 1 })}`) - - assert.deepEqual(body, { visits: 1 }) - }) - - test('persist session data inside a cookie', async ({ assert }) => { - const sessionId = '1234' - - const server = httpServer.create(async (req, res) => { - const request = new RequestFactory().merge({ req, res, encryption }).create() - const response = new ResponseFactory().merge({ req, res, encryption }).create() - const ctx = new HttpContextFactory().merge({ request, response }).create() - - const session = new CookieDriver(cookieConfig, ctx) - session.write(sessionId, { visits: 0 }) - response.finish() - }) - - const { headers } = await supertest(server).get('/') - const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) - assert.deepEqual(cookieClient.decrypt(sessionId, cookies[sessionId].value), { - visits: 0, - }) - }) - - test('touch cookie by re-updating its attributes', async ({ assert }) => { - const sessionId = '1234' - - const server = httpServer.create(async (req, res) => { - const request = new RequestFactory().merge({ req, res, encryption }).create() - const response = new ResponseFactory().merge({ req, res, encryption }).create() - const ctx = new HttpContextFactory().merge({ request, response }).create() - - const session = new CookieDriver(cookieConfig, ctx) - session.touch(sessionId) - response.finish() - }) - - const { headers } = await supertest(server) - .get('/') - .set('Cookie', `${sessionId}=${cookieClient.encrypt(sessionId, { visits: 1 })}`) - - const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) - assert.deepEqual(cookieClient.decrypt(sessionId, cookies[sessionId].value), { - visits: 1, - }) - }) - - test('do not write cookie to response unless touch or write methods are called', async ({ - assert, - }) => { - const sessionId = '1234' - - const server = httpServer.create(async (req, res) => { - const request = new RequestFactory().merge({ req, res, encryption }).create() - const response = new ResponseFactory().merge({ req, res, encryption }).create() - const ctx = new HttpContextFactory().merge({ request, response }).create() - - const session = new CookieDriver(cookieConfig, ctx) - response.json(session.read(sessionId)) - response.finish() - }) - - const { headers, body } = await supertest(server) - .get('/') - .set('Cookie', `${sessionId}=${cookieClient.encrypt(sessionId, { visits: 1 })}`) - - const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) - assert.deepEqual(cookies, {}) - assert.deepEqual(body, { visits: 1 }) - }) - - test('delete session data cookie', async ({ assert }) => { - const sessionId = '1234' - - const server = httpServer.create(async (req, res) => { - const request = new RequestFactory().merge({ req, res, encryption }).create() - const response = new ResponseFactory().merge({ req, res, encryption }).create() - const ctx = new HttpContextFactory().merge({ request, response }).create() - - const session = new CookieDriver(cookieConfig, ctx) - response.json(session.read(sessionId)) - session.destroy(sessionId) - response.finish() - }) - - const { headers, body } = await supertest(server) - .get('/') - .set('Cookie', `${sessionId}=${cookieClient.encrypt(sessionId, { visits: 1 })}`) - - const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) - assert.equal(cookies[sessionId].maxAge, -1) - assert.equal(cookies[sessionId].expires, new Date('1970-01-01').toString()) - assert.deepEqual(body, { visits: 1 }) - }) -}) diff --git a/tests/file_driver.spec.ts b/tests/file_driver.spec.ts deleted file mode 100644 index 109018b..0000000 --- a/tests/file_driver.spec.ts +++ /dev/null @@ -1,126 +0,0 @@ -/* - * @adonisjs/session - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { join } from 'node:path' -import { test } from '@japa/runner' -import { setTimeout } from 'node:timers/promises' - -import { FileDriver } from '../src/drivers/file.js' - -test.group('File driver', () => { - test('do not create file for a new session', async ({ fs, assert }) => { - const sessionId = '1234' - const session = new FileDriver({ location: fs.basePath }, '2 hours') - - const value = await session.read(sessionId) - assert.isNull(value) - - await assert.fileNotExists('1234.txt') - }) - - test('create intermediate directories when missing', async ({ fs, assert }) => { - const sessionId = '1234' - const session = new FileDriver( - { - location: join(fs.basePath, 'app/sessions'), - }, - '2 hours' - ) - - await session.write(sessionId, { message: 'hello-world' }) - - await assert.fileExists('app/sessions/1234.txt') - await assert.fileEquals( - 'app/sessions/1234.txt', - JSON.stringify({ message: { message: 'hello-world' }, purpose: '1234' }) - ) - }) - - test('get session existing value', async ({ assert, fs }) => { - const sessionId = '1234' - const session = new FileDriver({ location: fs.basePath }, '2 hours') - await session.write(sessionId, { message: 'hello-world' }) - - const value = await session.read(sessionId) - assert.deepEqual(value, { message: 'hello-world' }) - }) - - test('return null when session data is expired', async ({ assert, fs }) => { - const sessionId = '1234' - const session = new FileDriver({ location: fs.basePath }, 1000) - await session.write(sessionId, { message: 'hello-world' }) - - await setTimeout(2000) - - const value = await session.read(sessionId) - assert.isNull(value) - }).disableTimeout() - - test('ignore malformed file contents', async ({ fs, assert }) => { - const sessionId = '1234' - const session = new FileDriver({ location: fs.basePath }, '2 hours') - await fs.create('1234.txt', 'foo') - - const value = await session.read(sessionId) - assert.isNull(value) - }) - - test('remove file on destroy', async ({ assert, fs }) => { - const sessionId = '1234' - - const session = new FileDriver({ location: fs.basePath }, '2 hours') - await session.write(sessionId, { message: 'hello-world' }) - await session.destroy(sessionId) - - await assert.fileNotExists('1234.txt') - }) - - test('do not fail when destroying a non-existing session', async ({ assert, fs }) => { - const sessionId = '1234' - - await assert.fileNotExists('1234.txt') - - const session = new FileDriver({ location: fs.basePath }, '2 hours') - await session.destroy(sessionId) - - await assert.fileNotExists('1234.txt') - }) - - test('update session expiry on touch', async ({ assert, fs }) => { - const sessionId = '1234' - - const session = new FileDriver({ location: fs.basePath }, '2 hours') - await session.write(sessionId, { message: 'hello-world' }) - - /** - * Waiting a bit - */ - await setTimeout(2000) - - /** - * Making sure the original mTime of the file was smaller - * than the current time after wait - */ - const { mtimeMs } = await fs.adapter.stat(join(fs.basePath, '1234.txt')) - assert.isBelow(mtimeMs, Date.now()) - - await session.touch(sessionId) - - /** - * Ensuring the new mTime is greater than the old mTime - */ - let { mtimeMs: newMtimeMs } = await fs.adapter.stat(join(fs.basePath, '1234.txt')) - assert.isAbove(newMtimeMs, mtimeMs) - - await assert.fileEquals( - '1234.txt', - JSON.stringify({ message: { message: 'hello-world' }, purpose: '1234' }) - ) - }).disableTimeout() -}) diff --git a/tests/redis_driver.spec.ts b/tests/redis_driver.spec.ts deleted file mode 100644 index 33c8e01..0000000 --- a/tests/redis_driver.spec.ts +++ /dev/null @@ -1,128 +0,0 @@ -/* - * @adonisjs/session - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' -import { RedisDriver } from '../src/drivers/redis.js' -import { setup, sleep, sessionConfig, getRedisManager } from '../test_helpers/index.js' - -const config = Object.assign({}, sessionConfig, { driver: 'redis', redisConnection: 'session' }) - -test.group('Redis driver', () => { - test('return null when value is missing', async ({ fs, assert }) => { - const { app } = await setup(fs) - - const sessionId = '1234' - const redis = getRedisManager(app) - const session = new RedisDriver(config, redis) - - const value = await session.read(sessionId) - await redis.disconnectAll() - assert.isNull(value) - }) - - test('write session value to the redis store', async ({ fs, assert }) => { - const { app } = await setup(fs) - - const sessionId = '1234' - const redis = getRedisManager(app) - - const session = new RedisDriver(config, redis) - await session.write(sessionId, { message: 'hello-world' }) - - const contents = await redis.connection('session').get('1234') - await redis.connection('session').del('1234') - await redis.disconnectAll() - - assert.deepEqual(JSON.parse(contents!), { - message: { message: 'hello-world' }, - purpose: '1234', - }) - }) - - test('get session existing value', async ({ fs, assert }) => { - const { app } = await setup(fs) - - const sessionId = '1234' - const redis = getRedisManager(app) - - await redis.connection('session').set( - '1234', - JSON.stringify({ - message: { message: 'hello-world' }, - purpose: '1234', - }) - ) - - const session = new RedisDriver(config, redis) - const contents = await session.read(sessionId) - await redis.connection('session').del('1234') - await redis.disconnectAll() - - assert.deepEqual(contents, { message: 'hello-world' }) - }) - - test('remove session', async ({ assert, fs }) => { - const { app } = await setup(fs) - - const sessionId = '1234' - const redis = getRedisManager(app) - - await redis.connection('session').set( - '1234', - JSON.stringify({ - message: { message: 'hello-world' }, - purpose: '1234', - }) - ) - - const session = new RedisDriver(config, redis) - let contents = await session.read(sessionId) - assert.deepEqual(contents, { message: 'hello-world' }) - - await session.destroy('1234') - contents = await session.read(sessionId) - - await redis.disconnectAll() - assert.isNull(contents) - }) - - test('update session expiry', async ({ fs, assert }) => { - const { app } = await setup(fs) - - const sessionId = '1234' - const redis = getRedisManager(app) - - const session = new RedisDriver(config, redis) - await redis.connection('session').set( - '1234', - JSON.stringify({ - message: { message: 'hello-world' }, - purpose: '1234', - }) - ) - - await sleep(1000) - - let expiry = await redis.connection('session').ttl('1234') - assert.isBelow(expiry, 3) - - /** - * Update expiry - */ - await session.touch(sessionId) - expiry = await redis.connection('session').ttl('1234') - assert.equal(expiry, 3) - - const contents = await session.read(sessionId) - await session.destroy('1234') - await redis.disconnectAll() - - assert.deepEqual(contents, { message: 'hello-world' }) - }).timeout(0) -}) diff --git a/tests/session.spec.ts b/tests/session.spec.ts index 621173d..b7d01b3 100644 --- a/tests/session.spec.ts +++ b/tests/session.spec.ts @@ -7,845 +7,887 @@ * file that was distributed with this source code. */ -import { test } from '@japa/runner' -import type { RedisService } from '@adonisjs/redis/types' import supertest from 'supertest' -import { createServer } from 'node:http' +import { test } from '@japa/runner' +import { cuid } from '@adonisjs/core/helpers' +import setCookieParser from 'set-cookie-parser' +import { Emitter } from '@adonisjs/core/events' +import { EventsList } from '@adonisjs/core/types' +import { CookieClient } from '@adonisjs/core/http' +import { AppFactory } from '@adonisjs/core/factories/app' +import { EncryptionFactory } from '@adonisjs/core/factories/encryption' +import { HttpContextFactory, RequestFactory, ResponseFactory } from '@adonisjs/core/factories/http' -import { Store } from '../src/store.js' import { Session } from '../src/session.js' -import { MemoryDriver } from '../src/drivers/memory.js' -import { - setup, - sessionConfig, - unsignCookie, - signCookie, - createHttpContext, -} from '../test_helpers/index.js' - -test.group('Session', (group) => { - group.each.teardown(async () => { - MemoryDriver.sessions.clear() +import type { SessionConfig } from '../src/types/main.js' +import { httpServer } from '../test_helpers/index.js' +import { CookieDriver } from '../src/drivers/cookie.js' + +const app = new AppFactory().create(new URL('./', import.meta.url), () => {}) +const emitter = new Emitter(app) +const encryption = new EncryptionFactory().create() +const cookieClient = new CookieClient(encryption) +const sessionConfig: SessionConfig = { + enabled: true, + age: '2 hours', + clearWithBrowser: false, + cookieName: 'adonis_session', + driver: 'cookie', + cookie: {}, +} + +test.group('Session', () => { + test('do not define session id cookie when not initiated', async ({ assert }) => { + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() + + const driver = new CookieDriver(sessionConfig.cookie, ctx) + const session = new Session(sessionConfig, driver, emitter, ctx) + + assert.isFalse(session.initiated) + + await session.commit() + response.finish() + }) + + const { headers } = await supertest(server).get('/') + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) + assert.deepEqual(cookies, {}) }) test("initiate session with fresh session id when there isn't any session", async ({ assert, - fs, }) => { - const { app } = await setup(fs) + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) - - const driver = new MemoryDriver() - const session = new Session(ctx, sessionConfig, driver) + const driver = new CookieDriver(sessionConfig.cookie, ctx) + const session = new Session(sessionConfig, driver, emitter, ctx) await session.initiate(false) assert.isTrue(session.fresh) assert.isTrue(session.initiated) - res.end() + + await session.commit() + response.finish() }) - await supertest(server).get('/') + const { headers } = await supertest(server).get('/') + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) + assert.property(cookies, 'adonis_session') }) - test('initiate session with empty store when session id exists', async ({ fs, assert }) => { - const { app } = await setup(fs) + test('do not commit to store when session store is empty', async ({ assert }) => { + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) - - const driver = new MemoryDriver() - const session = new Session(ctx, sessionConfig, driver) + const driver = new CookieDriver(sessionConfig.cookie, ctx) + const session = new Session(sessionConfig, driver, emitter, ctx) await session.initiate(false) - assert.isFalse(session.fresh) - assert.equal(session.sessionId, '1234') + assert.isTrue(session.fresh) assert.isTrue(session.initiated) - res.end() + + await session.commit() + response.finish() }) - await supertest(server) - .get('/') - .set('cookie', await signCookie(app, '1234', sessionConfig.cookieName)) + const { headers } = await supertest(server).get('/') + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) + assert.property(cookies, 'adonis_session') + assert.lengthOf(Object.keys(cookies), 1) }) - test('write session values with driver on commit', async ({ assert, fs }) => { - const { app } = await setup(fs) + test('commit to store when session has data', async ({ assert }) => { + let sessionId: string | undefined - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new MemoryDriver() - const session = new Session(ctx, sessionConfig, driver) + const driver = new CookieDriver(sessionConfig.cookie, ctx) + const session = new Session(sessionConfig, driver, emitter, ctx) await session.initiate(false) - session.put('user', { username: 'virk' }) + session.put('username', 'virk') + sessionId = session.sessionId + await session.commit() - ctx.response.send('') - ctx.response.finish() + response.finish() }) - const { header } = await supertest(server).get('/') + const { headers } = await supertest(server).get('/') + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) - const sessionId = await unsignCookie(app, header, sessionConfig.cookieName) - assert.exists(sessionId) - const session = MemoryDriver.sessions.get(sessionId)! - assert.deepEqual(new Store(session).all(), { user: { username: 'virk' } }) + assert.property(cookies, 'adonis_session') + assert.property(cookies, sessionId!) + assert.equal(cookies[sessionId!].maxAge, 90) + assert.deepEqual(cookieClient.decrypt(sessionId!, cookies[sessionId!].value), { + username: 'virk', + }) }) - test('re-use existing session id', async ({ assert, fs }) => { - const { app } = await setup(fs) + test('append to existing store', async ({ assert }) => { + let sessionId = cuid() - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new MemoryDriver() - const session = new Session(ctx, sessionConfig, driver) + const driver = new CookieDriver(sessionConfig.cookie, ctx) + const session = new Session(sessionConfig, driver, emitter, ctx) await session.initiate(false) - session.put('user', { username: 'virk' }) + session.put('username', 'virk') + assert.isTrue(session.has('username')) + assert.isTrue(session.has('age')) + assert.deepEqual(session.all(), { username: 'virk', age: 22 }) + await session.commit() - ctx.response.send('') - ctx.response.finish() + response.finish() }) - const { header } = await supertest(server) + const sessionIdCookie = `adonis_session=${cookieClient.sign('adonis_session', sessionId)}` + const sessionStoreCookie = `${sessionId}=${cookieClient.encrypt(sessionId, { age: 22 })}` + + const { headers } = await supertest(server) .get('/') - .set('cookie', await signCookie(app, '1234', sessionConfig.cookieName)) + .set('cookie', `${sessionIdCookie}; ${sessionStoreCookie}`) - const sessionId = await unsignCookie(app, header, sessionConfig.cookieName) - assert.equal(sessionId, '1234') + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) - const session = MemoryDriver.sessions.get('1234')! - assert.deepEqual(new Store(session).all(), { user: { username: 'virk' } }) + assert.property(cookies, 'adonis_session') + assert.property(cookies, sessionId!) + assert.equal(cookies[sessionId!].maxAge, 90) + assert.deepEqual(cookieClient.decrypt(sessionId!, cookies[sessionId!].value), { + username: 'virk', + age: 22, + }) }) - test('retain driver existing values', async ({ fs, assert }) => { - const { app } = await setup(fs) + test('delete store when session store is empty', async ({ assert }) => { + let sessionId = cuid() - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new MemoryDriver() - const session = new Session(ctx, sessionConfig, driver) + const driver = new CookieDriver(sessionConfig.cookie, ctx) + const session = new Session(sessionConfig, driver, emitter, ctx) await session.initiate(false) - session.put('user.username', 'virk') + session.forget('age') await session.commit() - ctx.response.send('') - ctx.response.finish() + response.finish() }) - /** - * Initial driver value - */ - const store = new Store(null) - store.set('user.age', 22) - MemoryDriver.sessions.set('1234', store.toJSON()) + const sessionIdCookie = `adonis_session=${cookieClient.sign('adonis_session', sessionId)}` + const sessionStoreCookie = `${sessionId}=${cookieClient.encrypt(sessionId, { age: 22 })}` - /** - * Request - */ - const { header } = await supertest(server) + const { headers } = await supertest(server) .get('/') - .set('cookie', await signCookie(app, '1234', sessionConfig.cookieName)) + .set('cookie', `${sessionIdCookie}; ${sessionStoreCookie}`) - const sessionId = await unsignCookie(app, header, sessionConfig.cookieName) - assert.equal(sessionId, '1234') + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) - /** - * Ensure driver has existing + new values - */ - const session = MemoryDriver.sessions.get('1234')! - assert.deepEqual(new Store(session).all(), { user: { username: 'virk', age: 22 } }) + assert.property(cookies, 'adonis_session') + assert.equal(cookies[sessionId].maxAge, -1) + assert.lengthOf(Object.keys(cookies), 2) }) - test('regenerate session id when regenerate method is called', async ({ fs, assert }) => { - const { app } = await setup(fs) + test('pull value from the session store', async ({ assert }) => { + let sessionId = cuid() - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new MemoryDriver() - const session = new Session(ctx, sessionConfig, driver) + const driver = new CookieDriver(sessionConfig.cookie, ctx) + const session = new Session(sessionConfig, driver, emitter, ctx) await session.initiate(false) - session.regenerate() - - session.put('user.username', 'virk') + assert.equal(session.pull('age'), 22) await session.commit() - ctx.response.send('') - ctx.response.finish() + response.finish() }) - /** - * Initial driver value - */ - const store = new Store(null) - store.set('user.age', 22) - MemoryDriver.sessions.set('1234', store.toJSON()) + const sessionIdCookie = `adonis_session=${cookieClient.sign('adonis_session', sessionId)}` + const sessionStoreCookie = `${sessionId}=${cookieClient.encrypt(sessionId, { age: 22 })}` - const { header } = await supertest(server) + const { headers } = await supertest(server) .get('/') - .set('cookie', await signCookie(app, '1234', sessionConfig.cookieName)) - - const sessionId = await unsignCookie(app, header, sessionConfig.cookieName) - assert.exists(sessionId) - assert.notEqual(sessionId, '1234') + .set('cookie', `${sessionIdCookie}; ${sessionStoreCookie}`) - const session = MemoryDriver.sessions.get(sessionId)! - assert.equal(MemoryDriver.sessions.size, 1) - assert.deepEqual(new Store(session).all(), { user: { username: 'virk', age: 22 } }) + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) - /** - * Ensure old values have been cleared - */ - assert.isUndefined(MemoryDriver.sessions.get('1234')) + assert.property(cookies, 'adonis_session') + assert.equal(cookies[sessionId].maxAge, -1) + assert.lengthOf(Object.keys(cookies), 2) }) - test('get session value', async ({ assert, fs }) => { - const { app } = await setup(fs) + test('initiate value with 1 on increment', async ({ assert }) => { + let sessionId = cuid() - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new MemoryDriver() - const session = new Session(ctx, sessionConfig, driver) + const driver = new CookieDriver(sessionConfig.cookie, ctx) + const session = new Session(sessionConfig, driver, emitter, ctx) await session.initiate(false) - ctx.response.send(session.get('user.age')) + + session.increment('visits') await session.commit() - ctx.response.finish() + response.finish() }) - /** - * Initial driver value - */ - const store = new Store(null) - store.set('user.age', 22) - MemoryDriver.sessions.set('1234', store.toJSON()) + const sessionIdCookie = `adonis_session=${cookieClient.sign('adonis_session', sessionId)}` - /** - * Request - */ - const { text } = await supertest(server) - .get('/') - .set('cookie', await signCookie(app, '1234', sessionConfig.cookieName)) + const { headers } = await supertest(server).get('/').set('cookie', `${sessionIdCookie}`) - assert.equal(text, '22') + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) + assert.property(cookies, 'adonis_session') + assert.deepEqual(cookieClient.decrypt(sessionId!, cookies[sessionId!].value), { + visits: 1, + }) }) - test('get nested value using form input syntax', async ({ assert, fs }) => { - const { app } = await setup(fs) + test('initiate value with -1 on decrement', async ({ assert }) => { + let sessionId = cuid() - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new MemoryDriver() - const session = new Session(ctx, sessionConfig, driver) + const driver = new CookieDriver(sessionConfig.cookie, ctx) + const session = new Session(sessionConfig, driver, emitter, ctx) await session.initiate(false) - ctx.response.send(session.get('user[age]')) + + session.decrement('visits') await session.commit() - ctx.response.finish() + response.finish() }) - /** - * Initial driver value - */ - const store = new Store(null) - store.set('user.age', 22) - MemoryDriver.sessions.set('1234', store.toJSON()) + const sessionIdCookie = `adonis_session=${cookieClient.sign('adonis_session', sessionId)}` - /** - * Request - */ - const { text } = await supertest(server) - .get('/') - .set('cookie', await signCookie(app, '1234', sessionConfig.cookieName)) + const { headers } = await supertest(server).get('/').set('cookie', `${sessionIdCookie}`) - assert.equal(text, '22') - }) -}) - -test.group('Session | Flash', (group) => { - group.each.teardown(async () => { - MemoryDriver.sessions.clear() + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) + assert.property(cookies, 'adonis_session') + assert.deepEqual(cookieClient.decrypt(sessionId!, cookies[sessionId!].value), { + visits: -1, + }) }) - test('set custom flash messages', async ({ assert, fs }) => { - const { app } = await setup(fs) + test('touch session store when not modified', async ({ assert }) => { + let sessionId = cuid() - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new MemoryDriver() - const session = new Session(ctx, sessionConfig, driver) + const driver = new CookieDriver(sessionConfig.cookie, ctx) + const session = new Session(sessionConfig, driver, emitter, ctx) await session.initiate(false) - session.flash('success', 'User created succesfully') await session.commit() - ctx.response.send('') - ctx.response.finish() + response.finish() }) - const { header } = await supertest(server).get('/') + const sessionIdCookie = `adonis_session=${cookieClient.sign('adonis_session', sessionId)}` + const sessionStoreCookie = `${sessionId}=${cookieClient.encrypt(sessionId, { age: 22 })}` - /** - * Ensure session id is changed - */ - const sessionId = await unsignCookie(app, header, sessionConfig.cookieName) - assert.exists(sessionId) - const session = MemoryDriver.sessions.get(sessionId)! + const { headers } = await supertest(server) + .get('/') + .set('cookie', `${sessionIdCookie}; ${sessionStoreCookie}`) - assert.deepEqual(new Store(session).all(), { - __flash__: { - success: 'User created succesfully', - }, + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) + + assert.property(cookies, 'adonis_session') + assert.property(cookies, sessionId!) + assert.equal(cookies[sessionId!].maxAge, 90) + assert.deepEqual(cookieClient.decrypt(sessionId!, cookies[sessionId!].value), { + age: 22, }) }) - test('flash input values', async ({ assert, fs }) => { - const { app } = await setup(fs) + test('clear session store', async ({ assert }) => { + let sessionId = cuid() - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) - ctx.request.setInitialBody({ username: 'virk', age: 28 }) + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new MemoryDriver() - const session = new Session(ctx, sessionConfig, driver) + const driver = new CookieDriver(sessionConfig.cookie, ctx) + const session = new Session(sessionConfig, driver, emitter, ctx) await session.initiate(false) - session.flashAll() + session.clear() await session.commit() - ctx.response.send('') - ctx.response.finish() + response.finish() }) - const { header } = await supertest(server).get('/') + const sessionIdCookie = `adonis_session=${cookieClient.sign('adonis_session', sessionId)}` + const sessionStoreCookie = `${sessionId}=${cookieClient.encrypt(sessionId, { age: 22 })}` - const sessionId = await unsignCookie(app, header, sessionConfig.cookieName) - assert.exists(sessionId) - const session = MemoryDriver.sessions.get(sessionId)! + const { headers } = await supertest(server) + .get('/') + .set('cookie', `${sessionIdCookie}; ${sessionStoreCookie}`) - assert.deepEqual(new Store(session).all(), { - __flash__: { - username: 'virk', - age: 28, - }, - }) - }) + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) - test('flash selected input values', async ({ assert, fs }) => { - const { app } = await setup(fs) + assert.property(cookies, 'adonis_session') + assert.equal(cookies[sessionId].maxAge, -1) + assert.lengthOf(Object.keys(cookies), 2) + }) - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) + test('throw error when trying to read from uninitiated store', async () => { + const ctx = new HttpContextFactory().create() + const driver = new CookieDriver(sessionConfig.cookie, ctx) + const session = new Session(sessionConfig, driver, emitter, ctx) + session.get('username') + }).throws( + 'Session store has not been initiated. Make sure you have registered the session middleware' + ) + + test('throw error when trying to write to a read only store', async ({ assert }) => { + const ctx = new HttpContextFactory().create() + const driver = new CookieDriver(sessionConfig.cookie, ctx) + const session = new Session(sessionConfig, driver, emitter, ctx) + + await session.initiate(true) + assert.isUndefined(session.get('username')) + + session.put('username', 'foo') + }).throws('Session store is in readonly mode and cannot be mutated') +}) - ctx.request.setInitialBody({ - username: 'virk', - age: 28, - profile: { - twitterHandle: '@AmanVirk1', - }, - }) +test.group('Session | Regenerate', () => { + test("initiate session with fresh session id when there isn't any session", async ({ + assert, + }) => { + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new MemoryDriver() - const session = new Session(ctx, sessionConfig, driver) + const driver = new CookieDriver(sessionConfig.cookie, ctx) + const session = new Session(sessionConfig, driver, emitter, ctx) await session.initiate(false) - session.flashOnly(['username', 'profile.twitterHandle']) - await session.commit() - ctx.response.send('') - ctx.response.finish() - }) - - const { header } = await supertest(server).get('/') + session.regenerate() - const sessionId = await unsignCookie(app, header, sessionConfig.cookieName) - assert.exists(sessionId) + assert.isTrue(session.fresh) + assert.isTrue(session.initiated) + assert.isFalse(session.hasRegeneratedSession) - const session = MemoryDriver.sessions.get(sessionId)! - assert.deepEqual(new Store(session).all(), { - __flash__: { - username: 'virk', - profile: { - twitterHandle: '@AmanVirk1', - }, - }, + await session.commit() + response.finish() }) + + const { headers } = await supertest(server).get('/') + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) + assert.property(cookies, 'adonis_session') }) - test("flash all input values except the defined one's", async ({ assert, fs }) => { - const { app } = await setup(fs) + test('do not commit to store when session store is empty', async ({ assert }) => { + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) + const driver = new CookieDriver(sessionConfig.cookie, ctx) + const session = new Session(sessionConfig, driver, emitter, ctx) + await session.initiate(false) - ctx.request.setInitialBody({ - username: 'virk', - age: 28, - profile: { - twitterHandle: '@AmanVirk1', - }, - }) + session.regenerate() - const driver = new MemoryDriver() - const session = new Session(ctx, sessionConfig, driver) - await session.initiate(false) + assert.isTrue(session.fresh) + assert.isTrue(session.initiated) + assert.isFalse(session.hasRegeneratedSession) - session.flashExcept(['age']) await session.commit() - ctx.response.send('') - ctx.response.finish() + response.finish() }) - const { header } = await supertest(server).get('/') - - const sessionId = await unsignCookie(app, header, sessionConfig.cookieName) - assert.exists(sessionId) - const session = MemoryDriver.sessions.get(sessionId)! - - assert.deepEqual(new Store(session).all(), { - __flash__: { - username: 'virk', - profile: { - twitterHandle: '@AmanVirk1', - }, - }, - }) + const { headers } = await supertest(server).get('/') + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) + assert.property(cookies, 'adonis_session') + assert.lengthOf(Object.keys(cookies), 1) }) - test('flash input along with custom messages', async ({ assert, fs }) => { - const { app } = await setup(fs) - - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) + test('commit to store when session has data', async ({ assert }) => { + let sessionId: string | undefined - ctx.request.setInitialBody({ - username: 'virk', - age: 28, - }) + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new MemoryDriver() - const session = new Session(ctx, sessionConfig, driver) + const driver = new CookieDriver(sessionConfig.cookie, ctx) + const session = new Session(sessionConfig, driver, emitter, ctx) await session.initiate(false) - session.flashExcept(['age']) - session.flash('success', 'User created') + session.regenerate() + assert.isFalse(session.hasRegeneratedSession) + + session.put('username', 'virk') + sessionId = session.sessionId + await session.commit() - ctx.response.send('') - ctx.response.finish() + response.finish() }) - const { header } = await supertest(server).get('/') + const { headers } = await supertest(server).get('/') + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) - const sessionId = await unsignCookie(app, header, sessionConfig.cookieName) - assert.exists(sessionId) - const session = MemoryDriver.sessions.get(sessionId)! - - assert.deepEqual(new Store(session).all(), { - __flash__: { - username: 'virk', - success: 'User created', - }, + assert.property(cookies, 'adonis_session') + assert.property(cookies, sessionId!) + assert.equal(cookies[sessionId!].maxAge, 90) + assert.deepEqual(cookieClient.decrypt(sessionId!, cookies[sessionId!].value), { + username: 'virk', }) }) - test('read old flash values', async ({ assert, fs }) => { - const { app } = await setup(fs) + test('append to existing store', async ({ assert }) => { + let sessionId = cuid() + let newSessionId: string | undefined - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new MemoryDriver() - const session = new Session(ctx, sessionConfig, driver) + const driver = new CookieDriver(sessionConfig.cookie, ctx) + const session = new Session(sessionConfig, driver, emitter, ctx) await session.initiate(false) - assert.deepEqual(session.flashMessages.all(), { - username: 'virk', - success: 'User created', - }) + session.put('username', 'virk') + session.regenerate() + newSessionId = session.sessionId - await session.commit() - ctx.response.send('') - ctx.response.finish() - }) + assert.isTrue(session.hasRegeneratedSession) - /** - * Initial driver value - */ - const store = new Store(null) - store.set('__flash__', { - username: 'virk', - success: 'User created', + await session.commit() + response.finish() }) - MemoryDriver.sessions.set('1234', store.toJSON()) + const sessionIdCookie = `adonis_session=${cookieClient.sign('adonis_session', sessionId)}` + const sessionStoreCookie = `${sessionId}=${cookieClient.encrypt(sessionId, { age: 22 })}` - const { header } = await supertest(server) + const { headers } = await supertest(server) .get('/') - .set('cookie', await signCookie(app, '1234', sessionConfig.cookieName)) + .set('cookie', `${sessionIdCookie}; ${sessionStoreCookie}`) + + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) - const sessionId = await unsignCookie(app, header, sessionConfig.cookieName) - assert.exists(sessionId) - const session = MemoryDriver.sessions.get(sessionId)! - assert.deepEqual(new Store(session).all(), {}) + assert.property(cookies, 'adonis_session') + assert.notEqual(newSessionId, sessionId) + assert.property(cookies, newSessionId!) + assert.equal(cookies[sessionId!].maxAge, -1) + assert.equal(cookies[newSessionId!].maxAge, 90) + assert.deepEqual(cookieClient.decrypt(newSessionId!, cookies[newSessionId!].value), { + username: 'virk', + age: 22, + }) }) - test('read selected old values', async ({ assert, fs }) => { - const { app } = await setup(fs) + test('delete store when session store is empty', async ({ assert }) => { + let sessionId = cuid() + let newSessionId: string | undefined - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new MemoryDriver() - const session = new Session(ctx, sessionConfig, driver) + const driver = new CookieDriver(sessionConfig.cookie, ctx) + const session = new Session(sessionConfig, driver, emitter, ctx) await session.initiate(false) - assert.deepEqual(session.flashMessages.get('username'), 'virk') + session.forget('age') + session.regenerate() + newSessionId = session.sessionId - await session.commit() - ctx.response.send('') - ctx.response.finish() - }) + assert.isTrue(session.hasRegeneratedSession) - /** - * Initial driver value - */ - const store = new Store(null) - store.set('__flash__', { - username: 'virk', - success: 'User created', + await session.commit() + response.finish() }) - MemoryDriver.sessions.set('1234', store.toJSON()) + const sessionIdCookie = `adonis_session=${cookieClient.sign('adonis_session', sessionId)}` + const sessionStoreCookie = `${sessionId}=${cookieClient.encrypt(sessionId, { age: 22 })}` - const { header } = await supertest(server) + const { headers } = await supertest(server) .get('/') - .set('cookie', await signCookie(app, '1234', sessionConfig.cookieName)) + .set('cookie', `${sessionIdCookie}; ${sessionStoreCookie}`) + + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) - const sessionId = await unsignCookie(app, header, sessionConfig.cookieName) - assert.exists(sessionId) - const session = MemoryDriver.sessions.get(sessionId)! - assert.deepEqual(new Store(session).all(), {}) + assert.notEqual(newSessionId, sessionId) + assert.property(cookies, 'adonis_session') + assert.notProperty(cookies, newSessionId!) + assert.equal(cookies[sessionId].maxAge, -1) + assert.lengthOf(Object.keys(cookies), 2) }) - test('flash custom messages as an object', async ({ assert, fs }) => { - const { app } = await setup(fs) + test('touch session store when not modified', async ({ assert }) => { + let sessionId = cuid() + let newSessionId: string | undefined - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new MemoryDriver() - const session = new Session(ctx, sessionConfig, driver) + const driver = new CookieDriver(sessionConfig.cookie, ctx) + const session = new Session(sessionConfig, driver, emitter, ctx) await session.initiate(false) + session.regenerate() + newSessionId = session.sessionId + + assert.isTrue(session.hasRegeneratedSession) - session.flash({ success: 'User created succesfully' }) - session.flash({ error: 'There was an error too :wink' }) await session.commit() - ctx.response.send('') - ctx.response.finish() + response.finish() }) - const { header } = await supertest(server).get('/') + const sessionIdCookie = `adonis_session=${cookieClient.sign('adonis_session', sessionId)}` + const sessionStoreCookie = `${sessionId}=${cookieClient.encrypt(sessionId, { age: 22 })}` - const sessionId = await unsignCookie(app, header, sessionConfig.cookieName) - assert.exists(sessionId) - const session = MemoryDriver.sessions.get(sessionId)! + const { headers } = await supertest(server) + .get('/') + .set('cookie', `${sessionIdCookie}; ${sessionStoreCookie}`) - assert.deepEqual(new Store(session).all(), { - __flash__: { - success: 'User created succesfully', - error: 'There was an error too :wink', - }, + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) + + assert.property(cookies, 'adonis_session') + assert.property(cookies, sessionId!) + assert.equal(cookies[sessionId!].maxAge, -1) + assert.property(cookies, newSessionId!) + assert.equal(cookies[newSessionId!].maxAge, 90) + assert.deepEqual(cookieClient.decrypt(newSessionId!, cookies[newSessionId!].value), { + age: 22, }) }) +}) - test('always flash original input values', async ({ assert, fs }) => { - const { app } = await setup(fs) - - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) +test.group('Session | Flash', () => { + test('flash data using the session store', async ({ assert }) => { + let sessionId: string | undefined - ctx.request.setInitialBody({ username: 'virk', age: 28 }) - ctx.request.updateBody({ username: 'nikk', age: 22 }) + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new MemoryDriver() - const session = new Session(ctx, sessionConfig, driver) + const driver = new CookieDriver(sessionConfig.cookie, ctx) + const session = new Session(sessionConfig, driver, emitter, ctx) await session.initiate(false) - session.flashAll() + session.flash('status', 'Task created successfully') + sessionId = session.sessionId + await session.commit() - ctx.response.send('') - ctx.response.finish() + response.finish() }) - const { header } = await supertest(server).get('/') - - const sessionId = await unsignCookie(app, header, sessionConfig.cookieName) - assert.exists(sessionId) - const session = MemoryDriver.sessions.get(sessionId)! + const { headers } = await supertest(server).get('/') + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) - assert.deepEqual(new Store(session).all(), { + assert.deepEqual(cookieClient.decrypt(sessionId!, cookies[sessionId!].value), { __flash__: { - username: 'virk', - age: 28, + status: 'Task created successfully', }, }) }) - test('do not attempt to commit when initiate raises an exception', async ({ assert, fs }) => { - assert.plan(3) - const { app } = await setup(fs) + test('flash key-value pair', async ({ assert }) => { + let sessionId: string | undefined - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() - ctx.request.setInitialBody({ username: 'virk', age: 28 }) - ctx.request.updateBody({ username: 'nikk', age: 22 }) - - const driver = new MemoryDriver() - driver.read = function () { - throw new Error('Blowup') - } - - const session = new Session(ctx, sessionConfig, driver) + const driver = new CookieDriver(sessionConfig.cookie, ctx) + const session = new Session(sessionConfig, driver, emitter, ctx) + await session.initiate(false) - try { - await session.initiate(false) - } catch (error) { - assert.equal(error.message, 'Blowup') - } + session.flash({ status: 'Task created successfully' }) + sessionId = session.sessionId await session.commit() - ctx.response.send('') - ctx.response.finish() + response.finish() }) - const { header } = await supertest(server).get('/') + const { headers } = await supertest(server).get('/') + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) - const sessionId = await unsignCookie(app, header, sessionConfig.cookieName) - assert.exists(sessionId) - const session = MemoryDriver.sessions.get(sessionId)! - - assert.deepEqual(new Store(session).all(), {}) + assert.deepEqual(cookieClient.decrypt(sessionId!, cookies[sessionId!].value), { + __flash__: { + status: 'Task created successfully', + }, + }) }) - test('reflash existing flash values', async ({ assert, fs }) => { - const { app } = await setup(fs) + test('flash input values', async ({ assert }) => { + let sessionId: string | undefined - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() + ctx.request.setInitialBody({ + username: 'virk', + }) - const driver = new MemoryDriver() - const session = new Session(ctx, sessionConfig, driver) + const driver = new CookieDriver(sessionConfig.cookie, ctx) + const session = new Session(sessionConfig, driver, emitter, ctx) await session.initiate(false) - session.reflash() - await session.commit() - ctx.response.send('') - ctx.response.finish() - }) - /** - * Initial driver value - */ - const store = new Store(null) - store.set('__flash__', { - username: 'virk', - success: 'User created', - }) + session.flash({ status: 'Task created successfully' }) + session.flashAll() + sessionId = session.sessionId - MemoryDriver.sessions.set('1234', store.toJSON()) + await session.commit() + response.finish() + }) - const { header } = await supertest(server) - .get('/') - .set('cookie', await signCookie(app, '1234', sessionConfig.cookieName)) + const { headers } = await supertest(server).get('/') + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) - const sessionId = await unsignCookie(app, header, sessionConfig.cookieName) - assert.exists(sessionId) - const session = MemoryDriver.sessions.get(sessionId)! - assert.deepEqual(new Store(session).all(), { + assert.deepEqual(cookieClient.decrypt(sessionId!, cookies[sessionId!].value), { __flash__: { username: 'virk', - success: 'User created', + status: 'Task created successfully', }, }) }) - test('cherry pick keys during reflash', async ({ assert, fs }) => { - const { app } = await setup(fs) + test('flash selected input values', async ({ assert }) => { + let sessionId: string | undefined - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() + ctx.request.setInitialBody({ + username: 'virk', + age: 22, + }) - const driver = new MemoryDriver() - const session = new Session(ctx, sessionConfig, driver) + const driver = new CookieDriver(sessionConfig.cookie, ctx) + const session = new Session(sessionConfig, driver, emitter, ctx) await session.initiate(false) - session.reflashOnly(['username']) + session.flash({ status: 'Task created successfully' }) - await session.commit() - ctx.response.send('') - ctx.response.finish() - }) + /** + * The last method call will overwrite others + */ + session.flashAll() + session.flashExcept(['username']) + session.flashOnly(['username']) - /** - * Initial driver value - */ - const store = new Store(null) - store.set('__flash__', { - username: 'virk', - success: 'User created', - }) + sessionId = session.sessionId - MemoryDriver.sessions.set('1234', store.toJSON()) + await session.commit() + response.finish() + }) - const { header } = await supertest(server) - .get('/') - .set('cookie', await signCookie(app, '1234', sessionConfig.cookieName)) + const { headers } = await supertest(server).get('/') + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) - const sessionId = await unsignCookie(app, header, sessionConfig.cookieName) - assert.exists(sessionId) - const session = MemoryDriver.sessions.get(sessionId)! - assert.deepEqual(new Store(session).all(), { + assert.deepEqual(cookieClient.decrypt(sessionId!, cookies[sessionId!].value), { __flash__: { username: 'virk', + status: 'Task created successfully', }, }) }) - test('ignore keys during reflash', async ({ assert, fs }) => { - const { app } = await setup(fs) + test('read flash messages from the request', async ({ assert }) => { + let sessionId: string | undefined - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new MemoryDriver() - const session = new Session(ctx, sessionConfig, driver) + const driver = new CookieDriver(sessionConfig.cookie, ctx) + const session = new Session(sessionConfig, driver, emitter, ctx) await session.initiate(false) + sessionId = session.sessionId + + if (request.url() === '/prg') { + response.json(session.flashMessages.all()) + await session.commit() + response.finish() + } else { + session.flash({ status: 'Task created successfully' }) + await session.commit() + } - session.reflashExcept(['username']) - - await session.commit() - ctx.response.send('') - ctx.response.finish() + response.finish() }) - /** - * Initial driver value - */ - const store = new Store(null) - store.set('__flash__', { - username: 'virk', - success: 'User created', - }) + const { headers } = await supertest(server).get('/') + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) - MemoryDriver.sessions.set('1234', store.toJSON()) + const { body, headers: newHeaders } = await supertest(server) + .get('/prg') + .set( + 'Cookie', + `adonis_session=${cookies.adonis_session.value}; ${sessionId}=${cookies[sessionId!].value}` + ) - const { header } = await supertest(server) - .get('/') - .set('cookie', await signCookie(app, '1234', sessionConfig.cookieName)) + const newCookies = setCookieParser.parse(newHeaders['set-cookie'], { map: true }) - const sessionId = await unsignCookie(app, header, sessionConfig.cookieName) - assert.exists(sessionId) - const session = MemoryDriver.sessions.get(sessionId)! - assert.deepEqual(new Store(session).all(), { - __flash__: { - success: 'User created', - }, - }) + assert.deepEqual(body, { status: 'Task created successfully' }) + assert.equal(newCookies[sessionId!].maxAge, -1) }) - test('should not store session if empty - redis', async ({ assert, fs }) => { - const { app } = await setup(fs, { - redis: { - connection: 'session', - connections: { - session: { - host: process.env.REDIS_HOST || '0.0.0.0', - port: process.env.REDIS_PORT || 6379, - }, - }, - }, + test('reflash flash messages', async ({ assert }) => { + let sessionId: string | undefined - session: { - driver: 'redis', - cookieName: 'adonis-session', - age: '2h', - redisConnection: 'session', - }, - }) + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) - - await ctx.session.initiate(false) - await ctx.session.commit() + const driver = new CookieDriver(sessionConfig.cookie, ctx) + const session = new Session(sessionConfig, driver, emitter, ctx) + await session.initiate(false) + sessionId = session.sessionId + + if (request.url() === '/prg') { + response.json(session.flashMessages.all()) + await session.commit() + response.finish() + } else if (request.url() === '/reflash') { + session.reflash() + await session.commit() + } else { + session.flash({ status: 'Task created successfully' }) + await session.commit() + } - ctx.response.finish() + response.finish() }) - const { header } = await supertest(server).get('/') - const sessionId = await unsignCookie(app, header, 'adonis-session') - const redis = (await app.container.make('redis')) as RedisService + const { headers } = await supertest(server).get('/') + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) - const sessionValue = await redis.get(sessionId) - assert.isNull(sessionValue) - }) + const { headers: reflashedHeaders } = await supertest(server) + .get('/reflash') + .set( + 'Cookie', + `adonis_session=${cookies.adonis_session.value}; ${sessionId}=${cookies[sessionId!].value}` + ) + const reflashedCookies = setCookieParser.parse(reflashedHeaders['set-cookie'], { map: true }) - test('should delete session storage if empty - redis', async ({ assert, fs }) => { - const { app } = await setup(fs, { - redis: { - connection: 'session', - connections: { - session: { - host: process.env.REDIS_HOST || '0.0.0.0', - port: process.env.REDIS_PORT || 6379, - }, - }, - }, + const { body, headers: newHeaders } = await supertest(server) + .get('/prg') + .set( + 'Cookie', + `adonis_session=${reflashedCookies.adonis_session.value}; ${sessionId}=${ + reflashedCookies[sessionId!].value + }` + ) - session: { - driver: 'redis', - cookieName: 'adonis-session', - age: '2h', - redisConnection: 'session', - }, - }) + const newCookies = setCookieParser.parse(newHeaders['set-cookie'], { map: true }) - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) + assert.deepEqual(body, { status: 'Task created successfully' }) + assert.equal(newCookies[sessionId!].maxAge, -1) + }) - await ctx.session.initiate(false) + test('reflash and flash together', async ({ assert }) => { + let sessionId: string | undefined - if (ctx.request.qs().set) { - ctx.session.put('user', { username: 'jul' }) - } + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() - if (ctx.request.qs().delete) { - ctx.session.forget('user') + const driver = new CookieDriver(sessionConfig.cookie, ctx) + const session = new Session(sessionConfig, driver, emitter, ctx) + await session.initiate(false) + sessionId = session.sessionId + + if (request.url() === '/prg') { + response.json(session.flashMessages.all()) + await session.commit() + response.finish() + } else if (request.url() === '/reflash') { + session.reflash() + session.reflashExcept(['id']) + session.reflashOnly(['id']) + session.flash({ state: 'success' }) + await session.commit() + } else { + session.flash({ status: 'Task created successfully', id: 1 }) + await session.commit() } - await ctx.session.commit() - - ctx.response.finish() + response.finish() }) - const { header } = await supertest(server).get('/?set=1') - const sessionId = await unsignCookie(app, header, 'adonis-session') - const redis = (await app.container.make('redis')) as RedisService + const { headers } = await supertest(server).get('/') + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) - const sessionValue = await redis.get(sessionId) - assert.isNotNull(sessionValue) + const { headers: reflashedHeaders } = await supertest(server) + .get('/reflash') + .set( + 'Cookie', + `adonis_session=${cookies.adonis_session.value}; ${sessionId}=${cookies[sessionId!].value}` + ) + const reflashedCookies = setCookieParser.parse(reflashedHeaders['set-cookie'], { map: true }) - const { header: header2 } = await supertest(server) - .get('/?delete=1') - .set('cookie', await signCookie(app, sessionId, 'adonis-session')) + const { body, headers: newHeaders } = await supertest(server) + .get('/prg') + .set( + 'Cookie', + `adonis_session=${reflashedCookies.adonis_session.value}; ${sessionId}=${ + reflashedCookies[sessionId!].value + }` + ) - const sessionId2 = await unsignCookie(app, header2, 'adonis-session') - assert.equal(sessionId, sessionId2) + const newCookies = setCookieParser.parse(newHeaders['set-cookie'], { map: true }) - const sessionValue2 = await redis.get(sessionId2) - assert.isNull(sessionValue2) + assert.deepEqual(body, { id: 1, state: 'success' }) + assert.equal(newCookies[sessionId!].maxAge, -1) }) + + test('throw error when trying to write to flash messages without initialization', async () => { + const ctx = new HttpContextFactory().create() + const driver = new CookieDriver(sessionConfig.cookie, ctx) + const session = new Session(sessionConfig, driver, emitter, ctx) + session.flash('username', 'virk') + }).throws( + 'Session store has not been initiated. Make sure you have registered the session middleware' + ) + + test('throw error when trying to write flash messages to a read only store', async () => { + const ctx = new HttpContextFactory().create() + const driver = new CookieDriver(sessionConfig.cookie, ctx) + const session = new Session(sessionConfig, driver, emitter, ctx) + + await session.initiate(true) + session.flash('username', 'foo') + }).throws('Session store is in readonly mode and cannot be mutated') }) diff --git a/tests/session_client.spec.ts b/tests/session_client.spec.ts index 909cade..1d49d5b 100644 --- a/tests/session_client.spec.ts +++ b/tests/session_client.spec.ts @@ -8,174 +8,74 @@ */ import { test } from '@japa/runner' -import supertest from 'supertest' -import { createServer } from 'node:http' -import setCookieParser from 'set-cookie-parser' +import { CookieClient } from '@adonisjs/core/http' +import { EncryptionFactory } from '@adonisjs/core/factories/encryption' +import { SessionClient } from '../src/client.js' +import type { SessionConfig } from '../src/types/main.js' import { MemoryDriver } from '../src/drivers/memory.js' -import { setup, sessionConfig, createHttpContext } from '../test_helpers/index.js' -import { SessionManagerFactory } from '../factories/session_manager_factory.js' -import { CookieClient } from '@adonisjs/core/http' + +const encryption = new EncryptionFactory().create() +const cookieClient = new CookieClient(encryption) +const sessionConfig: SessionConfig = { + enabled: true, + age: '2 hours', + clearWithBrowser: false, + cookieName: 'adonis_session', + driver: 'cookie', + cookie: {}, +} test.group('Session Client', (group) => { group.each.teardown(async () => { - MemoryDriver.sessions = new Map() + MemoryDriver.sessions.clear() }) - test('set session using the session client', async ({ fs, assert }) => { - assert.plan(1) - const { app } = await setup(fs) - - const config = Object.assign({}, sessionConfig, { driver: 'memory', clearWithBrowser: true }) - const manager = new SessionManagerFactory().merge(config).create(app) - - const client = manager.client() - client.set('username', 'virk') - const { signedSessionId, cookieName } = await client.commit() - - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) - const session = manager.create(ctx) - await session.initiate(false) + test('define session data using session id', async ({ assert }) => { + const driver = new MemoryDriver() + const client = new SessionClient(sessionConfig, driver, cookieClient) + const { sessionId, signedSessionId, cookieName } = await client.commit( + { foo: 'bar' }, + { success: true } + ) - assert.deepEqual(session.all(), { username: 'virk' }) - ctx.response.finish() + assert.equal(cookieName, 'adonis_session') + assert.equal(cookieClient.unsign(cookieName, signedSessionId), sessionId) + assert.deepEqual(driver.read(sessionId), { + foo: 'bar', + __flash__: { + success: true, + }, }) - - await supertest(server).get('/').set('Cookie', `${cookieName}=${signedSessionId}`) }) - test('set flash messages', async ({ fs, assert }) => { - assert.plan(1) - const { app } = await setup(fs) - - const config = Object.assign({}, sessionConfig, { driver: 'memory', clearWithBrowser: true }) - const manager = new SessionManagerFactory().merge(config).create(app) - - const client = manager.client() - client.flashMessages.merge({ foo: 'bar' }) - const { signedSessionId, cookieName } = await client.commit() - - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) + test('read existing session data', async ({ assert }) => { + const driver = new MemoryDriver() + const client = new SessionClient(sessionConfig, driver, cookieClient) + const { sessionId } = await client.commit({ foo: 'bar' }, { success: true }) - const session = manager.create(ctx) - await session.initiate(false) - - assert.deepEqual(session.flashMessages.all(), { foo: 'bar' }) - ctx.response.finish() + assert.deepEqual(await client.load({}), { + sessionId, + session: { + foo: 'bar', + }, + flashMessages: { + success: true, + }, }) - - await supertest(server).get('/').set('Cookie', `${cookieName}=${signedSessionId}`) }) - test('clear session store', async ({ fs, assert }) => { - assert.plan(1) - const { app } = await setup(fs) - - const config = Object.assign({}, sessionConfig, { driver: 'memory', clearWithBrowser: true }) - const manager = new SessionManagerFactory().merge(config).create(app) - - const client = manager.client() - client.set('username', 'virk') - const { signedSessionId, cookieName } = await client.commit() - - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) - const session = manager.create(ctx) - await session.initiate(false) - - assert.deepEqual(session.all(), {}) - ctx.response.finish() - }) + test('clear session data', async ({ assert }) => { + const driver = new MemoryDriver() + const client = new SessionClient(sessionConfig, driver, cookieClient) + const { sessionId } = await client.commit({ foo: 'bar' }, { success: true }) await client.forget() - await supertest(server).get('/').set('Cookie', `${cookieName}=${signedSessionId}`) - }) - - test('get session data from the driver', async ({ fs, assert }) => { - const { app } = await setup(fs) - const config = Object.assign({}, sessionConfig, { driver: 'memory', clearWithBrowser: true }) - const manager = new SessionManagerFactory().merge(config).create(app) - - const client = manager.client() - client.set('username', 'virk') - const { signedSessionId, cookieName } = await client.commit() - - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) - const session = manager.create(ctx) - await session.initiate(false) - session.put('age', 22) - session.regenerate() - - await session.commit() - ctx.response.finish() + assert.deepEqual(await client.load({}), { + sessionId, + session: {}, + flashMessages: null, }) - - const response = await supertest(server) - .get('/') - .set('Cookie', `${cookieName}=${signedSessionId}`) - - const cookieClient = new CookieClient(await app.container.make('encryption')) - const cookies = setCookieParser.parse(response.header['set-cookie'], { map: true }) - const parsedCookies = Object.keys(cookies).reduce( - (result, key) => { - const value = cookies[key] - value.value = cookieClient.parse(value.name, value.value) - result[key] = value - return result - }, - {} as Record - ) - - const { session, flashMessages } = await client.load(parsedCookies) - - assert.deepEqual(session, { username: 'virk', age: 22 }) - assert.isNull(flashMessages) - }) - - test('get flash messages from the driver', async ({ fs, assert }) => { - const { app } = await setup(fs) - - const config = Object.assign({}, sessionConfig, { driver: 'memory', clearWithBrowser: true }) - const manager = new SessionManagerFactory().merge(config).create(app) - - const client = manager.client() - const { signedSessionId, cookieName } = await client.commit() - - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) - const session = manager.create(ctx) - await session.initiate(false) - - session.flash({ foo: 'bar' }) - session.put('username', 'virk') - - await session.commit() - ctx.response.finish() - }) - - const response = await supertest(server) - .get('/') - .set('Cookie', `${cookieName}=${signedSessionId}`) - - const cookieClient = new CookieClient(await app.container.make('encryption')) - const cookies = setCookieParser.parse(response.header['set-cookie'], { map: true }) - const parsedCookies = Object.keys(cookies).reduce( - (result, key) => { - const value = cookies[key] - value.value = cookieClient.parse(value.name, value.value) - result[key] = value - return result - }, - {} as Record - ) - - const { session, flashMessages } = await client.load(parsedCookies) - - assert.deepEqual(session, { username: 'virk' }) - assert.deepEqual(flashMessages, { foo: 'bar' }) }) }) diff --git a/tests/session_manager.spec.ts b/tests/session_manager.spec.ts deleted file mode 100644 index d7192ba..0000000 --- a/tests/session_manager.spec.ts +++ /dev/null @@ -1,169 +0,0 @@ -/** - * @adonisjs/session - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' -import supertest from 'supertest' -import { createServer } from 'node:http' -import { MessageBuilder } from '@poppinss/utils' -import setCookieParser from 'set-cookie-parser' -import { HttpContextFactory } from '@adonisjs/core/factories/http' - -import { Store } from '../src/store.js' -import { - setup, - sessionConfig, - unsignCookie, - getRedisManager, - createHttpContext, -} from '../test_helpers/index.js' -import { SessionManagerFactory } from '../factories/session_manager_factory.js' -import { SessionDriverContract } from '../src/types.js' - -test.group('Session Manager', () => { - test('do not set maxAge when clearWithBrowser is true', async ({ assert, fs }) => { - const { app } = await setup(fs) - const config = Object.assign({}, sessionConfig, { clearWithBrowser: true }) - const manager = new SessionManagerFactory().merge(config).create(app) - - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) - const session = manager.create(ctx) - await session.initiate(false) - - session.put('user', { username: 'virk' }) - await session.commit() - ctx.response.send('') - ctx.response.finish() - }) - - const { header } = await supertest(server).get('/') - assert.lengthOf(header['set-cookie'][0].split(';'), 3) - }) - - test('set maxAge when clearWithBrowser is false', async ({ assert, fs }) => { - const { app } = await setup(fs) - const manager = new SessionManagerFactory().create(app) - - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) - - const session = manager.create(ctx) - await session.initiate(false) - - session.put('user', { username: 'virk' }) - await session.commit() - ctx.response.send('') - ctx.response.finish() - }) - - const { header } = await supertest(server).get('/') - const cookies = setCookieParser.parse(header['set-cookie'][0]) - - assert.isDefined(cookies[0].maxAge) - assert.equal(cookies[0].maxAge, sessionConfig.age) - }) - - test('use file driver to persist session value', async ({ assert, fs }) => { - const { app } = await setup(fs) - const config = Object.assign({}, sessionConfig, { - driver: 'file', - file: { location: fs.basePath }, - }) - const manager = new SessionManagerFactory().merge(config).create(app) - - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) - const session = manager.create(ctx) - await session.initiate(false) - - session.put('user', { username: 'virk' }) - await session.commit() - ctx.response.send('') - ctx.response.finish() - }) - - const { header } = await supertest(server).get('/') - - const sessionId = await unsignCookie(app, header, sessionConfig.cookieName) - const sessionContents = await fs.contents(`${sessionId}.txt`) - const sessionValues = new MessageBuilder().verify(sessionContents, sessionId) - assert.deepEqual(new Store(sessionValues).all(), { user: { username: 'virk' } }) - }) - - test('use redis driver to persist session value', async ({ assert, fs }) => { - const { app } = await setup(fs) - const config = Object.assign({}, sessionConfig, { - driver: 'redis', - redisConnection: 'session', - }) - - const redis = getRedisManager(app) - const manager = new SessionManagerFactory() - .merge(config) - .mergeRedisManagerOptions({ - connection: 'session', - connections: { session: { host: 'localhost', port: 6379 } }, - }) - .create(app) - - // @ts-ignore - app.container.singleton('redis', () => redis) - - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) - const session = manager.create(ctx) - await session.initiate(false) - - session.put('user', { username: 'virk' }) - await session.commit() - ctx.response.send('') - ctx.response.finish() - }) - - const { header } = await supertest(server).get('/') - const sessionId = await unsignCookie(app, header, sessionConfig.cookieName) - - const sessionContents = await redis.connection('session').get(sessionId) - const sessionValues = new MessageBuilder().verify(sessionContents, sessionId) - - await redis.connection('session').del(sessionId) - await redis.disconnectAll() - - assert.deepEqual(new Store(sessionValues).all(), { user: { username: 'virk' } }) - }) - - test('extend by adding a custom driver', async ({ assert, fs }) => { - assert.plan(1) - - const { app } = await setup(fs, { - session: { - driver: 'mongo', - }, - }) - - class MongoDriver implements SessionDriverContract { - read() { - return {} - } - write(_: string, data: any) { - assert.deepEqual(data, { name: 'virk' }) - } - touch() {} - destroy() {} - } - - const sessionManager = await app.container.make('session') - sessionManager.extend('mongo', () => new MongoDriver()) - const session = sessionManager.create(new HttpContextFactory().create()) - - await session.initiate(false) - session.put('name', 'virk') - await session.commit() - }) -}) diff --git a/tests/session_middleware.spec.ts b/tests/session_middleware.spec.ts index 4fb3a88..fb6deb3 100644 --- a/tests/session_middleware.spec.ts +++ b/tests/session_middleware.spec.ts @@ -1,5 +1,5 @@ -/* - * @adonisjs/cors +/** + * @adonisjs/session * * (c) AdonisJS * @@ -9,40 +9,85 @@ import supertest from 'supertest' import { test } from '@japa/runner' -import { createServer } from 'node:http' +import setCookieParser from 'set-cookie-parser' +import { CookieClient } from '@adonisjs/core/http' +import { EncryptionFactory } from '@adonisjs/core/factories/encryption' +import { HttpContextFactory, RequestFactory, ResponseFactory } from '@adonisjs/core/factories/http' -import SessionMiddleware from '../src/session_middleware.js' -import { createHttpContext, setup, unsignCookie } from '../test_helpers/index.js' +import { httpServer } from '../test_helpers/index.js' +import { CookieDriver } from '../src/drivers/cookie.js' +import type { SessionConfig } from '../src/types/main.js' +import sessionDriversList from '../src/drivers_collection.js' +import { SessionMiddlewareFactory } from '../factories/session_middleware_factory.js' -test.group('Session', () => { - test('should initiate and commit the session', async ({ assert, fs }) => { - assert.plan(3) +const encryption = new EncryptionFactory().create() +const cookieClient = new CookieClient(encryption) +const sessionConfig: SessionConfig = { + enabled: true, + age: '2 hours', + clearWithBrowser: false, + cookieName: 'adonis_session', + driver: 'cookie', + cookie: {}, +} - const { app } = await setup(fs, { - session: { - enabled: true, - cookieName: 'adonis-session', - driver: 'file', - file: { location: fs.basePath }, - }, - }) +test.group('Session middleware', () => { + test('initiate and commit session around request', async ({ assert }) => { + let sessionId: string | undefined + + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() + + const middleware = await new SessionMiddlewareFactory() + .merge({ + config: sessionConfig, + }) + .create() - const server = createServer(async (req, res) => { - const session = await app.container.make('session') - const middleware = new SessionMiddleware(session) - const ctx = await createHttpContext(app, req, res) await middleware.handle(ctx, () => { - assert.isTrue(ctx.session.initiated) - ctx.session.put('username', 'jul') + sessionId = ctx.session.sessionId + ctx.session.put('username', 'virk') + ctx.session.flash({ status: 'Completed' }) }) + + ctx.response.finish() + }) + + const { headers } = await supertest(server).get('/') + + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) + assert.deepEqual(cookieClient.decrypt(sessionId!, cookies[sessionId!].value), { + username: 'virk', + __flash__: { + status: 'Completed', + }, + }) + }) + + test('do not initiate session when not enabled', async ({ assert }) => { + sessionDriversList.extend('cookie', (config, ctx) => new CookieDriver(config.cookie, ctx)) + + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() + + const middleware = await new SessionMiddlewareFactory() + .merge({ + config: { ...sessionConfig, enabled: false }, + }) + .create() + + await middleware.handle(ctx, () => {}) + assert.isUndefined(ctx.session) ctx.response.finish() }) - const { header } = await supertest(server).get('/') - const sessionId = await unsignCookie(app, header, 'adonis-session') + const { headers } = await supertest(server).get('/') - await assert.fileExists(`${sessionId}.txt`) - const content = await fs.contentsJson(`${sessionId}.txt`) - assert.deepInclude(content, { message: { username: 'jul' } }) + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) + assert.deepEqual(cookies, {}) }) }) diff --git a/tests/session_provider.spec.ts b/tests/session_provider.spec.ts index ee51978..eed8e38 100644 --- a/tests/session_provider.spec.ts +++ b/tests/session_provider.spec.ts @@ -1,5 +1,5 @@ /* - * @adonisjs/session + * @adonisjs/redis * * (c) AdonisJS * @@ -8,145 +8,189 @@ */ import { test } from '@japa/runner' -import { createServer } from 'node:http' -import { ApiClient, ApiRequest } from '@japa/api-client' -import { createHttpContext, setup } from '../test_helpers/index.js' -import { SessionManager } from '../src/session_manager.js' -import { MemoryDriver } from '../src/drivers/memory.js' +import { IgnitorFactory } from '@adonisjs/core/factories' -test.group('Session Provider', (group) => { - group.each.teardown(async () => { - ApiClient.clearSetupHooks() - ApiClient.clearTeardownHooks() - ApiClient.clearRequestHandlers() - }) +import { defineConfig } from '../index.js' +import SessionMiddleware from '../src/session_middleware.js' +import sessionDriversList from '../src/drivers_collection.js' - test('register session provider', async ({ fs, assert }) => { - const { app } = await setup(fs) +const BASE_URL = new URL('./tmp/', import.meta.url) +const IMPORTER = (filePath: string) => { + if (filePath.startsWith('./') || filePath.startsWith('../')) { + return import(new URL(filePath, BASE_URL).href) + } + return import(filePath) +} - assert.instanceOf(await app.container.make('session'), SessionManager) - assert.deepEqual(await app.container.make('session'), await app.container.make('session')) +test.group('Session Provider', (group) => { + group.each.setup(() => { + return () => { + sessionDriversList.list = {} + } }) - test('register test api request methods', async ({ assert, fs }) => { - await setup(fs, {}, 'test') - - assert.isTrue(ApiRequest.prototype.hasOwnProperty('session')) - assert.isTrue(ApiRequest.prototype.hasOwnProperty('flashMessages')) - assert.isTrue(ApiRequest.prototype.hasOwnProperty('sessionClient')) + test('register session provider', async ({ assert }) => { + const ignitor = new IgnitorFactory() + .merge({ + rcFileContents: { + providers: ['../../providers/session_provider.js'], + }, + }) + .withCoreConfig() + .withCoreProviders() + .merge({ + config: { + session: defineConfig({ + driver: 'cookie', + }), + }, + }) + .create(BASE_URL, { + importer: IMPORTER, + }) + + const app = ignitor.createApp('web') + await app.init() + await app.boot() + + assert.instanceOf(await app.container.make(SessionMiddleware), SessionMiddleware) }) - test('set session before making the api request', async ({ fs, assert }) => { - const { app } = await setup( - fs, - { session: { driver: 'memory', cookieName: 'adonis-session' } }, - 'test' - ) - - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) - - await ctx.session.initiate(false) - - try { - ctx.response.send(ctx.session.all()) - } catch (error) { - ctx.response.status(500).send(error.stack) - } - - ctx.response.finish() - }) - server.listen(3333) - - const client = new ApiClient('http://localhost:3333') - const response = await client.get('/').session({ username: 'virk' }) - server.close() - - assert.deepEqual(response.status(), 200) - assert.deepEqual(response.body(), { username: 'virk' }) + test('register cookie driver with the driversCollection', async ({ assert }) => { + const ignitor = new IgnitorFactory() + .merge({ + rcFileContents: { + providers: ['../../providers/session_provider.js'], + }, + }) + .withCoreConfig() + .withCoreProviders() + .merge({ + config: { + session: defineConfig({ + driver: 'cookie', + }), + }, + }) + .create(BASE_URL, { + importer: IMPORTER, + }) + + const app = ignitor.createApp('web') + await app.init() + await app.boot() + + assert.deepEqual(sessionDriversList.list, {}) + assert.instanceOf(await app.container.make(SessionMiddleware), SessionMiddleware) + + assert.property(sessionDriversList.list, 'cookie') + assert.notProperty(sessionDriversList.list, 'file') + assert.notProperty(sessionDriversList.list, 'memory') + assert.notProperty(sessionDriversList.list, 'redis') }) - test('get session data from the response', async ({ fs, assert }) => { - const { app } = await setup( - fs, - { session: { driver: 'memory', cookieName: 'adonis-session' } }, - 'test' - ) - - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) - - await ctx.session.initiate(false) - ctx.session.put('username', 'virk') - await ctx.session.commit() - - ctx.response.finish() - }) - server.listen(3333) - - const client = new ApiClient('http://localhost:3333', assert) - const response = await client.get('/') - server.close() - - assert.equal(MemoryDriver.sessions.size, 0) - assert.deepEqual(response.status(), 200) - - response.assertSession('username', 'virk') - response.assertSessionMissing('age') + test('register file driver with the driversCollection', async ({ fs, assert }) => { + const ignitor = new IgnitorFactory() + .merge({ + rcFileContents: { + providers: ['../../providers/session_provider.js'], + }, + }) + .withCoreConfig() + .withCoreProviders() + .merge({ + config: { + session: defineConfig({ + driver: 'file', + file: { + location: fs.basePath, + }, + }), + }, + }) + .create(BASE_URL, { + importer: IMPORTER, + }) + + const app = ignitor.createApp('web') + await app.init() + await app.boot() + + assert.deepEqual(sessionDriversList.list, {}) + assert.instanceOf(await app.container.make(SessionMiddleware), SessionMiddleware) + + assert.property(sessionDriversList.list, 'file') + assert.notProperty(sessionDriversList.list, 'cookie') + assert.notProperty(sessionDriversList.list, 'memory') + assert.notProperty(sessionDriversList.list, 'redis') }) - test('get flash messages from the response', async ({ fs, assert }) => { - const { app } = await setup( - fs, - { session: { driver: 'memory', cookieName: 'adonis-session' } }, - 'test' - ) - - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) - - await ctx.session.initiate(false) - ctx.session.flash({ username: 'virk' }) - await ctx.session.commit() - - ctx.response.finish() - }) - server.listen(3333) - - const client = new ApiClient('http://localhost:3333', assert) - const response = await client.get('/') - server.close() - - assert.equal(MemoryDriver.sessions.size, 0) - assert.deepEqual(response.status(), 200) - - response.assertFlashMessage('username', 'virk') - response.assertFlashMissing('age') + test('register redis driver with the driversCollection', async ({ assert }) => { + const ignitor = new IgnitorFactory() + .merge({ + rcFileContents: { + providers: ['../../providers/session_provider.js', '@adonisjs/redis/redis_provider'], + }, + }) + .withCoreConfig() + .withCoreProviders() + .merge({ + config: { + session: defineConfig({ + driver: 'redis', + redis: { + connection: 'main', + }, + }), + }, + }) + .create(BASE_URL, { + importer: IMPORTER, + }) + + const app = ignitor.createApp('web') + await app.init() + await app.boot() + + assert.deepEqual(sessionDriversList.list, {}) + assert.instanceOf(await app.container.make(SessionMiddleware), SessionMiddleware) + + assert.property(sessionDriversList.list, 'redis') + assert.notProperty(sessionDriversList.list, 'cookie') + assert.notProperty(sessionDriversList.list, 'memory') + assert.notProperty(sessionDriversList.list, 'file') }) - test('destroy session when request fails', async ({ fs, assert }) => { - const { app } = await setup( - fs, - { session: { driver: 'memory', cookieName: 'adonis-session' } }, - 'test' - ) - - const server = createServer(async (req, res) => { - const ctx = await createHttpContext(app, req, res) - - await ctx.session.initiate(false) - ctx.session.put('username', 'virk') - await ctx.session.commit() - - ctx.response.status(500).send('Server error') - ctx.response.finish() - }) - server.listen(3333) - - const client = new ApiClient('http://localhost:3333', assert) - await assert.rejects(() => client.get('/')) - server.close() - - assert.equal(MemoryDriver.sessions.size, 0) + test('register memory driver with the driversCollection', async ({ assert }) => { + const ignitor = new IgnitorFactory() + .merge({ + rcFileContents: { + providers: ['../../providers/session_provider.js'], + }, + }) + .withCoreConfig() + .withCoreProviders() + .merge({ + config: { + session: defineConfig({ + driver: 'memory', + }), + }, + }) + .create(BASE_URL, { + importer: IMPORTER, + }) + + const app = ignitor.createApp('web') + await app.init() + await app.boot() + + assert.deepEqual(sessionDriversList.list, {}) + assert.instanceOf(await app.container.make(SessionMiddleware), SessionMiddleware) + + assert.property(sessionDriversList.list, 'memory') + assert.notProperty(sessionDriversList.list, 'cookie') + assert.notProperty(sessionDriversList.list, 'redis') + assert.notProperty(sessionDriversList.list, 'file') }) }) diff --git a/tsconfig.json b/tsconfig.json index 183f742..9d417c2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,8 +3,5 @@ "compilerOptions": { "rootDir": "./", "outDir": "./build", - "types": [ - "@adonisjs/view" - ] }, } From 16e54f91d6a7a22f0d92063bb89eadbadbed0d7a Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 1 Aug 2023 08:59:42 +0530 Subject: [PATCH 051/112] refactor: add debug logs --- package.json | 3 ++- src/debug.ts | 12 ++++++++++++ src/define_config.ts | 2 ++ src/drivers/cookie.ts | 7 +++++++ src/drivers/file.ts | 8 ++++++++ src/drivers/redis.ts | 8 ++++++++ src/session.ts | 6 ++++++ 7 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 src/debug.ts diff --git a/package.json b/package.json index a5cbdea..9dce267 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ }, "scripts": { "pretest": "npm run lint", - "test": "c8 npm run quick:test", + "test": "cross-env NODE_DEBUG=adonisjs:session c8 npm run quick:test", "clean": "del-cli build", "typecheck": "tsc --noEmit", "copy:templates": "copyfiles \"stubs/**/*.stub\" build", @@ -57,6 +57,7 @@ "@types/supertest": "^2.0.12", "c8": "^8.0.0", "copyfiles": "^2.4.1", + "cross-env": "^7.0.3", "del-cli": "^5.0.0", "eslint": "^8.45.0", "github-label-sync": "^2.3.1", diff --git a/src/debug.ts b/src/debug.ts new file mode 100644 index 0000000..6a9321e --- /dev/null +++ b/src/debug.ts @@ -0,0 +1,12 @@ +/* + * @adonisjs/session + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { debuglog } from 'node:util' + +export default debuglog('adonisjs:session') diff --git a/src/define_config.ts b/src/define_config.ts index d22a8a4..66b4577 100644 --- a/src/define_config.ts +++ b/src/define_config.ts @@ -12,6 +12,7 @@ import { InvalidArgumentsException } from '@poppinss/utils' import type { CookieOptions } from '@adonisjs/core/types/http' import type { SessionConfig } from './types/main.js' +import debug from './debug.js' /** * Helper to normalize session config @@ -35,6 +36,7 @@ export function defineConfig( * not a session cookie. */ if (!clearWithBrowser) { + debug('computing maxAge for session id cookie') cookieOptions.maxAge = string.seconds.parse(config.age || age) } diff --git a/src/drivers/cookie.ts b/src/drivers/cookie.ts index fda29ed..87f2fdd 100644 --- a/src/drivers/cookie.ts +++ b/src/drivers/cookie.ts @@ -10,6 +10,7 @@ import type { HttpContext } from '@adonisjs/core/http' import { CookieOptions } from '@adonisjs/core/types/http' import type { SessionData, SessionDriverContract } from '../types/main.js' +import debug from '../debug.js' /** * Cookie driver stores the session data inside an encrypted @@ -22,12 +23,15 @@ export class CookieDriver implements SessionDriverContract { constructor(config: Partial, ctx: HttpContext) { this.#config = config this.#ctx = ctx + debug('initiating cookie driver %O', this.#config) } /** * Read session value from the cookie */ read(sessionId: string): SessionData | null { + debug('cookie driver: reading session data %s', sessionId) + const cookieValue = this.#ctx.request.encryptedCookie(sessionId) if (typeof cookieValue !== 'object') { return null @@ -40,6 +44,7 @@ export class CookieDriver implements SessionDriverContract { * Write session values to the cookie */ write(sessionId: string, values: SessionData): void { + debug('cookie driver: writing session data %s: %O', sessionId, values) this.#ctx.response.encryptedCookie(sessionId, values, this.#config) } @@ -47,6 +52,7 @@ export class CookieDriver implements SessionDriverContract { * Removes the session cookie */ destroy(sessionId: string): void { + debug('cookie driver: destroying session data %s', sessionId) if (this.#ctx.request.cookiesList()[sessionId]) { this.#ctx.response.clearCookie(sessionId) } @@ -57,6 +63,7 @@ export class CookieDriver implements SessionDriverContract { */ touch(sessionId: string): void { const value = this.read(sessionId) + debug('cookie driver: touching session data %s', sessionId) if (!value) { return } diff --git a/src/drivers/file.ts b/src/drivers/file.ts index 682c53c..787851b 100644 --- a/src/drivers/file.ts +++ b/src/drivers/file.ts @@ -14,6 +14,7 @@ import { MessageBuilder } from '@poppinss/utils' import { access, mkdir, readFile, rm, writeFile, utimes, stat } from 'node:fs/promises' import type { FileDriverConfig, SessionData, SessionDriverContract } from '../types/main.js' +import debug from '../debug.js' /** * File driver writes the session data on the file system as. Each session @@ -26,6 +27,7 @@ export class FileDriver implements SessionDriverContract { constructor(config: FileDriverConfig, age: string | number) { this.#config = config this.#age = age + debug('initiating file driver %O', this.#config) } /** @@ -79,6 +81,7 @@ export class FileDriver implements SessionDriverContract { */ async read(sessionId: string): Promise { const filePath = this.#getFilePath(sessionId) + debug('file driver: reading session data %', sessionId) /** * Return null when no session id file exists in first @@ -94,6 +97,7 @@ export class FileDriver implements SessionDriverContract { */ const sessionWillExpireAt = stats.mtimeMs + string.milliseconds.parse(this.#age) if (Date.now() > sessionWillExpireAt) { + debug('file driver: expired session data %s', sessionId) return null } @@ -121,6 +125,8 @@ export class FileDriver implements SessionDriverContract { * Writes the session data to the disk as a string */ async write(sessionId: string, values: SessionData): Promise { + debug('file driver: writing session data %s: %O', sessionId, values) + const filePath = this.#getFilePath(sessionId) const message = new MessageBuilder().build(values, undefined, sessionId) @@ -131,6 +137,7 @@ export class FileDriver implements SessionDriverContract { * Removes the session file from the disk */ async destroy(sessionId: string): Promise { + debug('file driver: destroying session data %s', sessionId) await rm(this.#getFilePath(sessionId), { force: true }) } @@ -139,6 +146,7 @@ export class FileDriver implements SessionDriverContract { * persistence store */ async touch(sessionId: string): Promise { + debug('file driver: touching session data %s', sessionId) await utimes(this.#getFilePath(sessionId), new Date(), new Date()) } } diff --git a/src/drivers/redis.ts b/src/drivers/redis.ts index d60d8ba..b49ae6e 100644 --- a/src/drivers/redis.ts +++ b/src/drivers/redis.ts @@ -12,6 +12,7 @@ import { MessageBuilder } from '@poppinss/utils' import type { RedisService } from '@adonisjs/redis/types' import type { SessionDriverContract, RedisDriverConfig, SessionData } from '../types/main.js' +import debug from '../debug.js' /** * File driver to read/write session to filesystem @@ -25,6 +26,7 @@ export class RedisDriver implements SessionDriverContract { this.#config = config this.#redis = redis this.#ttlSeconds = string.seconds.parse(age) + debug('initiating redis driver %O', this.#config) } /** @@ -32,6 +34,8 @@ export class RedisDriver implements SessionDriverContract { * missing. */ async read(sessionId: string): Promise { + debug('redis driver: reading session data %s', sessionId) + const contents = await this.#redis.connection(this.#config.connection).get(sessionId) if (!contents) { return null @@ -52,6 +56,8 @@ export class RedisDriver implements SessionDriverContract { * Write session values to a file */ async write(sessionId: string, values: Object): Promise { + debug('redis driver: writing session data %s, %O', sessionId, values) + const message = new MessageBuilder().build(values, undefined, sessionId) await this.#redis .connection(this.#config.connection) @@ -62,6 +68,7 @@ export class RedisDriver implements SessionDriverContract { * Cleanup session file by removing it */ async destroy(sessionId: string): Promise { + debug('redis driver: destroying session data %s', sessionId) await this.#redis.connection(this.#config.connection).del(sessionId) } @@ -69,6 +76,7 @@ export class RedisDriver implements SessionDriverContract { * Updates the value expiry */ async touch(sessionId: string): Promise { + debug('redis driver: touching session data %s', sessionId) await this.#redis.connection(this.#config.connection).expire(sessionId, this.#ttlSeconds) } } diff --git a/src/session.ts b/src/session.ts index d6e58ba..c12afec 100644 --- a/src/session.ts +++ b/src/session.ts @@ -20,6 +20,7 @@ import type { AllowedSessionValues, SessionDriverContract, } from './types/main.js' +import debug from './debug.js' /** * The session class exposes the API to read and write values to @@ -176,6 +177,8 @@ export class Session { return } + debug('initiating session (readonly: %s)', readonly) + this.#readonly = readonly const contents = await this.#driver.read(this.#sessionId) this.#store = new Store(contents) @@ -185,6 +188,7 @@ export class Session { * copy of it. */ if (this.has(this.flashKey)) { + debug('reading flash data') if (this.#readonly) { this.flashMessages.update(this.get(this.flashKey, null)) } else { @@ -353,6 +357,8 @@ export class Session { this.put(this.flashKey, { ...reflashed, ...input, ...others }) } + debug('committing session data') + /** * Touch the session id cookie to stay alive */ From 7d0b4802cdadfc2cbd7436186079a80ed14a9691 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 1 Aug 2023 09:02:40 +0530 Subject: [PATCH 052/112] docs: update README --- README.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index bf78a66..83636f3 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,10 @@
-[![gh-workflow-image]][gh-workflow-url] [![npm-image]][npm-url] ![][typescript-image] [![license-image]][license-url] [![snyk-image]][snyk-url] +[![gh-workflow-image]][gh-workflow-url] [![npm-image]][npm-url] ![][typescript-image] [![license-image]][license-url] ## Introduction -Add sessions to your AdonisJS application. Cookie, Redis, and File drivers are included out of the box. +Use sessions in your AdonisJS applications with a unified API to persist session data across different data-stores. Has inbuilt support for **cookie**, **files**, and **redis** drivers. ## Official Documentation The documentation is available on the [AdonisJS website](https://docs.adonisjs.com/guides/sessions) @@ -19,7 +19,7 @@ We encourage you to read the [contribution guide](https://github.com/adonisjs/.g In order to ensure that the AdonisJS community is welcoming to all, please review and abide by the [Code of Conduct](https://github.com/adonisjs/.github/blob/main/docs/CODE_OF_CONDUCT.md). ## License -AdonisJS ally is open-sourced software licensed under the [MIT license](LICENSE.md). +AdonisJS session is open-sourced software licensed under the [MIT license](LICENSE.md). [gh-workflow-image]: https://img.shields.io/github/actions/workflow/status/adonisjs/session/test.yml?style=for-the-badge [gh-workflow-url]: https://github.com/adonisjs/session/actions/workflows/test.yml "Github action" @@ -31,6 +31,3 @@ AdonisJS ally is open-sourced software licensed under the [MIT license](LICENSE. [license-url]: LICENSE.md [license-image]: https://img.shields.io/github/license/adonisjs/session?style=for-the-badge - -[snyk-image]: https://img.shields.io/snyk/vulnerabilities/github/adonisjs/session?label=Snyk%20Vulnerabilities&style=for-the-badge -[snyk-url]: https://snyk.io/test/github/adonisjs/session?targetFile=package.json "snyk" From 413893629787cd927266f735c0286243db3d4963 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 1 Aug 2023 11:08:01 +0530 Subject: [PATCH 053/112] feat: handle validation errors --- index.ts | 2 +- package.json | 3 ++- src/session.ts | 19 +++++++++++++++++ src/session_middleware.ts | 15 ++++++++++++- tests/session.spec.ts | 44 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 80 insertions(+), 3 deletions(-) diff --git a/index.ts b/index.ts index eacea28..0b8c636 100644 --- a/index.ts +++ b/index.ts @@ -13,4 +13,4 @@ export * as errors from './src/errors.js' export { configure } from './configure.js' export { stubsRoot } from './stubs/main.js' export { defineConfig } from './src/define_config.js' -export { default as sessionDriversList } from './src/drivers_collection.js' +export { default as driversList } from './src/drivers_collection.js' diff --git a/package.json b/package.json index 9dce267..059d2db 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "quick:test": "node --enable-source-maps --loader=ts-node/esm bin/test.ts" }, "devDependencies": { - "@adonisjs/core": "^6.1.5-12", + "@adonisjs/core": "^6.1.5-14", "@adonisjs/eslint-config": "^1.1.8", "@adonisjs/prettier-config": "^1.1.8", "@adonisjs/redis": "^8.0.0-8", @@ -55,6 +55,7 @@ "@types/node": "^20.4.2", "@types/set-cookie-parser": "^2.4.3", "@types/supertest": "^2.0.12", + "@vinejs/vine": "^1.6.0", "c8": "^8.0.0", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", diff --git a/src/session.ts b/src/session.ts index c12afec..97e0468 100644 --- a/src/session.ts +++ b/src/session.ts @@ -21,6 +21,7 @@ import type { SessionDriverContract, } from './types/main.js' import debug from './debug.js' +import { HttpError } from '@adonisjs/core/types/http' /** * The session class exposes the API to read and write values to @@ -273,6 +274,24 @@ export class Session { return this.#getStore('write').clear() } + /** + * Flash validation error messages. Make sure the error + * is an instance of VineJS ValidationException + */ + flashValidationErrors(error: HttpError) { + const errorsBag = error.messages.reduce((result: Record, message: any) => { + if (result[message.field]) { + result[message.field].push(message.message) + } else { + result[message.field] = [message.message] + } + return result + }, {}) + + this.flashExcept(['_csrf', '_method']) + this.flash('errors', errorsBag) + } + /** * Add a key-value pair to flash messages */ diff --git a/src/session_middleware.ts b/src/session_middleware.ts index 68beddc..2085802 100644 --- a/src/session_middleware.ts +++ b/src/session_middleware.ts @@ -7,9 +7,9 @@ * file that was distributed with this source code. */ -import { HttpContext } from '@adonisjs/core/http' import { EmitterService } from '@adonisjs/core/types' import type { NextFn } from '@adonisjs/core/types/http' +import { ExceptionHandler, HttpContext } from '@adonisjs/core/http' import { Session } from './session.js' import type { SessionConfig } from './types/main.js' @@ -24,6 +24,19 @@ declare module '@adonisjs/core/http' { } } +/** + * Overwriting validation exception renderer + */ +const originalErrorHandler = ExceptionHandler.prototype.renderValidationErrorAsHTML +ExceptionHandler.macro('renderValidationErrorAsHTML', async function (error, ctx) { + if (ctx.session) { + ctx.session.flashValidationErrors(error) + ctx.response.redirect('back', true) + } else { + return originalErrorHandler(error, ctx) + } +}) + /** * Session middleware is used to initiate the session store * and commit its values during an HTTP request diff --git a/tests/session.spec.ts b/tests/session.spec.ts index b7d01b3..a90c374 100644 --- a/tests/session.spec.ts +++ b/tests/session.spec.ts @@ -13,7 +13,9 @@ import { cuid } from '@adonisjs/core/helpers' import setCookieParser from 'set-cookie-parser' import { Emitter } from '@adonisjs/core/events' import { EventsList } from '@adonisjs/core/types' +import { SimpleErrorReporter } from '@vinejs/vine' import { CookieClient } from '@adonisjs/core/http' +import { fieldContext } from '@vinejs/vine/factories' import { AppFactory } from '@adonisjs/core/factories/app' import { EncryptionFactory } from '@adonisjs/core/factories/encryption' import { HttpContextFactory, RequestFactory, ResponseFactory } from '@adonisjs/core/factories/http' @@ -890,4 +892,46 @@ test.group('Session | Flash', () => { await session.initiate(true) session.flash('username', 'foo') }).throws('Session store is in readonly mode and cannot be mutated') + + test('flash validation error messages', async ({ assert }) => { + let sessionId: string | undefined + + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() + + const driver = new CookieDriver(sessionConfig.cookie, ctx) + const session = new Session(sessionConfig, driver, emitter, ctx) + await session.initiate(false) + + const errorReporter = new SimpleErrorReporter() + errorReporter.report('Invalid username', 'alpha', fieldContext.create('username', ''), {}) + errorReporter.report( + 'Username is required', + 'required', + fieldContext.create('username', ''), + {} + ) + errorReporter.report('Invalid email', 'email', fieldContext.create('email', ''), {}) + + session.flashValidationErrors(errorReporter.createError()) + sessionId = session.sessionId + + await session.commit() + response.finish() + }) + + const { headers } = await supertest(server).get('/') + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) + + assert.deepEqual(cookieClient.decrypt(sessionId!, cookies[sessionId!].value), { + __flash__: { + errors: { + email: ['Invalid email'], + username: ['Invalid username', 'Username is required'], + }, + }, + }) + }) }) From 70c27efce5bd93bae1b96c8e744aa51148f37c5c Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 1 Aug 2023 11:08:58 +0530 Subject: [PATCH 054/112] refactor: make session middleware singleton --- providers/session_provider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/session_provider.ts b/providers/session_provider.ts index f72d013..b45167e 100644 --- a/providers/session_provider.ts +++ b/providers/session_provider.ts @@ -20,7 +20,7 @@ export default class SessionProvider { constructor(protected app: ApplicationService) {} register() { - this.app.container.bind(SessionMiddleware, async (resolver) => { + this.app.container.singleton(SessionMiddleware, async (resolver) => { const config = this.app.config.get('session', {}) const emitter = await resolver.make('emitter') return new SessionMiddleware(config, emitter) From e063ef41499a728b234ff8df15b70d09a6f0a6bc Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 1 Aug 2023 11:09:47 +0530 Subject: [PATCH 055/112] refactor: log when registering driver --- src/helpers.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/helpers.ts b/src/helpers.ts index 50d56ad..db11142 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -9,6 +9,7 @@ import type { ApplicationService } from '@adonisjs/core/types' +import debug from './debug.js' import sessionDriversList from './drivers_collection.js' import type { SessionDriversList } from './types/main.js' @@ -19,6 +20,8 @@ export async function registerSessionDriver( app: ApplicationService, driverInUse: keyof SessionDriversList ) { + debug('registering %s driver', driverInUse) + if (driverInUse === 'cookie') { const { CookieDriver } = await import('../src/drivers/cookie.js') sessionDriversList.extend('cookie', (config, ctx) => new CookieDriver(config.cookie, ctx)) From 1c88b37aa415c456e54d0fdd501d1a3a8796a614 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 1 Aug 2023 11:13:44 +0530 Subject: [PATCH 056/112] ci: rename test.yml to checks.yml --- .github/workflows/{test.yml => checks.yml} | 0 README.md | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename .github/workflows/{test.yml => checks.yml} (100%) diff --git a/.github/workflows/test.yml b/.github/workflows/checks.yml similarity index 100% rename from .github/workflows/test.yml rename to .github/workflows/checks.yml diff --git a/README.md b/README.md index 83636f3..15ba8ee 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,8 @@ In order to ensure that the AdonisJS community is welcoming to all, please revie ## License AdonisJS session is open-sourced software licensed under the [MIT license](LICENSE.md). -[gh-workflow-image]: https://img.shields.io/github/actions/workflow/status/adonisjs/session/test.yml?style=for-the-badge -[gh-workflow-url]: https://github.com/adonisjs/session/actions/workflows/test.yml "Github action" +[gh-workflow-image]: https://img.shields.io/github/actions/workflow/status/adonisjs/session/checks.yml?style=for-the-badge +[gh-workflow-url]: https://github.com/adonisjs/session/actions/workflows/checks.yml "Github action" [npm-image]: https://img.shields.io/npm/v/@adonisjs/session/latest.svg?style=for-the-badge&logo=npm [npm-url]: https://www.npmjs.com/package/@adonisjs/session/v/latest "npm" From 737052cee2d4e43132fb3475803bede320dd1b8d Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 1 Aug 2023 11:15:05 +0530 Subject: [PATCH 057/112] ci: skip redis tests on windows machine --- tests/concurrent_session.spec.ts | 6 +++++- tests/drivers/redis_driver.spec.ts | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/concurrent_session.spec.ts b/tests/concurrent_session.spec.ts index d032e8b..97c1ee5 100644 --- a/tests/concurrent_session.spec.ts +++ b/tests/concurrent_session.spec.ts @@ -302,7 +302,11 @@ test.group('Concurrency | file driver', () => { }).timeout(6000) }) -test.group('Concurrency | redis driver', () => { +test.group('Concurrency | redis driver', (group) => { + group.tap((t) => { + t.skip(!!process.env.NO_REDIS, 'Redis not available in windows env') + }) + test('concurrently read and read slowly', async ({ assert, cleanup }) => { let sessionId = cuid() cleanup(async () => { diff --git a/tests/drivers/redis_driver.spec.ts b/tests/drivers/redis_driver.spec.ts index d3f790c..4d447ca 100644 --- a/tests/drivers/redis_driver.spec.ts +++ b/tests/drivers/redis_driver.spec.ts @@ -31,6 +31,10 @@ declare module '@adonisjs/redis/types' { } test.group('Redis driver', (group) => { + group.tap((t) => { + t.skip(!!process.env.NO_REDIS, 'Redis not available in windows env') + }) + group.each.setup(() => { return async () => { await redis.del(sessionId) From ccbb2adaeb6a28e94495a387ccf8fe97d8b7725b Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 1 Aug 2023 11:20:27 +0530 Subject: [PATCH 058/112] chore(release): 7.0.0-3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 059d2db..5631376 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@adonisjs/session", "description": "Session provider for AdonisJS", - "version": "7.0.0-2", + "version": "7.0.0-3", "engines": { "node": ">=18.16.0" }, From 98ad1229b1340ddef587085edd48cfb36d541b1f Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 2 Aug 2023 12:19:38 +0530 Subject: [PATCH 059/112] refactor: simplify config stub --- stubs/config.stub | 118 +++++++++++----------------------------------- 1 file changed, 28 insertions(+), 90 deletions(-) diff --git a/stubs/config.stub b/stubs/config.stub index 0c5ceb6..3a7dfcb 100644 --- a/stubs/config.stub +++ b/stubs/config.stub @@ -7,107 +7,45 @@ import app from '@adonisjs/core/services/app' import { defineConfig } from '@adonisjs/session' export default defineConfig({ - /* - |-------------------------------------------------------------------------- - | Enable/Disable sessions - |-------------------------------------------------------------------------- - | - | Setting the following property to "false" will disable the session for the - | entire application - | - */ enabled: true, - - /* - |-------------------------------------------------------------------------- - | Driver - |-------------------------------------------------------------------------- - | - | The session driver to use. You can choose between one of the following - | drivers. - | - | - cookie (Uses signed cookies to store session values) - | - file (Uses filesystem to store session values) - | - redis (Uses redis. Make sure to install "@adonisjs/redis" as well) - | - | Note: Switching drivers will make existing sessions invalid. - | - */ - driver: env.get('SESSION_DRIVER'), - - /* - |-------------------------------------------------------------------------- - | Cookie name - |-------------------------------------------------------------------------- - | - | The name of the cookie that will hold the session id. - | - */ cookieName: 'adonis-session', - /* - |-------------------------------------------------------------------------- - | Clear session when browser closes - |-------------------------------------------------------------------------- - | - | Whether or not you want to destroy the session when browser closes. Setting - | this value to "true" will ignore the "age". - | - */ + /** + * When set to true, the session id cookie will be deleted + * once the user closes the browser. + */ clearWithBrowser: false, - /* - |-------------------------------------------------------------------------- - | Session age - |-------------------------------------------------------------------------- - | - | The duration for which session stays active after no activity. A new HTTP - | request to the server is considered as activity. - | - | The value can be a number in milliseconds or a string that must be valid - | as per https://npmjs.org/package/ms package. - | - | Example: "2 days", "2.5 hrs", "1y", "5s" and so on. - | - */ + /** + * Define how long to keep the session data alive without + * any activity. + */ age: '2h', - /* - |-------------------------------------------------------------------------- - | Cookie values - |-------------------------------------------------------------------------- - | - | The cookie settings are used to setup the session id cookie and also the - | driver will use the same values. - | - */ + /** + * The driver to use. Make sure to validate the environment + * variable in order to infer the driver name without any + * errors. + */ + driver: env.get('SESSION_DRIVER'), + cookie: { path: '/', httpOnly: true, sameSite: false, }, - /* - |-------------------------------------------------------------------------- - | Configuration for the file driver - |-------------------------------------------------------------------------- - | - | The file driver needs absolute path to the directory in which sessions - | must be stored. - | - */ - file: { - location: app.tmpPath('sessions'), - }, - - /* - |-------------------------------------------------------------------------- - | Redis driver - |-------------------------------------------------------------------------- - | - | The redis connection you want session driver to use. The same connection - | must be defined inside "config/redis.ts" file as well. - | - */ - redisConnection: 'local', + /** + * Settings for the file driver + */ + // file: { + // location: app.tmpPath('sessions'), + // }, + + /** + * Settings for the redis driver + */ + // redis: { + // connection: 'main' + // }, }) From e59facdae6df31ae30c9f8c90baf7b9bc7756c34 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 2 Aug 2023 12:20:35 +0530 Subject: [PATCH 060/112] chore: update dependencies --- package.json | 6 +++--- src/drivers/file.ts | 2 +- src/drivers/redis.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 5631376..ef63607 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "quick:test": "node --enable-source-maps --loader=ts-node/esm bin/test.ts" }, "devDependencies": { - "@adonisjs/core": "^6.1.5-14", + "@adonisjs/core": "^6.1.5-15", "@adonisjs/eslint-config": "^1.1.8", "@adonisjs/prettier-config": "^1.1.8", "@adonisjs/redis": "^8.0.0-8", @@ -74,8 +74,8 @@ "@poppinss/utils": "^6.5.0-5" }, "peerDependencies": { - "@adonisjs/core": "^6.1.5-12", - "@adonisjs/redis": "^8.0.0-7" + "@adonisjs/core": "^6.1.5-15", + "@adonisjs/redis": "^8.0.0-8" }, "peerDependenciesMeta": { "@adonisjs/redis": { diff --git a/src/drivers/file.ts b/src/drivers/file.ts index 787851b..1def419 100644 --- a/src/drivers/file.ts +++ b/src/drivers/file.ts @@ -10,7 +10,7 @@ import type { Stats } from 'node:fs' import { dirname, join } from 'node:path' import string from '@poppinss/utils/string' -import { MessageBuilder } from '@poppinss/utils' +import { MessageBuilder } from '@adonisjs/core/helpers' import { access, mkdir, readFile, rm, writeFile, utimes, stat } from 'node:fs/promises' import type { FileDriverConfig, SessionData, SessionDriverContract } from '../types/main.js' diff --git a/src/drivers/redis.ts b/src/drivers/redis.ts index b49ae6e..f6d02a6 100644 --- a/src/drivers/redis.ts +++ b/src/drivers/redis.ts @@ -8,7 +8,7 @@ */ import string from '@poppinss/utils/string' -import { MessageBuilder } from '@poppinss/utils' +import { MessageBuilder } from '@adonisjs/core/helpers' import type { RedisService } from '@adonisjs/redis/types' import type { SessionDriverContract, RedisDriverConfig, SessionData } from '../types/main.js' From 1f508f1b596fd8117b669b937ca69a1d8b15631d Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 2 Aug 2023 12:29:21 +0530 Subject: [PATCH 061/112] test: add tests for the drivers collection class --- src/drivers_collection.ts | 4 ++-- src/helpers.ts | 7 +++++++ tests/drivers_collection.spec.ts | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 tests/drivers_collection.spec.ts diff --git a/src/drivers_collection.ts b/src/drivers_collection.ts index 2387e72..bfbc1d4 100644 --- a/src/drivers_collection.ts +++ b/src/drivers_collection.ts @@ -1,5 +1,5 @@ /* - * @adonisjs/redis + * @adonisjs/session * * (c) AdonisJS * @@ -44,7 +44,7 @@ class SessionDriversCollection { const driverFactory = this.list[name] if (!driverFactory) { throw new RuntimeException( - `Unknown redis driver "${String(name)}". Make sure the driver is registered` + `Unknown session driver "${String(name)}". Make sure the driver is registered` ) } diff --git a/src/helpers.ts b/src/helpers.ts index db11142..80846ae 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -20,6 +20,13 @@ export async function registerSessionDriver( app: ApplicationService, driverInUse: keyof SessionDriversList ) { + /** + * Noop when the driver is already registered + */ + if (sessionDriversList.list[driverInUse]) { + return + } + debug('registering %s driver', driverInUse) if (driverInUse === 'cookie') { diff --git a/tests/drivers_collection.spec.ts b/tests/drivers_collection.spec.ts new file mode 100644 index 0000000..89d3861 --- /dev/null +++ b/tests/drivers_collection.spec.ts @@ -0,0 +1,32 @@ +/* + * @adonisjs/session + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { ApplicationService } from '@adonisjs/core/types' +import { AppFactory } from '@adonisjs/core/factories/app' + +import { MemoryDriver } from '../src/drivers/memory.js' +import { registerSessionDriver } from '../src/helpers.js' +import sessionDriversList from '../src/drivers_collection.js' + +test.group('Drivers collection', () => { + test('raise error when trying to access a non-existing driver', () => { + sessionDriversList.create('cookie', {} as any, {} as any) + }).throws('Unknown session driver "cookie". Make sure the driver is registered') + + test('create driver instance when exists', async ({ assert }) => { + const app = new AppFactory().create( + new URL('./', import.meta.url), + () => {} + ) as ApplicationService + + await registerSessionDriver(app, 'memory') + assert.instanceOf(sessionDriversList.create('memory', {} as any, {} as any), MemoryDriver) + }) +}) From 2f47e0a4dba194350fe3cc7b4f229add74bf56cb Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 2 Aug 2023 12:33:19 +0530 Subject: [PATCH 062/112] chore(release): 7.0.0-4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ef63607..426d8ae 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@adonisjs/session", "description": "Session provider for AdonisJS", - "version": "7.0.0-3", + "version": "7.0.0-4", "engines": { "node": ">=18.16.0" }, From 4322881d0dbe6a1ab6c773bfd270c1a720c2954d Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 21 Aug 2023 17:15:12 +0530 Subject: [PATCH 063/112] chore: update dependencies --- package.json | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 426d8ae..eb5b9f7 100644 --- a/package.json +++ b/package.json @@ -40,19 +40,18 @@ "quick:test": "node --enable-source-maps --loader=ts-node/esm bin/test.ts" }, "devDependencies": { - "@adonisjs/core": "^6.1.5-15", + "@adonisjs/core": "^6.1.5-18", "@adonisjs/eslint-config": "^1.1.8", "@adonisjs/prettier-config": "^1.1.8", - "@adonisjs/redis": "^8.0.0-8", + "@adonisjs/redis": "^8.0.0-9", "@adonisjs/tsconfig": "^1.1.8", - "@japa/api-client": "^2.0.0-0", "@japa/assert": "^2.0.0-1", "@japa/file-system": "^2.0.0-1", "@japa/plugin-adonisjs": "^2.0.0-1", "@japa/runner": "^3.0.0-6", "@japa/snapshot": "^2.0.0-1", - "@swc/core": "^1.3.70", - "@types/node": "^20.4.2", + "@swc/core": "^1.3.78", + "@types/node": "^20.5.1", "@types/set-cookie-parser": "^2.4.3", "@types/supertest": "^2.0.12", "@vinejs/vine": "^1.6.0", @@ -60,11 +59,11 @@ "copyfiles": "^2.4.1", "cross-env": "^7.0.3", "del-cli": "^5.0.0", - "eslint": "^8.45.0", + "eslint": "^8.47.0", "github-label-sync": "^2.3.1", "husky": "^8.0.3", "np": "^8.0.4", - "prettier": "^3.0.0", + "prettier": "^3.0.2", "set-cookie-parser": "^2.6.0", "supertest": "^6.3.3", "ts-node": "^10.9.1", @@ -74,8 +73,8 @@ "@poppinss/utils": "^6.5.0-5" }, "peerDependencies": { - "@adonisjs/core": "^6.1.5-15", - "@adonisjs/redis": "^8.0.0-8" + "@adonisjs/core": "^6.1.5-18", + "@adonisjs/redis": "^8.0.0-9" }, "peerDependenciesMeta": { "@adonisjs/redis": { From b13c120ec02f47afc0576fbf1bc7cda4a9125a19 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 21 Aug 2023 17:20:40 +0530 Subject: [PATCH 064/112] feat: use latest codemods --- configure.ts | 21 +++++++++++++++++++++ package.json | 1 + tests/configure.spec.ts | 11 +++++++++-- 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/configure.ts b/configure.ts index 6f2ef6d..05ad9d8 100644 --- a/configure.ts +++ b/configure.ts @@ -13,8 +13,29 @@ import type Configure from '@adonisjs/core/commands/configure' * Configures the package */ export async function configure(command: Configure) { + /** + * Publish config file + */ await command.publishStub('config.stub') + + /** + * Define environment variables + */ await command.defineEnvVariables({ SESSION_DRIVER: 'cookie' }) + + /** + * Define environment variables validations + */ + await command.defineEnvValidations({ + variables: { + SESSION_DRIVER: `Env.schema.enum(['cookie', 'redis', 'file', 'memory' as const])`, + }, + leadingComment: 'Variables for configuring session package', + }) + + /** + * Register provider + */ await command.updateRcFile((rcFile) => { rcFile.addProvider('@adonisjs/session/session_provider') }) diff --git a/package.json b/package.json index eb5b9f7..d50dd29 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "quick:test": "node --enable-source-maps --loader=ts-node/esm bin/test.ts" }, "devDependencies": { + "@adonisjs/assembler": "^6.1.3-18", "@adonisjs/core": "^6.1.5-18", "@adonisjs/eslint-config": "^1.1.8", "@adonisjs/prettier-config": "^1.1.8", diff --git a/tests/configure.spec.ts b/tests/configure.spec.ts index c86f3d7..1cd885d 100644 --- a/tests/configure.spec.ts +++ b/tests/configure.spec.ts @@ -35,6 +35,9 @@ test.group('Configure', (group) => { }) await fs.create('.env', '') + await fs.createJson('tsconfig.json', {}) + await fs.create('start/env.ts', `export default Env.create(new URL('./'), {})`) + await fs.create('adonisrc.ts', `export default defineConfig({}) {}`) const app = ignitor.createApp('web') await app.init() @@ -45,9 +48,13 @@ test.group('Configure', (group) => { await command.exec() await assert.fileExists('config/session.ts') - await assert.fileExists('.adonisrc.json') - await assert.fileContains('.adonisrc.json', '@adonisjs/session/session_provider') + await assert.fileExists('adonisrc.ts') + await assert.fileContains('adonisrc.ts', '@adonisjs/session/session_provider') await assert.fileContains('config/session.ts', 'defineConfig') await assert.fileContains('.env', 'SESSION_DRIVER=cookie') + await assert.fileContains( + 'start/env.ts', + `SESSION_DRIVER: Env.schema.enum(['cookie', 'redis', 'file', 'memory' as const])` + ) }) }) From d6f7b47cf380c863608d0b797c0add03c0b2d87c Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 21 Aug 2023 17:23:45 +0530 Subject: [PATCH 065/112] chore(release): 7.0.0-5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d50dd29..03bfc4e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@adonisjs/session", "description": "Session provider for AdonisJS", - "version": "7.0.0-4", + "version": "7.0.0-5", "engines": { "node": ">=18.16.0" }, From 88ae6512d415b106b3d761d13ac262aedb9e5df8 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 22 Aug 2023 11:39:28 +0530 Subject: [PATCH 066/112] chore: update dependencies --- configure.ts | 8 +++++--- package.json | 8 ++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/configure.ts b/configure.ts index 05ad9d8..9723761 100644 --- a/configure.ts +++ b/configure.ts @@ -18,15 +18,17 @@ export async function configure(command: Configure) { */ await command.publishStub('config.stub') + const codemods = await command.createCodemods() + /** * Define environment variables */ - await command.defineEnvVariables({ SESSION_DRIVER: 'cookie' }) + await codemods.defineEnvVariables({ SESSION_DRIVER: 'cookie' }) /** * Define environment variables validations */ - await command.defineEnvValidations({ + await codemods.defineEnvValidations({ variables: { SESSION_DRIVER: `Env.schema.enum(['cookie', 'redis', 'file', 'memory' as const])`, }, @@ -36,7 +38,7 @@ export async function configure(command: Configure) { /** * Register provider */ - await command.updateRcFile((rcFile) => { + await codemods.updateRcFile((rcFile) => { rcFile.addProvider('@adonisjs/session/session_provider') }) } diff --git a/package.json b/package.json index 03bfc4e..b75e3d3 100644 --- a/package.json +++ b/package.json @@ -41,10 +41,10 @@ }, "devDependencies": { "@adonisjs/assembler": "^6.1.3-18", - "@adonisjs/core": "^6.1.5-18", + "@adonisjs/core": "^6.1.5-19", "@adonisjs/eslint-config": "^1.1.8", "@adonisjs/prettier-config": "^1.1.8", - "@adonisjs/redis": "^8.0.0-9", + "@adonisjs/redis": "^8.0.0-10", "@adonisjs/tsconfig": "^1.1.8", "@japa/assert": "^2.0.0-1", "@japa/file-system": "^2.0.0-1", @@ -74,8 +74,8 @@ "@poppinss/utils": "^6.5.0-5" }, "peerDependencies": { - "@adonisjs/core": "^6.1.5-18", - "@adonisjs/redis": "^8.0.0-9" + "@adonisjs/core": "^6.1.5-19", + "@adonisjs/redis": "^8.0.0-10" }, "peerDependenciesMeta": { "@adonisjs/redis": { From 6a97be931ed87be2ab625813c59b576c48d26753 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 22 Aug 2023 11:43:27 +0530 Subject: [PATCH 067/112] chore(release): 7.0.0-6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b75e3d3..3231431 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@adonisjs/session", "description": "Session provider for AdonisJS", - "version": "7.0.0-5", + "version": "7.0.0-6", "engines": { "node": ">=18.16.0" }, From ac9c1dfdab7d7b9f7ab5577f0ad53f8a1b08cdc3 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 22 Aug 2023 16:42:49 +0530 Subject: [PATCH 068/112] ci: increase test timeout for ci --- tests/configure.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/configure.spec.ts b/tests/configure.spec.ts index 1cd885d..7a58b51 100644 --- a/tests/configure.spec.ts +++ b/tests/configure.spec.ts @@ -56,5 +56,5 @@ test.group('Configure', (group) => { 'start/env.ts', `SESSION_DRIVER: Env.schema.enum(['cookie', 'redis', 'file', 'memory' as const])` ) - }) + }).timeout(6000) }) From e9e90064c09871c54c20c192bf7f1fc77bbd6adb Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 22 Aug 2023 20:25:50 +0530 Subject: [PATCH 069/112] feat: share session and flash messages with edge template --- package.json | 10 ++-- src/session.ts | 13 +++- src/store.ts | 135 ++++++++++++++++++++++-------------------- tests/session.spec.ts | 105 ++++++++++++++++++++++++++++++-- 4 files changed, 191 insertions(+), 72 deletions(-) diff --git a/package.json b/package.json index 3231431..7f883cf 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ }, "devDependencies": { "@adonisjs/assembler": "^6.1.3-18", - "@adonisjs/core": "^6.1.5-19", + "@adonisjs/core": "^6.1.5-21", "@adonisjs/eslint-config": "^1.1.8", "@adonisjs/prettier-config": "^1.1.8", "@adonisjs/redis": "^8.0.0-10", @@ -60,6 +60,7 @@ "copyfiles": "^2.4.1", "cross-env": "^7.0.3", "del-cli": "^5.0.0", + "edge.js": "^6.0.0-8", "eslint": "^8.47.0", "github-label-sync": "^2.3.1", "husky": "^8.0.3", @@ -74,14 +75,15 @@ "@poppinss/utils": "^6.5.0-5" }, "peerDependencies": { - "@adonisjs/core": "^6.1.5-19", - "@adonisjs/redis": "^8.0.0-10" + "@adonisjs/core": "^6.1.5-21", + "@adonisjs/redis": "^8.0.0-10", + "edge.js": "^6.0.0-8" }, "peerDependenciesMeta": { "@adonisjs/redis": { "optional": true }, - "@japa/api-client": { + "edge.js": { "optional": true } }, diff --git a/src/session.ts b/src/session.ts index 97e0468..cba9c09 100644 --- a/src/session.ts +++ b/src/session.ts @@ -12,7 +12,7 @@ import { cuid } from '@adonisjs/core/helpers' import { EmitterService } from '@adonisjs/core/types' import type { HttpContext } from '@adonisjs/core/http' -import { Store } from './store.js' +import { ReadOnlyStore, Store } from './store.js' import * as errors from './errors.js' import type { SessionData, @@ -197,6 +197,17 @@ export class Session { } } + /** + * Share session with the templates. We assume the view property + * is a reference to edge templates + */ + if ('view' in this.#ctx) { + this.#ctx.view.share({ + session: new ReadOnlyStore(this.#store.all()), + flashMessages: new ReadOnlyStore(this.flashMessages.all()), + }) + } + this.#emitter.emit('session:initiated', { session: this }) } diff --git a/src/store.ts b/src/store.ts index f7164db..defc183 100644 --- a/src/store.ts +++ b/src/store.ts @@ -13,30 +13,87 @@ import { RuntimeException } from '@poppinss/utils' import type { AllowedSessionValues, SessionData } from './types/main.js' /** - * Session store encapsulates the session data and offers a - * declarative API to mutate it. + * Readonly session store */ -export class Store { +export class ReadOnlyStore { /** * Underlying store values */ - #values: SessionData + protected values: SessionData /** - * A boolean to know if store has been - * modified + * Find if store is empty or not */ - #modified: boolean = false + get isEmpty() { + return !this.values || Object.keys(this.values).length === 0 + } constructor(values: SessionData | null) { - this.#values = values || {} + this.values = values || {} } /** - * Find if store is empty or not + * Get value for a given key */ - get isEmpty() { - return !this.#values || Object.keys(this.#values).length === 0 + get(key: string, defaultValue?: any): any { + return lodash.get(this.values, key, defaultValue) + } + + /** + * A boolean to know if value exists. Extra guards to check + * arrays for it's length as well. + */ + has(key: string, checkForArraysLength: boolean = true): boolean { + const value = this.get(key) + if (!Array.isArray(value)) { + return !!value + } + + return checkForArraysLength ? value.length > 0 : !!value + } + + /** + * Get all values + */ + all(): any { + return this.values + } + + /** + * Returns object representation of values + */ + toObject() { + return this.all() + } + + /** + * Returns the store values + */ + toJSON(): any { + return this.all() + } + + /** + * Returns string representation of the store + */ + toString() { + return JSON.stringify(this.all()) + } +} + +/** + * Session store encapsulates the session data and offers a + * declarative API to mutate it. + */ +export class Store extends ReadOnlyStore { + /** + * A boolean to know if store has been + * modified + */ + #modified: boolean = false + + constructor(values: SessionData | null) { + super(values) } /** @@ -51,7 +108,7 @@ export class Store { */ set(key: string, value: AllowedSessionValues): void { this.#modified = true - lodash.set(this.#values, key, value) + lodash.set(this.values, key, value) } /** @@ -59,7 +116,7 @@ export class Store { */ unset(key: string): void { this.#modified = true - lodash.unset(this.#values, key) + lodash.unset(this.values, key) } /** @@ -104,7 +161,7 @@ export class Store { */ update(values: { [key: string]: any }): void { this.#modified = true - this.#values = values + this.values = values } /** @@ -112,7 +169,7 @@ export class Store { */ merge(values: { [key: string]: any }): any { this.#modified = true - lodash.merge(this.#values, values) + lodash.merge(this.values, values) } /** @@ -121,52 +178,4 @@ export class Store { clear(): void { this.update({}) } - - /** - * Get value for a given key - */ - get(key: string, defaultValue?: any): any { - return lodash.get(this.#values, key, defaultValue) - } - - /** - * A boolean to know if value exists. Extra guards to check - * arrays for it's length as well. - */ - has(key: string, checkForArraysLength: boolean = true): boolean { - const value = this.get(key) - if (!Array.isArray(value)) { - return !!value - } - - return checkForArraysLength ? value.length > 0 : !!value - } - - /** - * Get all values - */ - all(): any { - return this.#values - } - - /** - * Returns object representation of values - */ - toObject() { - return this.all() - } - - /** - * Returns the store values - */ - toJSON(): any { - return this.all() - } - - /** - * Returns string representation of the store - */ - toString() { - return JSON.stringify(this.all()) - } } diff --git a/tests/session.spec.ts b/tests/session.spec.ts index a90c374..2b031c2 100644 --- a/tests/session.spec.ts +++ b/tests/session.spec.ts @@ -7,25 +7,32 @@ * file that was distributed with this source code. */ +import edge from 'edge.js' import supertest from 'supertest' import { test } from '@japa/runner' import { cuid } from '@adonisjs/core/helpers' import setCookieParser from 'set-cookie-parser' import { Emitter } from '@adonisjs/core/events' -import { EventsList } from '@adonisjs/core/types' +import { ApplicationService, EventsList } from '@adonisjs/core/types' import { SimpleErrorReporter } from '@vinejs/vine' import { CookieClient } from '@adonisjs/core/http' import { fieldContext } from '@vinejs/vine/factories' import { AppFactory } from '@adonisjs/core/factories/app' import { EncryptionFactory } from '@adonisjs/core/factories/encryption' -import { HttpContextFactory, RequestFactory, ResponseFactory } from '@adonisjs/core/factories/http' +import EdgeServiceProvider from '@adonisjs/core/providers/edge_provider' +import { + RouterFactory, + RequestFactory, + ResponseFactory, + HttpContextFactory, +} from '@adonisjs/core/factories/http' import { Session } from '../src/session.js' -import type { SessionConfig } from '../src/types/main.js' import { httpServer } from '../test_helpers/index.js' import { CookieDriver } from '../src/drivers/cookie.js' +import type { SessionConfig } from '../src/types/main.js' -const app = new AppFactory().create(new URL('./', import.meta.url), () => {}) +const app = new AppFactory().create(new URL('./', import.meta.url), () => {}) as ApplicationService const emitter = new Emitter(app) const encryption = new EncryptionFactory().create() const cookieClient = new CookieClient(encryption) @@ -376,6 +383,48 @@ test.group('Session', () => { session.put('username', 'foo') }).throws('Session store is in readonly mode and cannot be mutated') + + test('share session data with templates', async ({ assert }) => { + let sessionId = cuid() + + const router = new RouterFactory().create() + await app.init() + + app.container.singleton('router', () => router) + await new EdgeServiceProvider(app).boot() + + edge.registerTemplate('welcome', { + template: `The user age is {{ session.get('age') }}`, + }) + + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() + + const driver = new CookieDriver(sessionConfig.cookie, ctx) + const session = new Session(sessionConfig, driver, emitter, ctx) + await session.initiate(false) + + try { + response.send(await ctx.view.render('welcome')) + } catch (error) { + console.log(error) + } + + await session.commit() + response.finish() + }) + + const sessionIdCookie = `adonis_session=${cookieClient.sign('adonis_session', sessionId)}` + const sessionStoreCookie = `${sessionId}=${cookieClient.encrypt(sessionId, { age: 22 })}` + + const { text } = await supertest(server) + .get('/') + .set('cookie', `${sessionIdCookie}; ${sessionStoreCookie}`) + + assert.equal(text, 'The user age is 22') + }) }) test.group('Session | Regenerate', () => { @@ -764,6 +813,54 @@ test.group('Session | Flash', () => { assert.equal(newCookies[sessionId!].maxAge, -1) }) + test('access flash messages inside templates', async ({ assert }) => { + let sessionId: string | undefined + + const router = new RouterFactory().create() + await app.init() + + app.container.singleton('router', () => router) + await new EdgeServiceProvider(app).boot() + + edge.registerTemplate('flash_messages', { + template: `{{ flashMessages.get('status') }}`, + }) + + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() + + const driver = new CookieDriver(sessionConfig.cookie, ctx) + const session = new Session(sessionConfig, driver, emitter, ctx) + await session.initiate(false) + sessionId = session.sessionId + + if (request.url() === '/prg') { + response.send(await ctx.view.render('flash_messages')) + await session.commit() + response.finish() + } else { + session.flash({ status: 'Task created successfully' }) + await session.commit() + } + + response.finish() + }) + + const { headers } = await supertest(server).get('/') + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) + + const { text } = await supertest(server) + .get('/prg') + .set( + 'Cookie', + `adonis_session=${cookies.adonis_session.value}; ${sessionId}=${cookies[sessionId!].value}` + ) + + assert.equal(text, 'Task created successfully') + }) + test('reflash flash messages', async ({ assert }) => { let sessionId: string | undefined From 610ee1aad21f1ea00c089ab90e52ef24c65e9fd9 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 22 Aug 2023 22:06:47 +0530 Subject: [PATCH 070/112] feat: add edge tags --- providers/session_provider.ts | 29 ++++ src/edge_plugin_adonisjs_session.ts | 135 +++++++++++++++++ src/session.ts | 3 + tests/session.spec.ts | 224 +++++++++++++++++++++------- 4 files changed, 336 insertions(+), 55 deletions(-) create mode 100644 src/edge_plugin_adonisjs_session.ts diff --git a/providers/session_provider.ts b/providers/session_provider.ts index b45167e..70a6007 100644 --- a/providers/session_provider.ts +++ b/providers/session_provider.ts @@ -7,8 +7,10 @@ * file that was distributed with this source code. */ +import type { Edge } from 'edge.js' import type { ApplicationService } from '@adonisjs/core/types' +import debug from '../src/debug.js' import { registerSessionDriver } from '../src/helpers.js' import SessionMiddleware from '../src/session_middleware.js' @@ -19,6 +21,22 @@ import SessionMiddleware from '../src/session_middleware.js' export default class SessionProvider { constructor(protected app: ApplicationService) {} + /** + * Returns edge when it's installed + */ + protected async getEdge(): Promise { + try { + const { default: edge } = await import('edge.js') + debug('Detected edge.js package. Adding session primitives to it') + return edge + } catch { + return null + } + } + + /** + * Registering muddleware + */ register() { this.app.container.singleton(SessionMiddleware, async (resolver) => { const config = this.app.config.get('session', {}) @@ -27,10 +45,21 @@ export default class SessionProvider { }) } + /** + * Registering the active driver when middleware is used + * + + * Adding edge tags (if edge is installed) + */ async boot() { this.app.container.resolving(SessionMiddleware, async () => { const config = this.app.config.get('session') await registerSessionDriver(this.app, config.driver) }) + + const edge = await this.getEdge() + if (edge) { + const { edgePluginAdonisJSSession } = await import('../src/edge_plugin_adonisjs_session.js') + edge.use(edgePluginAdonisJSSession) + } } } diff --git a/src/edge_plugin_adonisjs_session.ts b/src/edge_plugin_adonisjs_session.ts new file mode 100644 index 0000000..5d84c22 --- /dev/null +++ b/src/edge_plugin_adonisjs_session.ts @@ -0,0 +1,135 @@ +/* + * @adonisjs/session + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import type { PluginFn } from 'edge.js/types' +import debug from './debug.js' + +/** + * The edge plugin for AdonisJS Session adds tags to read + * flash messages + */ +export const edgePluginAdonisJSSession: PluginFn = (edge) => { + debug('registering session tags with edge') + + edge.registerTag({ + tagName: 'flashMessage', + seekable: true, + block: true, + compile(parser, buffer, token) { + const expression = parser.utils.transformAst( + parser.utils.generateAST(token.properties.jsArg, token.loc, token.filename), + token.filename, + parser + ) + + const key = parser.utils.stringify(expression) + + /** + * Write an if statement + */ + buffer.writeStatement( + `if (state.flashMessages.has(${key})) {`, + token.filename, + token.loc.start.line + ) + + /** + * Define a local variable + */ + buffer.writeExpression( + `let message = state.flashMessages.get(${key})`, + token.filename, + token.loc.start.line + ) + + /** + * Create a local variables scope and tell the parser about + * the existence of the "message" variable + */ + parser.stack.defineScope() + parser.stack.defineVariable('message') + + /** + * Process component children using the parser + */ + token.children.forEach((child) => { + parser.processToken(child, buffer) + }) + + /** + * Clear the scope of the local variables before we + * close the if statement + */ + parser.stack.clearScope() + + /** + * Close if statement + */ + buffer.writeStatement(`}`, token.filename, token.loc.start.line) + }, + }) + + edge.registerTag({ + tagName: 'error', + seekable: true, + block: true, + compile(parser, buffer, token) { + const expression = parser.utils.transformAst( + parser.utils.generateAST(token.properties.jsArg, token.loc, token.filename), + token.filename, + parser + ) + + const key = parser.utils.stringify(expression) + + /** + * Write an if statement + */ + buffer.writeStatement( + `if (!!state.flashMessages.get('errors')[${key}]) {`, + token.filename, + token.loc.start.line + ) + + /** + * Define a local variable + */ + buffer.writeExpression( + `let messages = state.flashMessages.get('errors')[${key}]`, + token.filename, + token.loc.start.line + ) + + /** + * Create a local variables scope and tell the parser about + * the existence of the "messages" variable + */ + parser.stack.defineScope() + parser.stack.defineVariable('messages') + + /** + * Process component children using the parser + */ + token.children.forEach((child) => { + parser.processToken(child, buffer) + }) + + /** + * Clear the scope of the local variables before we + * close the if statement + */ + parser.stack.clearScope() + + /** + * Close if statement + */ + buffer.writeStatement(`}`, token.filename, token.loc.start.line) + }, + }) +} diff --git a/src/session.ts b/src/session.ts index cba9c09..bcce1bc 100644 --- a/src/session.ts +++ b/src/session.ts @@ -205,6 +205,9 @@ export class Session { this.#ctx.view.share({ session: new ReadOnlyStore(this.#store.all()), flashMessages: new ReadOnlyStore(this.flashMessages.all()), + old: function (key: string, defaultValue?: any) { + return this.flashMessages.get(key, defaultValue) + }, }) } diff --git a/tests/session.spec.ts b/tests/session.spec.ts index 2b031c2..94f3738 100644 --- a/tests/session.spec.ts +++ b/tests/session.spec.ts @@ -31,6 +31,7 @@ import { Session } from '../src/session.js' import { httpServer } from '../test_helpers/index.js' import { CookieDriver } from '../src/drivers/cookie.js' import type { SessionConfig } from '../src/types/main.js' +import SessionProvider from '../providers/session_provider.js' const app = new AppFactory().create(new URL('./', import.meta.url), () => {}) as ApplicationService const emitter = new Emitter(app) @@ -45,7 +46,17 @@ const sessionConfig: SessionConfig = { cookie: {}, } -test.group('Session', () => { +test.group('Session', (group) => { + group.setup(async () => { + const router = new RouterFactory().create() + await app.init() + + app.container.singleton('router', () => router) + + await new EdgeServiceProvider(app).boot() + await new SessionProvider(app).boot() + }) + test('do not define session id cookie when not initiated', async ({ assert }) => { const server = httpServer.create(async (req, res) => { const request = new RequestFactory().merge({ req, res, encryption }).create() @@ -387,12 +398,6 @@ test.group('Session', () => { test('share session data with templates', async ({ assert }) => { let sessionId = cuid() - const router = new RouterFactory().create() - await app.init() - - app.container.singleton('router', () => router) - await new EdgeServiceProvider(app).boot() - edge.registerTemplate('welcome', { template: `The user age is {{ session.get('age') }}`, }) @@ -813,54 +818,6 @@ test.group('Session | Flash', () => { assert.equal(newCookies[sessionId!].maxAge, -1) }) - test('access flash messages inside templates', async ({ assert }) => { - let sessionId: string | undefined - - const router = new RouterFactory().create() - await app.init() - - app.container.singleton('router', () => router) - await new EdgeServiceProvider(app).boot() - - edge.registerTemplate('flash_messages', { - template: `{{ flashMessages.get('status') }}`, - }) - - const server = httpServer.create(async (req, res) => { - const request = new RequestFactory().merge({ req, res, encryption }).create() - const response = new ResponseFactory().merge({ req, res, encryption }).create() - const ctx = new HttpContextFactory().merge({ request, response }).create() - - const driver = new CookieDriver(sessionConfig.cookie, ctx) - const session = new Session(sessionConfig, driver, emitter, ctx) - await session.initiate(false) - sessionId = session.sessionId - - if (request.url() === '/prg') { - response.send(await ctx.view.render('flash_messages')) - await session.commit() - response.finish() - } else { - session.flash({ status: 'Task created successfully' }) - await session.commit() - } - - response.finish() - }) - - const { headers } = await supertest(server).get('/') - const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) - - const { text } = await supertest(server) - .get('/prg') - .set( - 'Cookie', - `adonis_session=${cookies.adonis_session.value}; ${sessionId}=${cookies[sessionId!].value}` - ) - - assert.equal(text, 'Task created successfully') - }) - test('reflash flash messages', async ({ assert }) => { let sessionId: string | undefined @@ -1031,4 +988,161 @@ test.group('Session | Flash', () => { }, }) }) + + test('access flash messages inside templates', async ({ assert }) => { + let sessionId: string | undefined + + edge.registerTemplate('flash_messages', { + template: `{{ old('status') }}`, + }) + + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() + + const driver = new CookieDriver(sessionConfig.cookie, ctx) + const session = new Session(sessionConfig, driver, emitter, ctx) + await session.initiate(false) + sessionId = session.sessionId + + if (request.url() === '/prg') { + response.send(await ctx.view.render('flash_messages')) + await session.commit() + response.finish() + } else { + session.flash({ status: 'Task created successfully' }) + await session.commit() + } + + response.finish() + }) + + const { headers } = await supertest(server).get('/') + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) + + const { text } = await supertest(server) + .get('/prg') + .set( + 'Cookie', + `adonis_session=${cookies.adonis_session.value}; ${sessionId}=${cookies[sessionId!].value}` + ) + + assert.equal(text, 'Task created successfully') + }) + + test('access flash messages using the @flashMessage tag', async ({ assert }) => { + let sessionId: string | undefined + + edge.registerTemplate('flash_messages_via_tag', { + template: `@flashMessage('status') +

{{ message }}

+ @end + @flashMessage('success') +

{{ message }}

+ @else +

No success message

+ @end + `, + }) + + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() + + const driver = new CookieDriver(sessionConfig.cookie, ctx) + const session = new Session(sessionConfig, driver, emitter, ctx) + await session.initiate(false) + sessionId = session.sessionId + + if (request.url() === '/prg') { + response.send(await ctx.view.render('flash_messages_via_tag')) + await session.commit() + response.finish() + } else { + session.flash({ status: 'Task created successfully' }) + await session.commit() + } + + response.finish() + }) + + const { headers } = await supertest(server).get('/') + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) + + const { text } = await supertest(server) + .get('/prg') + .set( + 'Cookie', + `adonis_session=${cookies.adonis_session.value}; ${sessionId}=${cookies[sessionId!].value}` + ) + + assert.deepEqual( + text.split('\n').map((line) => line.trim()), + ['

Task created successfully

', '

No success message

', ''] + ) + }) + + test('access flash error messages using the @error tag', async ({ assert }) => { + let sessionId: string | undefined + + edge.registerTemplate('flash_errors_messages', { + template: ` + @error('username') + @each(message in messages) +

{{ message }}

+ @end + @else +

No error message

+ @end + `, + }) + + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() + + const driver = new CookieDriver(sessionConfig.cookie, ctx) + const session = new Session(sessionConfig, driver, emitter, ctx) + await session.initiate(false) + sessionId = session.sessionId + + if (request.url() === '/prg') { + response.send(await ctx.view.render('flash_errors_messages')) + await session.commit() + response.finish() + } else { + const errorReporter = new SimpleErrorReporter() + errorReporter.report('Invalid username', 'alpha', fieldContext.create('username', ''), {}) + errorReporter.report( + 'Username is required', + 'required', + fieldContext.create('username', ''), + {} + ) + + session.flashValidationErrors(errorReporter.createError()) + await session.commit() + } + + response.finish() + }) + + const { headers } = await supertest(server).get('/') + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) + + const { text } = await supertest(server) + .get('/prg') + .set( + 'Cookie', + `adonis_session=${cookies.adonis_session.value}; ${sessionId}=${cookies[sessionId!].value}` + ) + + assert.deepEqual( + text.split('\n').map((line) => line.trim()), + ['', '

Invalid username

', '

Username is required

', ''] + ) + }) }) From 9c4230a4a61e1dac98b1ba44e616e04114946d8c Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 22 Aug 2023 22:21:00 +0530 Subject: [PATCH 071/112] chore: update dependencies --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 7f883cf..3c3f78c 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ }, "devDependencies": { "@adonisjs/assembler": "^6.1.3-18", - "@adonisjs/core": "^6.1.5-21", + "@adonisjs/core": "^6.1.5-22", "@adonisjs/eslint-config": "^1.1.8", "@adonisjs/prettier-config": "^1.1.8", "@adonisjs/redis": "^8.0.0-10", @@ -75,7 +75,7 @@ "@poppinss/utils": "^6.5.0-5" }, "peerDependencies": { - "@adonisjs/core": "^6.1.5-21", + "@adonisjs/core": "^6.1.5-22", "@adonisjs/redis": "^8.0.0-10", "edge.js": "^6.0.0-8" }, From 9546348ed8e704a7fdb7a26852aac73672529a34 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 22 Aug 2023 22:26:55 +0530 Subject: [PATCH 072/112] chore(release): 7.0.0-7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3c3f78c..1964f3d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@adonisjs/session", "description": "Session provider for AdonisJS", - "version": "7.0.0-6", + "version": "7.0.0-7", "engines": { "node": ">=18.16.0" }, From 23a51bf027423dbd6a5ec9d74a5d8eb429ba1e1c Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 22 Aug 2023 23:11:07 +0530 Subject: [PATCH 073/112] fix: stubs and codemods --- configure.ts | 2 +- stubs/config.stub | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/configure.ts b/configure.ts index 9723761..4daa94b 100644 --- a/configure.ts +++ b/configure.ts @@ -30,7 +30,7 @@ export async function configure(command: Configure) { */ await codemods.defineEnvValidations({ variables: { - SESSION_DRIVER: `Env.schema.enum(['cookie', 'redis', 'file', 'memory' as const])`, + SESSION_DRIVER: `Env.schema.enum(['cookie', 'redis', 'file', 'memory'] as const)`, }, leadingComment: 'Variables for configuring session package', }) diff --git a/stubs/config.stub b/stubs/config.stub index 3a7dfcb..7691e1c 100644 --- a/stubs/config.stub +++ b/stubs/config.stub @@ -1,9 +1,7 @@ --- to: {{ app.configPath('session.ts') }} --- - import env from '#start/env' -import app from '@adonisjs/core/services/app' import { defineConfig } from '@adonisjs/session' export default defineConfig({ From 10fc5eab16e480c4b6ccaab7784cca8735d7fc02 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 22 Aug 2023 23:34:34 +0530 Subject: [PATCH 074/112] feat: register middleware --- configure.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/configure.ts b/configure.ts index 4daa94b..69c01b9 100644 --- a/configure.ts +++ b/configure.ts @@ -35,6 +35,15 @@ export async function configure(command: Configure) { leadingComment: 'Variables for configuring session package', }) + /** + * Register middleware + */ + await codemods.registerMiddleware('router', [ + { + path: '@adonisjs/session/session_middleware', + }, + ]) + /** * Register provider */ From 721e7cc5d375c3c3a04ba1155e5829b7266209f0 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 23 Aug 2023 09:50:39 +0530 Subject: [PATCH 075/112] test: fix broken test --- tests/configure.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/configure.spec.ts b/tests/configure.spec.ts index 7a58b51..f0a4c4a 100644 --- a/tests/configure.spec.ts +++ b/tests/configure.spec.ts @@ -37,6 +37,7 @@ test.group('Configure', (group) => { await fs.create('.env', '') await fs.createJson('tsconfig.json', {}) await fs.create('start/env.ts', `export default Env.create(new URL('./'), {})`) + await fs.create('start/kernel.ts', `router.use([])`) await fs.create('adonisrc.ts', `export default defineConfig({}) {}`) const app = ignitor.createApp('web') @@ -54,7 +55,7 @@ test.group('Configure', (group) => { await assert.fileContains('.env', 'SESSION_DRIVER=cookie') await assert.fileContains( 'start/env.ts', - `SESSION_DRIVER: Env.schema.enum(['cookie', 'redis', 'file', 'memory' as const])` + `SESSION_DRIVER: Env.schema.enum(['cookie', 'redis', 'file', 'memory'] as const)` ) }).timeout(6000) }) From d47a0e7941f27332aa5dbcefe90890430652200d Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 23 Aug 2023 09:58:08 +0530 Subject: [PATCH 076/112] refactor: session.get use default value when original value is null --- src/store.ts | 7 ++++++- tests/session.spec.ts | 2 +- tests/store.spec.ts | 5 +++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/store.ts b/src/store.ts index defc183..660a332 100644 --- a/src/store.ts +++ b/src/store.ts @@ -36,7 +36,12 @@ export class ReadOnlyStore { * Get value for a given key */ get(key: string, defaultValue?: any): any { - return lodash.get(this.values, key, defaultValue) + const value = lodash.get(this.values, key) + if (defaultValue !== undefined && (value === null || value === undefined)) { + return defaultValue + } + + return value } /** diff --git a/tests/session.spec.ts b/tests/session.spec.ts index 94f3738..d3e1a93 100644 --- a/tests/session.spec.ts +++ b/tests/session.spec.ts @@ -13,11 +13,11 @@ import { test } from '@japa/runner' import { cuid } from '@adonisjs/core/helpers' import setCookieParser from 'set-cookie-parser' import { Emitter } from '@adonisjs/core/events' -import { ApplicationService, EventsList } from '@adonisjs/core/types' import { SimpleErrorReporter } from '@vinejs/vine' import { CookieClient } from '@adonisjs/core/http' import { fieldContext } from '@vinejs/vine/factories' import { AppFactory } from '@adonisjs/core/factories/app' +import { ApplicationService, EventsList } from '@adonisjs/core/types' import { EncryptionFactory } from '@adonisjs/core/factories/encryption' import EdgeServiceProvider from '@adonisjs/core/providers/edge_provider' import { diff --git a/tests/store.spec.ts b/tests/store.spec.ts index cdf7d2b..55e2ab6 100644 --- a/tests/store.spec.ts +++ b/tests/store.spec.ts @@ -18,6 +18,11 @@ test.group('Store', () => { assert.isFalse(store.hasBeenModified) }) + test('return default value when original value is null', ({ assert }) => { + const store = new Store({ title: null } as any) + assert.equal(store.get('title', ''), '') + }) + test('mutate values inside store', ({ assert }) => { const store = new Store({}) store.set('username', 'virk') From 489909bd69290cacc1cf3103a8450570a9fda396 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 23 Aug 2023 10:03:22 +0530 Subject: [PATCH 077/112] fix: handle case when there are no validation errors in flash messages --- src/edge_plugin_adonisjs_session.ts | 4 +-- tests/session.spec.ts | 47 ++++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/src/edge_plugin_adonisjs_session.ts b/src/edge_plugin_adonisjs_session.ts index 5d84c22..02d2f7a 100644 --- a/src/edge_plugin_adonisjs_session.ts +++ b/src/edge_plugin_adonisjs_session.ts @@ -92,7 +92,7 @@ export const edgePluginAdonisJSSession: PluginFn = (edge) => { * Write an if statement */ buffer.writeStatement( - `if (!!state.flashMessages.get('errors')[${key}]) {`, + `if (!!state.flashMessages.get('errors', {})[${key}]) {`, token.filename, token.loc.start.line ) @@ -101,7 +101,7 @@ export const edgePluginAdonisJSSession: PluginFn = (edge) => { * Define a local variable */ buffer.writeExpression( - `let messages = state.flashMessages.get('errors')[${key}]`, + `let messages = state.flashMessages.get('errors', {})[${key}]`, token.filename, token.loc.start.line ) diff --git a/tests/session.spec.ts b/tests/session.spec.ts index d3e1a93..4fe92e5 100644 --- a/tests/session.spec.ts +++ b/tests/session.spec.ts @@ -642,7 +642,17 @@ test.group('Session | Regenerate', () => { }) }) -test.group('Session | Flash', () => { +test.group('Session | Flash', (group) => { + group.setup(async () => { + const router = new RouterFactory().create() + await app.init() + + app.container.singleton('router', () => router) + + await new EdgeServiceProvider(app).boot() + await new SessionProvider(app).boot() + }) + test('flash data using the session store', async ({ assert }) => { let sessionId: string | undefined @@ -1084,6 +1094,41 @@ test.group('Session | Flash', () => { ) }) + test('use error tag when there are no error message', async ({ assert }) => { + edge.registerTemplate('flash_no_errors_messages', { + template: ` + @error('username') + @each(message in messages) +

{{ message }}

+ @end + @else +

No error message

+ @end + `, + }) + + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() + + const driver = new CookieDriver(sessionConfig.cookie, ctx) + const session = new Session(sessionConfig, driver, emitter, ctx) + await session.initiate(false) + + response.send(await ctx.view.render('flash_no_errors_messages')) + await session.commit() + response.finish() + }) + + const { text } = await supertest(server).get('/prg') + + assert.deepEqual( + text.split('\n').map((line) => line.trim()), + ['

No error message

', ''] + ) + }) + test('access flash error messages using the @error tag', async ({ assert }) => { let sessionId: string | undefined From ee56e1deca0ccd5752d45ddd36a716c7db36737b Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 23 Aug 2023 10:10:17 +0530 Subject: [PATCH 078/112] ci: increase test timeout for ci --- tests/configure.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/configure.spec.ts b/tests/configure.spec.ts index f0a4c4a..a796554 100644 --- a/tests/configure.spec.ts +++ b/tests/configure.spec.ts @@ -57,5 +57,5 @@ test.group('Configure', (group) => { 'start/env.ts', `SESSION_DRIVER: Env.schema.enum(['cookie', 'redis', 'file', 'memory'] as const)` ) - }).timeout(6000) + }).timeout(10000) }) From 52450a5093003e715109ddbb7a94bcece570f1c6 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 23 Aug 2023 14:55:36 +0530 Subject: [PATCH 079/112] refactor: fix module augmentation code --- package.json | 2 +- src/session_middleware.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 1964f3d..45a0123 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ }, "devDependencies": { "@adonisjs/assembler": "^6.1.3-18", - "@adonisjs/core": "^6.1.5-22", + "@adonisjs/core": "^6.1.5-24", "@adonisjs/eslint-config": "^1.1.8", "@adonisjs/prettier-config": "^1.1.8", "@adonisjs/redis": "^8.0.0-10", diff --git a/src/session_middleware.ts b/src/session_middleware.ts index 2085802..a822ee8 100644 --- a/src/session_middleware.ts +++ b/src/session_middleware.ts @@ -18,7 +18,7 @@ import sessionDriversList from './drivers_collection.js' /** * HttpContext augmentations */ -declare module '@adonisjs/core/http' { +declare module '@adonisjs/http-server' { interface HttpContext { session: Session } From b19c697945a46ae6b69837e6ec425bf752c11bb9 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 23 Aug 2023 15:36:46 +0530 Subject: [PATCH 080/112] chore(release): 7.0.0-8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 45a0123..b365ca7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@adonisjs/session", "description": "Session provider for AdonisJS", - "version": "7.0.0-7", + "version": "7.0.0-8", "engines": { "node": ">=18.16.0" }, From 6820d7fde20a5a1c4e1f26ce7023a297c64e20f8 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 5 Sep 2023 14:42:45 +0530 Subject: [PATCH 081/112] chore: export factories --- package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/package.json b/package.json index b365ca7..042bddd 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ ], "exports": { ".": "./build/index.js", + "./factories": "./build/factories/main.js", "./session_provider": "./build/providers/session_provider.js", "./session_middleware": "./build/src/session_middleware.js", "./client": "./build/src/client.js", @@ -46,7 +47,9 @@ "@adonisjs/prettier-config": "^1.1.8", "@adonisjs/redis": "^8.0.0-10", "@adonisjs/tsconfig": "^1.1.8", + "@japa/api-client": "^2.0.0-0", "@japa/assert": "^2.0.0-1", + "@japa/browser-client": "^2.0.0-3", "@japa/file-system": "^2.0.0-1", "@japa/plugin-adonisjs": "^2.0.0-1", "@japa/runner": "^3.0.0-6", @@ -65,6 +68,7 @@ "github-label-sync": "^2.3.1", "husky": "^8.0.3", "np": "^8.0.4", + "playwright": "^1.37.1", "prettier": "^3.0.2", "set-cookie-parser": "^2.6.0", "supertest": "^6.3.3", From 4b551853d5035ecf9eb05b952c9979ec285ad66f Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 5 Sep 2023 14:45:55 +0530 Subject: [PATCH 082/112] chore(release): 7.0.0-9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 042bddd..6d3010d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@adonisjs/session", "description": "Session provider for AdonisJS", - "version": "7.0.0-8", + "version": "7.0.0-9", "engines": { "node": ">=18.16.0" }, From 1fa1c92bbc30322c475c70c2ce00cd77df80c0c8 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 5 Sep 2023 15:01:13 +0530 Subject: [PATCH 083/112] chore: publish factories directory --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 6d3010d..eba65e6 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "build/src", "build/stubs", "build/providers", + "build/factories", "build/configure.d.ts", "build/configure.js", "build/index.d.ts", From db71224c5260a3809bd7e2aa518c3cdbb6f6f9b7 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 5 Sep 2023 15:03:57 +0530 Subject: [PATCH 084/112] chore(release): 7.0.0-10 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index eba65e6..eafda58 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@adonisjs/session", "description": "Session provider for AdonisJS", - "version": "7.0.0-9", + "version": "7.0.0-10", "engines": { "node": ">=18.16.0" }, From ae1cc30191cb138d08851ef367f210debe29d6d4 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 14 Sep 2023 11:17:00 +0530 Subject: [PATCH 085/112] chore: update dependencies --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index eafda58..67a25bb 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "@japa/runner": "^3.0.0-6", "@japa/snapshot": "^2.0.0-1", "@swc/core": "^1.3.78", - "@types/node": "^20.5.1", + "@types/node": "^20.6.0", "@types/set-cookie-parser": "^2.4.3", "@types/supertest": "^2.0.12", "@vinejs/vine": "^1.6.0", @@ -65,11 +65,11 @@ "cross-env": "^7.0.3", "del-cli": "^5.0.0", "edge.js": "^6.0.0-8", - "eslint": "^8.47.0", + "eslint": "^8.49.0", "github-label-sync": "^2.3.1", "husky": "^8.0.3", "np": "^8.0.4", - "playwright": "^1.37.1", + "playwright": "^1.38.0", "prettier": "^3.0.2", "set-cookie-parser": "^2.6.0", "supertest": "^6.3.3", From 339afa69b6cb1e7721b23021df4d318d11c150b8 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 14 Sep 2023 15:28:24 +0530 Subject: [PATCH 086/112] feat: add japa api client plugin --- package.json | 4 +- src/client.ts | 98 ++++++------ src/drivers_collection.ts | 5 +- src/plugins/japa/api_client.ts | 253 +++++++++++++++++++++++++++++++ src/types/main.ts | 2 +- test_helpers/index.ts | 51 +++++++ tests/plugins/api_client.spec.ts | 229 ++++++++++++++++++++++++++++ tests/session_client.spec.ts | 70 ++++----- 8 files changed, 620 insertions(+), 92 deletions(-) create mode 100644 src/plugins/japa/api_client.ts create mode 100644 tests/plugins/api_client.spec.ts diff --git a/package.json b/package.json index 67a25bb..174dc99 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "./factories": "./build/factories/main.js", "./session_provider": "./build/providers/session_provider.js", "./session_middleware": "./build/src/session_middleware.js", + "./plugin_edge": "./build/src/edge_plugin_adonisjs_session.js", "./client": "./build/src/client.js", "./types": "./build/src/types.js" }, @@ -50,7 +51,6 @@ "@adonisjs/tsconfig": "^1.1.8", "@japa/api-client": "^2.0.0-0", "@japa/assert": "^2.0.0-1", - "@japa/browser-client": "^2.0.0-3", "@japa/file-system": "^2.0.0-1", "@japa/plugin-adonisjs": "^2.0.0-1", "@japa/runner": "^3.0.0-6", @@ -66,10 +66,10 @@ "del-cli": "^5.0.0", "edge.js": "^6.0.0-8", "eslint": "^8.49.0", + "get-port": "^7.0.0", "github-label-sync": "^2.3.1", "husky": "^8.0.3", "np": "^8.0.4", - "playwright": "^1.38.0", "prettier": "^3.0.2", "set-cookie-parser": "^2.6.0", "supertest": "^6.3.3", diff --git a/src/client.ts b/src/client.ts index ae8abe0..4de56ce 100644 --- a/src/client.ts +++ b/src/client.ts @@ -8,19 +8,24 @@ */ import { cuid } from '@adonisjs/core/helpers' -import type { CookieClient } from '@adonisjs/core/http' +import debug from './debug.js' import { Store } from './store.js' -import type { SessionConfig, SessionData, SessionDriverContract } from './types/main.js' +import type { SessionData, SessionDriverContract } from './types/main.js' /** * Session client exposes the API to set session data as a client */ export class SessionClient { /** - * Session configuration + * Data store */ - #config: SessionConfig + #store = new Store({}) + + /** + * Flash messages store + */ + #flashMessagesStore = new Store({}) /** * The session driver to use for reading and writing session data @@ -28,74 +33,69 @@ export class SessionClient { #driver: SessionDriverContract /** - * Cookie client contract to sign and unsign cookies + * Session key for setting flash messages */ - #cookieClient: CookieClient + flashKey = '__flash__' /** * Session to use when no explicit session id is * defined */ - #sessionId = cuid() + sessionId = cuid() - /** - * Session key for setting flash messages - */ - flashKey = '__flash__' - - constructor(config: SessionConfig, driver: SessionDriverContract, cookieClient: CookieClient) { - this.#config = config + constructor(driver: SessionDriverContract) { this.#driver = driver - this.#cookieClient = cookieClient } /** - * Load session data from the driver + * Merge session data */ - async load( - cookies: Record, - sessionId?: string - ): Promise<{ sessionId: string; session: SessionData; flashMessages: SessionData }> { - const sessionIdCookie = cookies[this.#config.cookieName] - const sessId = sessionId || sessionIdCookie ? sessionIdCookie.value : this.#sessionId - - const contents = await this.#driver.read(sessId) - const store = new Store(contents) - const flashMessages = store.pull(this.flashKey, null) - - return { - sessionId: sessId, - session: store.all(), - flashMessages, - } + merge(values: SessionData) { + this.#store.merge(values) + return this } /** - * Commits the session data to the session store and returns - * the session id and cookie name for it to be accessible - * by the server + * Merge flash messages */ - async commit(values: SessionData | null, flashMessages: SessionData | null, sessionId?: string) { - const sessId = sessionId || this.#sessionId + flash(values: SessionData) { + this.#flashMessagesStore.merge(values) + return this + } - /** - * Persist session data to the store, alongside flash messages - */ - if (values || flashMessages) { - await this.#driver.write(sessId, Object.assign({ [this.flashKey]: flashMessages }, values)) + /** + * Commits data to the session store. + */ + async commit() { + if (!this.#flashMessagesStore.isEmpty) { + this.#store.set(this.flashKey, this.#flashMessagesStore.toJSON()) } - return { - sessionId: sessId, - signedSessionId: this.#cookieClient.sign(this.#config.cookieName, sessId)!, - cookieName: this.#config.cookieName, + debug('committing session data during api request') + if (!this.#store.isEmpty) { + this.#driver.write(this.sessionId, this.#store.toJSON()) } } /** - * Clear the session store + * Destroys the session data with the store + */ + async destroy(sessionId?: string) { + debug('destroying session data during api request') + this.#driver.destroy(sessionId || this.sessionId) + } + + /** + * Loads session data from the session store */ - async forget(sessionId?: string) { - await this.#driver.destroy(sessionId || this.#sessionId) + async load(sessionId?: string) { + const contents = await this.#driver.read(sessionId || this.sessionId) + const store = new Store(contents) + const flashMessages = store.pull(this.flashKey, {}) + + return { + values: store.all(), + flashMessages, + } } } diff --git a/src/drivers_collection.ts b/src/drivers_collection.ts index bfbc1d4..bdd627d 100644 --- a/src/drivers_collection.ts +++ b/src/drivers_collection.ts @@ -38,8 +38,7 @@ class SessionDriversCollection { */ create( name: Name, - config: Parameters[0], - ctx: HttpContext + ...args: Parameters ): ReturnType { const driverFactory = this.list[name] if (!driverFactory) { @@ -48,7 +47,7 @@ class SessionDriversCollection { ) } - return driverFactory(config as any, ctx) as ReturnType + return driverFactory(args[0], args[1]!) as ReturnType } } diff --git a/src/plugins/japa/api_client.ts b/src/plugins/japa/api_client.ts new file mode 100644 index 0000000..ec05a04 --- /dev/null +++ b/src/plugins/japa/api_client.ts @@ -0,0 +1,253 @@ +/* + * @adonisjs/session + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import lodash from '@poppinss/utils/lodash' +import type { PluginFn } from '@japa/runner/types' +import { RuntimeException } from '@poppinss/utils' +import type { ApplicationService } from '@adonisjs/core/types' +import { ApiClient, ApiRequest, ApiResponse } from '@japa/api-client' + +import { SessionClient } from '../../client.js' +import { registerSessionDriver } from '../../helpers.js' +import sessionDriversList from '../../drivers_collection.js' +import type { SessionConfig, SessionData } from '../../types/main.js' + +declare module '@japa/api-client' { + export interface ApiRequest { + sessionClient: SessionClient + + /** + * Make HTTP request along with the provided session data + */ + withSession(values: SessionData): this + + /** + * Make HTTP request along with the provided session flash + * messages. + */ + withFlashMessages(values: SessionData): this + } + + export interface ApiResponse { + sessionBag: { + values: SessionData + flashMessages: SessionData + } + + /** + * Get session data from the HTTP response + */ + session(key?: string): any + + /** + * Get flash messages from the HTTP response + */ + flashMessages(): SessionData + + /** + * Get flash messages for a specific key from the HTTP response + */ + flashMessage(key: string): SessionData + + /** + * Assert session key-value pair exists + */ + assertSession(key: string, value?: any): void + + /** + * Assert key is missing in session store + */ + assertSessionMissing(key: string): void + + /** + * Assert flash message key-value pair exists + */ + assertFlashMessage(key: string, value?: any): void + + /** + * Assert key is missing flash messages store + */ + assertFlashMissing(key: string): void + + /** + * Assert flash messages has validation errors for + * the given field + */ + assertHasValidationError(field: string): void + + /** + * Assert flash messages does not have validation errors + * for the given field + */ + assertDoesNotHaveValidationError(field: string): void + + /** + * Assert error message for a given field + */ + assertValidationError(field: string, message: string): void + + /** + * Assert all error messages for a given field + */ + assertValidationErrors(field: string, messages: string[]): void + } +} + +/** + * Hooks AdonisJS Session with the Japa API Client + * plugin + */ +export const sessionApiClient = (app: ApplicationService) => { + const pluginFn: PluginFn = async function () { + const config = app.config.get('session') + + /** + * Disallow usage of driver other than memory during testing + */ + if (config.driver !== 'memory') { + throw new RuntimeException( + `Cannot use session driver "${config.driver}" during testing. Switch to memory driver` + ) + } + + /** + * Register the memory driver if not already registered + */ + await registerSessionDriver(app, 'memory') + + /** + * Stick an singleton session client to APIRequest. The session + * client is used to keep a track of session data we have + * to send during the request. + */ + ApiRequest.getter( + 'sessionClient', + function () { + return new SessionClient(sessionDriversList.create('memory', config)) + }, + true + ) + + /** + * Define session data + */ + ApiRequest.macro('withSession', function (this: ApiRequest, data) { + this.sessionClient.merge(data) + return this + }) + + /** + * Define flash messages + */ + ApiRequest.macro('withFlashMessages', function (this: ApiRequest, data) { + this.sessionClient.flash(data) + return this + }) + + /** + * Get session data + */ + ApiResponse.macro('session', function (this: ApiResponse, key) { + return key ? lodash.get(this.sessionBag.values, key) : this.sessionBag.values + }) + + /** + * Get flash messages + */ + ApiResponse.macro('flashMessages', function (this: ApiResponse) { + return this.sessionBag.flashMessages + }) + ApiResponse.macro('flashMessage', function (this: ApiResponse, key) { + return lodash.get(this.sessionBag.flashMessages, key) + }) + + /** + * Response session assertions + */ + ApiResponse.macro('assertSession', function (this: ApiResponse, key, value) { + this.assert!.property(this.session(), key) + if (value !== undefined) { + this.assert!.deepEqual(this.session(key), value) + } + }) + ApiResponse.macro('assertSessionMissing', function (this: ApiResponse, key) { + this.assert!.notProperty(this.session(), key) + }) + ApiResponse.macro('assertFlashMessage', function (this: ApiResponse, key, value) { + this.assert!.property(this.flashMessages(), key) + if (value !== undefined) { + this.assert!.deepEqual(this.flashMessage(key), value) + } + }) + ApiResponse.macro('assertFlashMissing', function (this: ApiResponse, key) { + this.assert!.notProperty(this.flashMessages(), key) + }) + ApiResponse.macro('assertHasValidationError', function (this: ApiResponse, field) { + this.assert!.property(this.flashMessage('errors'), field) + }) + ApiResponse.macro('assertDoesNotHaveValidationError', function (this: ApiResponse, field) { + this.assert!.notProperty(this.flashMessage('errors'), field) + }) + ApiResponse.macro('assertValidationError', function (this: ApiResponse, field, message) { + this.assert!.include(this.flashMessage('errors')?.[field] || [], message) + }) + ApiResponse.macro('assertValidationErrors', function (this: ApiResponse, field, messages) { + this.assert!.deepEqual(this.flashMessage('errors')?.[field] || [], messages) + }) + + /** + * We define the hook using the "request.setup" method because we + * want to allow other Japa hooks to mutate the session store + * without running into race conditions + */ + ApiClient.onRequest((request) => { + request.setup(async () => { + /** + * Set cookie + */ + request.withCookie(config.cookieName, request.sessionClient.sessionId) + + /** + * Persist data + */ + await request.sessionClient.commit() + + /** + * Cleanup if request fails + */ + return async (error: any) => { + if (error) { + await request.sessionClient.destroy() + } + } + }) + + request.teardown(async (response) => { + const sessionId = response.cookie(config.cookieName) + + /** + * Reading session data from the response cookie + */ + response.sessionBag = sessionId + ? await response.request.sessionClient.load(sessionId.value) + : { + values: {}, + flashMessages: {}, + } + + /** + * Cleanup state + */ + await request.sessionClient.destroy(sessionId?.value) + }) + }) + } + + return pluginFn +} diff --git a/src/types/main.ts b/src/types/main.ts index a5adb86..de8e3ce 100644 --- a/src/types/main.ts +++ b/src/types/main.ts @@ -127,5 +127,5 @@ export interface SessionDriversList { file: (config: SessionConfig, ctx: HttpContext) => FileDriver cookie: (config: SessionConfig, ctx: HttpContext) => CookieDriver redis: (config: SessionConfig, ctx: HttpContext) => RedisDriver - memory: (config: SessionConfig, ctx: HttpContext) => MemoryDriver + memory: (config: SessionConfig, ctx?: HttpContext) => MemoryDriver } diff --git a/test_helpers/index.ts b/test_helpers/index.ts index 2a25bf6..fc6454a 100644 --- a/test_helpers/index.ts +++ b/test_helpers/index.ts @@ -8,7 +8,14 @@ */ import { getActiveTest } from '@japa/runner' +import { ApiClient } from '@japa/api-client' +import { runner } from '@japa/runner/factories' +import { pluginAdonisJS } from '@japa/plugin-adonisjs' +import type { ApplicationService } from '@adonisjs/core/types' import { IncomingMessage, ServerResponse, createServer } from 'node:http' +import { Suite, Emitter as JapaEmitter, Refiner, Test, TestContext } from '@japa/runner/core' + +import { sessionApiClient } from '../src/plugins/japa/api_client.js' export const httpServer = { create(callback: (req: IncomingMessage, res: ServerResponse) => any) { @@ -21,3 +28,47 @@ export const httpServer = { return server }, } + +/** + * Runs a japa test in isolation + */ +export async function runJapaTest(app: ApplicationService, callback: Parameters[0]) { + ApiClient.clearSetupHooks() + ApiClient.clearTeardownHooks() + ApiClient.clearRequestHandlers() + + const japaEmitter = new JapaEmitter() + const refiner = new Refiner() + + const t = new Test('make api request', (self) => new TestContext(self), japaEmitter, refiner) + t.run(callback) + + const suite = new Suite('unit', japaEmitter, refiner) + suite.add(t) + + await runner() + .configure({ + reporters: { + activated: ['sync'], + list: [ + { + name: 'sync', + handler(r, emitter) { + emitter.on('runner:end', function () { + const summary = r.getSummary() + if (summary.hasError) { + throw summary.failureTree[0].children[0].errors[0].error + } + }) + }, + }, + ], + }, + plugins: [pluginAdonisJS(app), sessionApiClient(app)], + files: [], + refiner: refiner, + }) + .useEmitter(japaEmitter) + .withSuites([suite]) + .run() +} diff --git a/tests/plugins/api_client.spec.ts b/tests/plugins/api_client.spec.ts new file mode 100644 index 0000000..a2c3dcd --- /dev/null +++ b/tests/plugins/api_client.spec.ts @@ -0,0 +1,229 @@ +/* + * @adonisjs/session + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import getPort from 'get-port' +import { test } from '@japa/runner' +import { Emitter } from '@adonisjs/core/events' +import { AppFactory } from '@adonisjs/core/factories/app' +import { ApplicationService, EventsList } from '@adonisjs/core/types' +import { EncryptionFactory } from '@adonisjs/core/factories/encryption' +import { HttpContextFactory, RequestFactory, ResponseFactory } from '@adonisjs/core/factories/http' + +import { Session } from '../../src/session.js' +import { SessionConfig } from '../../src/types/main.js' +import { defineConfig } from '../../src/define_config.js' +import { MemoryDriver } from '../../src/drivers/memory.js' +import sessionDriversList from '../../src/drivers_collection.js' +import { httpServer, runJapaTest } from '../../test_helpers/index.js' + +const app = new AppFactory().create(new URL('./', import.meta.url), () => {}) as ApplicationService + +const emitter = new Emitter(app) +const encryption = new EncryptionFactory().create() + +const sessionConfig: SessionConfig = { + enabled: true, + age: '2 hours', + clearWithBrowser: false, + cookieName: 'adonis_session', + driver: 'cookie', + cookie: {}, +} + +test.group('Api client', (group) => { + group.setup(async () => { + app.useConfig({ + session: defineConfig({ + driver: 'memory', + }), + }) + await app.init() + await app.boot() + app.container.singleton('encryption', () => encryption) + }) + + test('set session from the client', async ({ assert }) => { + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() + + const session = new Session( + sessionConfig, + sessionDriversList.create('memory', sessionConfig), + emitter, + ctx + ) + + await session.initiate(false) + assert.deepEqual(session.all(), { username: 'virk' }) + + await session.commit() + response.finish() + }) + + const port = await getPort({ port: 3333 }) + const url = `http://localhost:${port}` + server.listen(port) + + await runJapaTest(app, async ({ client }) => { + await client.get(url).withSession({ username: 'virk' }) + }) + }) + + test('set flash messages from the client', async ({ assert }) => { + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() + + const session = new Session( + sessionConfig, + sessionDriversList.create('memory', sessionConfig), + emitter, + ctx + ) + + await session.initiate(false) + assert.deepEqual(session.flashMessages.all(), { username: 'virk' }) + + await session.commit() + response.finish() + }) + + const port = await getPort({ port: 3333 }) + const url = `http://localhost:${port}` + server.listen(port) + + await runJapaTest(app, async ({ client }) => { + await client.get(url).withFlashMessages({ username: 'virk' }) + }) + }) + + test('read response session data', async ({ assert }) => { + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() + + const session = new Session( + sessionConfig, + sessionDriversList.create('memory', sessionConfig), + emitter, + ctx + ) + + await session.initiate(false) + session.put('name', 'virk') + + await session.commit() + response.finish() + }) + + const port = await getPort({ port: 3333 }) + const url = `http://localhost:${port}` + server.listen(port) + + await runJapaTest(app, async ({ client }) => { + const response = await client.get(url) + assert.deepEqual(response.session(), { name: 'virk' }) + assert.lengthOf(MemoryDriver.sessions, 0) + }) + }) + + test('read response flashMessages', async ({ assert }) => { + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() + + const session = new Session( + sessionConfig, + sessionDriversList.create('memory', sessionConfig), + emitter, + ctx + ) + + await session.initiate(false) + session.flash('name', 'virk') + + await session.commit() + response.finish() + }) + + const port = await getPort({ port: 3333 }) + const url = `http://localhost:${port}` + server.listen(port) + + await runJapaTest(app, async ({ client }) => { + const response = await client.get(url) + assert.deepEqual(response.flashMessages(), { name: 'virk' }) + assert.lengthOf(MemoryDriver.sessions, 0) + }) + }) + + test('assert session and flash messages', async ({ assert }) => { + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() + + const session = new Session( + sessionConfig, + sessionDriversList.create('memory', sessionConfig), + emitter, + ctx + ) + + await session.initiate(false) + session.put('name', 'virk') + session.flash({ + succeed: false, + hasErrors: true, + errors: { username: ['field is required', 'field must be alpha numeric'] }, + }) + + await session.commit() + response.finish() + }) + + const port = await getPort({ port: 3333 }) + const url = `http://localhost:${port}` + server.listen(port) + + await runJapaTest(app, async ({ client }) => { + const response = await client.get(url) + assert.lengthOf(MemoryDriver.sessions, 0) + + response.assertSession('name') + response.assertSession('name', 'virk') + response.assertSessionMissing('age') + + response.assertFlashMessage('succeed') + response.assertFlashMessage('hasErrors') + response.assertFlashMessage('hasErrors', true) + response.assertFlashMessage('succeed', false) + response.assertFlashMissing('notifications') + + response.assertValidationError('username', 'field is required') + response.assertValidationErrors('username', [ + 'field is required', + 'field must be alpha numeric', + ]) + response.assertDoesNotHaveValidationError('email') + + assert.throws(() => response.assertSession('name', 'foo')) + assert.throws(() => response.assertSessionMissing('name')) + assert.throws(() => response.assertFlashMissing('succeed')) + assert.throws(() => response.assertFlashMessage('succeed', true)) + assert.throws(() => response.assertDoesNotHaveValidationError('username')) + assert.throws(() => response.assertValidationError('username', 'field is missing')) + }) + }) +}) diff --git a/tests/session_client.spec.ts b/tests/session_client.spec.ts index 1d49d5b..a3d7e6b 100644 --- a/tests/session_client.spec.ts +++ b/tests/session_client.spec.ts @@ -8,24 +8,9 @@ */ import { test } from '@japa/runner' -import { CookieClient } from '@adonisjs/core/http' -import { EncryptionFactory } from '@adonisjs/core/factories/encryption' - import { SessionClient } from '../src/client.js' -import type { SessionConfig } from '../src/types/main.js' import { MemoryDriver } from '../src/drivers/memory.js' -const encryption = new EncryptionFactory().create() -const cookieClient = new CookieClient(encryption) -const sessionConfig: SessionConfig = { - enabled: true, - age: '2 hours', - clearWithBrowser: false, - cookieName: 'adonis_session', - driver: 'cookie', - cookie: {}, -} - test.group('Session Client', (group) => { group.each.teardown(async () => { MemoryDriver.sessions.clear() @@ -33,15 +18,13 @@ test.group('Session Client', (group) => { test('define session data using session id', async ({ assert }) => { const driver = new MemoryDriver() - const client = new SessionClient(sessionConfig, driver, cookieClient) - const { sessionId, signedSessionId, cookieName } = await client.commit( - { foo: 'bar' }, - { success: true } - ) + const client = new SessionClient(driver) + + client.merge({ foo: 'bar' }) + client.flash({ success: true }) + await client.commit() - assert.equal(cookieName, 'adonis_session') - assert.equal(cookieClient.unsign(cookieName, signedSessionId), sessionId) - assert.deepEqual(driver.read(sessionId), { + assert.deepEqual(driver.read(client.sessionId), { foo: 'bar', __flash__: { success: true, @@ -49,14 +32,16 @@ test.group('Session Client', (group) => { }) }) - test('read existing session data', async ({ assert }) => { + test('load data from the store', async ({ assert }) => { const driver = new MemoryDriver() - const client = new SessionClient(sessionConfig, driver, cookieClient) - const { sessionId } = await client.commit({ foo: 'bar' }, { success: true }) + const client = new SessionClient(driver) - assert.deepEqual(await client.load({}), { - sessionId, - session: { + client.merge({ foo: 'bar' }) + client.flash({ success: true }) + await client.commit() + + assert.deepEqual(await client.load(), { + values: { foo: 'bar', }, flashMessages: { @@ -65,17 +50,28 @@ test.group('Session Client', (group) => { }) }) - test('clear session data', async ({ assert }) => { + test('destroy session', async ({ assert }) => { const driver = new MemoryDriver() - const client = new SessionClient(sessionConfig, driver, cookieClient) - const { sessionId } = await client.commit({ foo: 'bar' }, { success: true }) + const client = new SessionClient(driver) + + client.merge({ foo: 'bar' }) + client.flash({ success: true }) + await client.commit() + + assert.deepEqual(await client.load(), { + values: { + foo: 'bar', + }, + flashMessages: { + success: true, + }, + }) - await client.forget() + await client.destroy() - assert.deepEqual(await client.load({}), { - sessionId, - session: {}, - flashMessages: null, + assert.deepEqual(await client.load(), { + values: {}, + flashMessages: {}, }) }) }) From 6cbcbb3b7bd33e5a8fcecb9c7a61d6cf22b800ef Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 14 Sep 2023 16:44:14 +0530 Subject: [PATCH 087/112] feat: implement session browser client --- package.json | 14 +- providers/session_provider.ts | 4 +- .../edge.ts} | 4 +- src/plugins/japa/browser_client.ts | 183 ++++++++++++++++ test_helpers/index.ts | 22 +- tests/drivers/memory.spec.ts | 2 +- tests/plugins/api_client.spec.ts | 2 + tests/plugins/browser_client.spec.ts | 199 ++++++++++++++++++ 8 files changed, 419 insertions(+), 11 deletions(-) rename src/{edge_plugin_adonisjs_session.ts => plugins/edge.ts} (96%) create mode 100644 src/plugins/japa/browser_client.ts create mode 100644 tests/plugins/browser_client.spec.ts diff --git a/package.json b/package.json index 174dc99..b8757dd 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,9 @@ "./factories": "./build/factories/main.js", "./session_provider": "./build/providers/session_provider.js", "./session_middleware": "./build/src/session_middleware.js", - "./plugin_edge": "./build/src/edge_plugin_adonisjs_session.js", + "./plugins/edge": "./build/src/plugins/edge.js", + "./plugins/api_client": "./build/src/plugins/japa/api_client.js", + "./plugins/browser_client": "./build/src/plugins/japa/browser_client.js", "./client": "./build/src/client.js", "./types": "./build/src/types.js" }, @@ -51,6 +53,7 @@ "@adonisjs/tsconfig": "^1.1.8", "@japa/api-client": "^2.0.0-0", "@japa/assert": "^2.0.0-1", + "@japa/browser-client": "^2.0.0-3", "@japa/file-system": "^2.0.0-1", "@japa/plugin-adonisjs": "^2.0.0-1", "@japa/runner": "^3.0.0-6", @@ -70,6 +73,7 @@ "github-label-sync": "^2.3.1", "husky": "^8.0.3", "np": "^8.0.4", + "playwright": "^1.38.0", "prettier": "^3.0.2", "set-cookie-parser": "^2.6.0", "supertest": "^6.3.3", @@ -82,6 +86,8 @@ "peerDependencies": { "@adonisjs/core": "^6.1.5-22", "@adonisjs/redis": "^8.0.0-10", + "@japa/api-client": "^2.0.0-0", + "@japa/browser-client": "^2.0.0-3", "edge.js": "^6.0.0-8" }, "peerDependenciesMeta": { @@ -90,6 +96,12 @@ }, "edge.js": { "optional": true + }, + "@japa/api-client": { + "optional": true + }, + "@japa/browser-client": { + "optional": true } }, "author": "virk,adonisjs", diff --git a/providers/session_provider.ts b/providers/session_provider.ts index 70a6007..2beb18a 100644 --- a/providers/session_provider.ts +++ b/providers/session_provider.ts @@ -58,8 +58,8 @@ export default class SessionProvider { const edge = await this.getEdge() if (edge) { - const { edgePluginAdonisJSSession } = await import('../src/edge_plugin_adonisjs_session.js') - edge.use(edgePluginAdonisJSSession) + const { edgePluginSession } = await import('../src/plugins/edge.js') + edge.use(edgePluginSession) } } } diff --git a/src/edge_plugin_adonisjs_session.ts b/src/plugins/edge.ts similarity index 96% rename from src/edge_plugin_adonisjs_session.ts rename to src/plugins/edge.ts index 02d2f7a..f4ed69c 100644 --- a/src/edge_plugin_adonisjs_session.ts +++ b/src/plugins/edge.ts @@ -8,13 +8,13 @@ */ import type { PluginFn } from 'edge.js/types' -import debug from './debug.js' +import debug from '../debug.js' /** * The edge plugin for AdonisJS Session adds tags to read * flash messages */ -export const edgePluginAdonisJSSession: PluginFn = (edge) => { +export const edgePluginSession: PluginFn = (edge) => { debug('registering session tags with edge') edge.registerTag({ diff --git a/src/plugins/japa/browser_client.ts b/src/plugins/japa/browser_client.ts new file mode 100644 index 0000000..c4cf53a --- /dev/null +++ b/src/plugins/japa/browser_client.ts @@ -0,0 +1,183 @@ +/* + * @adonisjs/session + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { RuntimeException } from '@poppinss/utils' +import type { PluginFn } from '@japa/runner/types' +import { decoratorsCollection } from '@japa/browser-client' +import type { ApplicationService } from '@adonisjs/core/types' +import type { CookieOptions as AdonisCookieOptions } from '@adonisjs/core/types/http' + +import { SessionClient } from '../../client.js' +import { registerSessionDriver } from '../../helpers.js' +import sessionDriversList from '../../drivers_collection.js' +import type { SessionConfig, SessionData } from '../../types/main.js' + +declare module 'playwright' { + export interface BrowserContext { + sessionClient: SessionClient + + /** + * Initiate session. The session id cookie will be defined + * if missing + */ + initiateSession(options?: Partial): Promise + + /** + * Returns data from the session store + */ + getSession(): Promise + + /** + * Returns data from the session store + */ + getFlashMessages(): Promise + + /** + * Set session data + */ + setSession(values: SessionData): Promise + + /** + * Set flash messages + */ + setFlashMessages(values: SessionData): Promise + } +} + +/** + * Transforming AdonisJS same site option to playwright + * same site option. + */ +function transformSameSiteOption(sameSite?: AdonisCookieOptions['sameSite']) { + if (!sameSite) { + return + } + + if (sameSite === true || sameSite === 'strict') { + return 'Strict' as const + } + + if (sameSite === 'lax') { + return 'Lax' as const + } + + if (sameSite === 'none') { + return 'None' as const + } +} + +/** + * Transforming AdonisJS session config to playwright cookie options. + */ +function getSessionCookieOptions( + config: SessionConfig, + cookieOptions?: Partial +) { + const options = { ...config.cookie, ...cookieOptions } + return { + ...options, + expires: undefined, + sameSite: transformSameSiteOption(options.sameSite), + } +} + +/** + * Hooks AdonisJS Session with the Japa API Client + * plugin + */ +export const sessionBrowserClient = (app: ApplicationService) => { + const pluginFn: PluginFn = async function () { + const config = app.config.get('session') + + /** + * Disallow usage of driver other than memory during testing + */ + if (config.driver !== 'memory') { + throw new RuntimeException( + `Cannot use session driver "${config.driver}" during testing. Switch to memory driver` + ) + } + + /** + * Register the memory driver if not already registered + */ + await registerSessionDriver(app, 'memory') + + decoratorsCollection.register({ + context(context) { + /** + * Reference to session client per browser context + */ + context.sessionClient = new SessionClient(sessionDriversList.create('memory', config)) + + /** + * Initiating session store + */ + context.initiateSession = async function (options) { + const sessionId = await context.getCookie(config.cookieName) + if (sessionId) { + context.sessionClient.sessionId = sessionId + return + } + + await context.setCookie( + config.cookieName, + context.sessionClient.sessionId, + getSessionCookieOptions(config, options) + ) + } + + /** + * Returns session data + */ + context.getSession = async function () { + await context.initiateSession() + const sessionData = await context.sessionClient.load() + return sessionData.values + } + + /** + * Returns flash messages from the data store + */ + context.getFlashMessages = async function () { + await context.initiateSession() + const sessionData = await context.sessionClient.load() + return sessionData.flashMessages + } + + /** + * Set session data + */ + context.setSession = async function (values) { + await context.initiateSession() + context.sessionClient.merge(values) + await context.sessionClient.commit() + } + + /** + * Set flash messages + */ + context.setFlashMessages = async function (values) { + await context.initiateSession() + context.sessionClient.flash(values) + await context.sessionClient.commit() + } + + /** + * Destroy session when context is closed + */ + context.on('close', async function () { + await context.sessionClient.destroy() + }) + }, + }) + } + + return pluginFn +} diff --git a/test_helpers/index.ts b/test_helpers/index.ts index fc6454a..004a70b 100644 --- a/test_helpers/index.ts +++ b/test_helpers/index.ts @@ -8,14 +8,16 @@ */ import { getActiveTest } from '@japa/runner' -import { ApiClient } from '@japa/api-client' import { runner } from '@japa/runner/factories' +import { browserClient } from '@japa/browser-client' import { pluginAdonisJS } from '@japa/plugin-adonisjs' +import { ApiClient, apiClient } from '@japa/api-client' import type { ApplicationService } from '@adonisjs/core/types' import { IncomingMessage, ServerResponse, createServer } from 'node:http' import { Suite, Emitter as JapaEmitter, Refiner, Test, TestContext } from '@japa/runner/core' import { sessionApiClient } from '../src/plugins/japa/api_client.js' +import { sessionBrowserClient } from '../src/plugins/japa/browser_client.js' export const httpServer = { create(callback: (req: IncomingMessage, res: ServerResponse) => any) { @@ -43,8 +45,7 @@ export async function runJapaTest(app: ApplicationService, callback: Parameters< const t = new Test('make api request', (self) => new TestContext(self), japaEmitter, refiner) t.run(callback) - const suite = new Suite('unit', japaEmitter, refiner) - suite.add(t) + const unit = new Suite('unit', japaEmitter, refiner) await runner() .configure({ @@ -64,11 +65,22 @@ export async function runJapaTest(app: ApplicationService, callback: Parameters< }, ], }, - plugins: [pluginAdonisJS(app), sessionApiClient(app)], + plugins: [ + apiClient(), + browserClient({ runInSuites: ['unit'] }), + pluginAdonisJS(app), + sessionApiClient(app), + sessionBrowserClient(app), + ({ runner: r }) => { + r.onSuite((suite) => { + suite.add(t) + }) + }, + ], files: [], refiner: refiner, }) .useEmitter(japaEmitter) - .withSuites([suite]) + .withSuites([unit]) .run() } diff --git a/tests/drivers/memory.spec.ts b/tests/drivers/memory.spec.ts index fb1281a..34ef317 100644 --- a/tests/drivers/memory.spec.ts +++ b/tests/drivers/memory.spec.ts @@ -12,7 +12,7 @@ import { MemoryDriver } from '../../src/drivers/memory.js' test.group('Memory driver', (group) => { group.each.setup(() => { - MemoryDriver.sessions.clear() + return () => MemoryDriver.sessions.clear() }) test('return null when session does not exists', async ({ assert }) => { diff --git a/tests/plugins/api_client.spec.ts b/tests/plugins/api_client.spec.ts index a2c3dcd..082c10a 100644 --- a/tests/plugins/api_client.spec.ts +++ b/tests/plugins/api_client.spec.ts @@ -74,6 +74,7 @@ test.group('Api client', (group) => { await runJapaTest(app, async ({ client }) => { await client.get(url).withSession({ username: 'virk' }) + assert.lengthOf(MemoryDriver.sessions, 0) }) }) @@ -103,6 +104,7 @@ test.group('Api client', (group) => { await runJapaTest(app, async ({ client }) => { await client.get(url).withFlashMessages({ username: 'virk' }) + assert.lengthOf(MemoryDriver.sessions, 0) }) }) diff --git a/tests/plugins/browser_client.spec.ts b/tests/plugins/browser_client.spec.ts new file mode 100644 index 0000000..dfd3011 --- /dev/null +++ b/tests/plugins/browser_client.spec.ts @@ -0,0 +1,199 @@ +/* + * @adonisjs/session + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import getPort from 'get-port' +import { test } from '@japa/runner' +import { Emitter } from '@adonisjs/core/events' +import { AppFactory } from '@adonisjs/core/factories/app' +import { ApplicationService, EventsList } from '@adonisjs/core/types' +import { EncryptionFactory } from '@adonisjs/core/factories/encryption' +import { HttpContextFactory, RequestFactory, ResponseFactory } from '@adonisjs/core/factories/http' + +import { Session } from '../../src/session.js' +import { SessionConfig } from '../../src/types/main.js' +import { defineConfig } from '../../src/define_config.js' +import { MemoryDriver } from '../../src/drivers/memory.js' +import sessionDriversList from '../../src/drivers_collection.js' +import { httpServer, runJapaTest } from '../../test_helpers/index.js' + +const app = new AppFactory().create(new URL('./', import.meta.url), () => {}) as ApplicationService + +const emitter = new Emitter(app) +const encryption = new EncryptionFactory().create() + +const sessionConfig: SessionConfig = { + enabled: true, + age: '2 hours', + clearWithBrowser: false, + cookieName: 'adonis_session', + driver: 'cookie', + cookie: {}, +} + +test.group('Browser client', (group) => { + group.setup(async () => { + app.useConfig({ + session: defineConfig({ + driver: 'memory', + }), + }) + await app.init() + await app.boot() + app.container.singleton('encryption', () => encryption) + }) + + test('set session from the client', async ({ assert }) => { + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() + + const session = new Session( + sessionConfig, + sessionDriversList.create('memory', sessionConfig), + emitter, + ctx + ) + + await session.initiate(false) + assert.deepEqual(session.all(), { username: 'virk' }) + + await session.commit() + response.finish() + }) + + const port = await getPort({ port: 3333 }) + const url = `http://localhost:${port}` + server.listen(port) + + await runJapaTest(app, async ({ visit, browserContext }) => { + await browserContext.initiateSession({ domain: new URL(url).host, path: '/' }) + await browserContext.setSession({ username: 'virk' }) + await visit(url) + + assert.lengthOf(MemoryDriver.sessions, 1) + await browserContext.close() + assert.lengthOf(MemoryDriver.sessions, 0) + }) + }) + + test('set flash messages from the client', async ({ assert }) => { + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() + + const session = new Session( + sessionConfig, + sessionDriversList.create('memory', sessionConfig), + emitter, + ctx + ) + + await session.initiate(false) + assert.deepEqual(session.flashMessages.all(), { username: 'virk' }) + + await session.commit() + response.finish() + }) + + const port = await getPort({ port: 3333 }) + const url = `http://localhost:${port}` + server.listen(port) + + await runJapaTest(app, async ({ browserContext, visit }) => { + await browserContext.initiateSession({ domain: new URL(url).host, path: '/' }) + await browserContext.setFlashMessages({ username: 'virk' }) + await visit(url) + + /** + * Since the server clears the session after + * reading the flash messages, the store + * should be empty post visit + */ + assert.lengthOf(MemoryDriver.sessions, 0) + await browserContext.close() + assert.lengthOf(MemoryDriver.sessions, 0) + }) + }) + + test('read response session data', async ({ assert }) => { + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() + + const session = new Session( + sessionConfig, + sessionDriversList.create('memory', sessionConfig), + emitter, + ctx + ) + + await session.initiate(false) + session.put('name', 'virk') + + await session.commit() + response.finish() + }) + + const port = await getPort({ port: 3333 }) + const url = `http://localhost:${port}` + server.listen(port) + + await runJapaTest(app, async ({ browserContext, visit }) => { + await browserContext.initiateSession({ domain: new URL(url).host, path: '/' }) + await browserContext.setFlashMessages({ username: 'virk' }) + await visit(url) + + assert.deepEqual(await browserContext.getSession(), { name: 'virk' }) + + assert.lengthOf(MemoryDriver.sessions, 1) + await browserContext.close() + assert.lengthOf(MemoryDriver.sessions, 0) + }) + }) + + test('read response flashMessages', async ({ assert }) => { + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() + + const session = new Session( + sessionConfig, + sessionDriversList.create('memory', sessionConfig), + emitter, + ctx + ) + + await session.initiate(false) + session.flash('name', 'virk') + + await session.commit() + response.finish() + }) + + const port = await getPort({ port: 3333 }) + const url = `http://localhost:${port}` + server.listen(port) + + await runJapaTest(app, async ({ browserContext, visit }) => { + await browserContext.initiateSession({ domain: new URL(url).host, path: '/' }) + await browserContext.setFlashMessages({ username: 'virk' }) + await visit(url) + + assert.deepEqual(await browserContext.getFlashMessages(), { name: 'virk' }) + + assert.lengthOf(MemoryDriver.sessions, 1) + await browserContext.close() + assert.lengthOf(MemoryDriver.sessions, 0) + }) + }) +}) From 332f6ecff23deb5b9e5543462b2122a847ebfa4d Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 14 Sep 2023 16:46:05 +0530 Subject: [PATCH 088/112] ci: install playwright browser --- .github/workflows/checks.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index e90709d..98b0520 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -32,6 +32,8 @@ jobs: node-version: ${{ matrix.node-version }} - name: Install run: npm install + - name: Install Playwright Browsers + run: npx playwright install --with-deps - name: Run tests run: npm test env: @@ -51,6 +53,8 @@ jobs: node-version: ${{ matrix.node-version }} - name: Install run: npm install + - name: Install Playwright Browsers + run: npx playwright install --with-deps - name: Run tests run: npm test env: From aa07fb554e567be951856105ca619c46c027cad4 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 14 Sep 2023 16:49:13 +0530 Subject: [PATCH 089/112] refactor: remove unused imports --- src/drivers_collection.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/drivers_collection.ts b/src/drivers_collection.ts index bdd627d..85f931d 100644 --- a/src/drivers_collection.ts +++ b/src/drivers_collection.ts @@ -8,7 +8,6 @@ */ import { RuntimeException } from '@poppinss/utils' -import type { HttpContext } from '@adonisjs/core/http' import type { SessionDriversList } from './types/main.js' From 066861f52d688cb5b133acfbf02c0bdb985315a8 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 14 Sep 2023 16:49:55 +0530 Subject: [PATCH 090/112] chore: pin swc/core as the latest release breaks --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b8757dd..31a494a 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "@japa/plugin-adonisjs": "^2.0.0-1", "@japa/runner": "^3.0.0-6", "@japa/snapshot": "^2.0.0-1", - "@swc/core": "^1.3.78", + "@swc/core": "1.3.82", "@types/node": "^20.6.0", "@types/set-cookie-parser": "^2.4.3", "@types/supertest": "^2.0.12", From 458b1b15f62aa9b0da7aabfd6b57ddc0c3dec51a Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 15 Sep 2023 09:18:02 +0530 Subject: [PATCH 091/112] chore(release): 7.0.0-11 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 31a494a..7e6905e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@adonisjs/session", "description": "Session provider for AdonisJS", - "version": "7.0.0-10", + "version": "7.0.0-11", "engines": { "node": ">=18.16.0" }, From 9edb89e06af1f22401a80566179d6efdda49efc6 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 18 Oct 2023 13:11:00 +0530 Subject: [PATCH 092/112] chore: update dependencies --- package.json | 38 +++++++++++++-------------- src/session.ts | 8 +++--- src/session_middleware.ts | 4 +-- stubs/config.stub | 6 ++--- test_helpers/index.ts | 41 +++++------------------------- tests/concurrent_session.spec.ts | 2 +- tests/drivers/file_driver.spec.ts | 5 ++-- tests/drivers/redis_driver.spec.ts | 4 +-- 8 files changed, 41 insertions(+), 67 deletions(-) diff --git a/package.json b/package.json index 7e6905e..aab2f1e 100644 --- a/package.json +++ b/package.json @@ -45,30 +45,30 @@ "quick:test": "node --enable-source-maps --loader=ts-node/esm bin/test.ts" }, "devDependencies": { - "@adonisjs/assembler": "^6.1.3-18", - "@adonisjs/core": "^6.1.5-24", + "@adonisjs/assembler": "^6.1.3-25", + "@adonisjs/core": "^6.1.5-28", "@adonisjs/eslint-config": "^1.1.8", "@adonisjs/prettier-config": "^1.1.8", - "@adonisjs/redis": "^8.0.0-10", + "@adonisjs/redis": "^8.0.0-11", "@adonisjs/tsconfig": "^1.1.8", - "@japa/api-client": "^2.0.0-0", - "@japa/assert": "^2.0.0-1", - "@japa/browser-client": "^2.0.0-3", - "@japa/file-system": "^2.0.0-1", - "@japa/plugin-adonisjs": "^2.0.0-1", - "@japa/runner": "^3.0.0-6", - "@japa/snapshot": "^2.0.0-1", + "@japa/api-client": "^2.0.0", + "@japa/assert": "^2.0.0", + "@japa/browser-client": "^2.0.0", + "@japa/file-system": "^2.0.0", + "@japa/plugin-adonisjs": "^2.0.0-3", + "@japa/runner": "^3.0.3", + "@japa/snapshot": "^2.0.0", "@swc/core": "1.3.82", - "@types/node": "^20.6.0", - "@types/set-cookie-parser": "^2.4.3", - "@types/supertest": "^2.0.12", + "@types/node": "^20.8.7", + "@types/set-cookie-parser": "^2.4.4", + "@types/supertest": "^2.0.14", "@vinejs/vine": "^1.6.0", "c8": "^8.0.0", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", "del-cli": "^5.0.0", "edge.js": "^6.0.0-8", - "eslint": "^8.49.0", + "eslint": "^8.51.0", "get-port": "^7.0.0", "github-label-sync": "^2.3.1", "husky": "^8.0.3", @@ -81,13 +81,13 @@ "typescript": "^5.1.6" }, "dependencies": { - "@poppinss/utils": "^6.5.0-5" + "@poppinss/utils": "^6.5.0" }, "peerDependencies": { - "@adonisjs/core": "^6.1.5-22", - "@adonisjs/redis": "^8.0.0-10", - "@japa/api-client": "^2.0.0-0", - "@japa/browser-client": "^2.0.0-3", + "@adonisjs/core": "^6.1.5-28", + "@adonisjs/redis": "^8.0.0-11", + "@japa/api-client": "^2.0.0", + "@japa/browser-client": "^2.0.0", "edge.js": "^6.0.0-8" }, "peerDependenciesMeta": { diff --git a/src/session.ts b/src/session.ts index bcce1bc..848e598 100644 --- a/src/session.ts +++ b/src/session.ts @@ -9,19 +9,19 @@ import lodash from '@poppinss/utils/lodash' import { cuid } from '@adonisjs/core/helpers' -import { EmitterService } from '@adonisjs/core/types' import type { HttpContext } from '@adonisjs/core/http' +import type { EmitterService } from '@adonisjs/core/types' +import type { HttpError } from '@adonisjs/core/types/http' -import { ReadOnlyStore, Store } from './store.js' +import debug from './debug.js' import * as errors from './errors.js' +import { ReadOnlyStore, Store } from './store.js' import type { SessionData, SessionConfig, AllowedSessionValues, SessionDriverContract, } from './types/main.js' -import debug from './debug.js' -import { HttpError } from '@adonisjs/core/types/http' /** * The session class exposes the API to read and write values to diff --git a/src/session_middleware.ts b/src/session_middleware.ts index a822ee8..d6f50c4 100644 --- a/src/session_middleware.ts +++ b/src/session_middleware.ts @@ -18,8 +18,8 @@ import sessionDriversList from './drivers_collection.js' /** * HttpContext augmentations */ -declare module '@adonisjs/http-server' { - interface HttpContext { +declare module '@adonisjs/core/http' { + export interface HttpContext { session: Session } } diff --git a/stubs/config.stub b/stubs/config.stub index 7691e1c..5a20de4 100644 --- a/stubs/config.stub +++ b/stubs/config.stub @@ -1,6 +1,6 @@ ---- -to: {{ app.configPath('session.ts') }} ---- +{{{ + exports({ to: app.configPath('session.ts') }) +}}} import env from '#start/env' import { defineConfig } from '@adonisjs/session' diff --git a/test_helpers/index.ts b/test_helpers/index.ts index 004a70b..a717437 100644 --- a/test_helpers/index.ts +++ b/test_helpers/index.ts @@ -7,14 +7,15 @@ * file that was distributed with this source code. */ +import type { Test } from '@japa/runner/core' import { getActiveTest } from '@japa/runner' -import { runner } from '@japa/runner/factories' import { browserClient } from '@japa/browser-client' import { pluginAdonisJS } from '@japa/plugin-adonisjs' import { ApiClient, apiClient } from '@japa/api-client' +import { NamedReporterContract } from '@japa/runner/types' +import { runner, syncReporter } from '@japa/runner/factories' import type { ApplicationService } from '@adonisjs/core/types' import { IncomingMessage, ServerResponse, createServer } from 'node:http' -import { Suite, Emitter as JapaEmitter, Refiner, Test, TestContext } from '@japa/runner/core' import { sessionApiClient } from '../src/plugins/japa/api_client.js' import { sessionBrowserClient } from '../src/plugins/japa/browser_client.js' @@ -39,48 +40,20 @@ export async function runJapaTest(app: ApplicationService, callback: Parameters< ApiClient.clearTeardownHooks() ApiClient.clearRequestHandlers() - const japaEmitter = new JapaEmitter() - const refiner = new Refiner() - - const t = new Test('make api request', (self) => new TestContext(self), japaEmitter, refiner) - t.run(callback) - - const unit = new Suite('unit', japaEmitter, refiner) - await runner() .configure({ reporters: { - activated: ['sync'], - list: [ - { - name: 'sync', - handler(r, emitter) { - emitter.on('runner:end', function () { - const summary = r.getSummary() - if (summary.hasError) { - throw summary.failureTree[0].children[0].errors[0].error - } - }) - }, - }, - ], + activated: [syncReporter.name], + list: [syncReporter as NamedReporterContract], }, plugins: [ apiClient(), - browserClient({ runInSuites: ['unit'] }), + browserClient({}), pluginAdonisJS(app), sessionApiClient(app), sessionBrowserClient(app), - ({ runner: r }) => { - r.onSuite((suite) => { - suite.add(t) - }) - }, ], files: [], - refiner: refiner, }) - .useEmitter(japaEmitter) - .withSuites([unit]) - .run() + .runTest('testing japa integration', callback) } diff --git a/tests/concurrent_session.spec.ts b/tests/concurrent_session.spec.ts index 97c1ee5..0844c55 100644 --- a/tests/concurrent_session.spec.ts +++ b/tests/concurrent_session.spec.ts @@ -15,12 +15,12 @@ import setCookieParser from 'set-cookie-parser' import { Emitter } from '@adonisjs/core/events' import { setTimeout } from 'node:timers/promises' import { EventsList } from '@adonisjs/core/types' -import { RedisService } from '@adonisjs/redis/types' import { AppFactory } from '@adonisjs/core/factories/app' import { IncomingMessage, ServerResponse } from 'node:http' import { RedisManagerFactory } from '@adonisjs/redis/factories' import { CookieClient, HttpContext } from '@adonisjs/core/http' import { EncryptionFactory } from '@adonisjs/core/factories/encryption' +import { RedisService, InferConnections } from '@adonisjs/redis/types' import { HttpContextFactory, RequestFactory, ResponseFactory } from '@adonisjs/core/factories/http' import { Session } from '../src/session.js' diff --git a/tests/drivers/file_driver.spec.ts b/tests/drivers/file_driver.spec.ts index 8bc5246..72ebd52 100644 --- a/tests/drivers/file_driver.spec.ts +++ b/tests/drivers/file_driver.spec.ts @@ -9,6 +9,7 @@ import { join } from 'node:path' import { test } from '@japa/runner' +import { stat } from 'node:fs/promises' import { setTimeout } from 'node:timers/promises' import { FileDriver } from '../../src/drivers/file.js' @@ -134,7 +135,7 @@ test.group('File driver', () => { * Making sure the original mTime of the file was smaller * than the current time after wait */ - const { mtimeMs } = await fs.adapter.stat(join(fs.basePath, '1234.txt')) + const { mtimeMs } = await stat(join(fs.basePath, '1234.txt')) assert.isBelow(mtimeMs, Date.now()) await session.touch(sessionId) @@ -142,7 +143,7 @@ test.group('File driver', () => { /** * Ensuring the new mTime is greater than the old mTime */ - let { mtimeMs: newMtimeMs } = await fs.adapter.stat(join(fs.basePath, '1234.txt')) + let { mtimeMs: newMtimeMs } = await stat(join(fs.basePath, '1234.txt')) assert.isAbove(newMtimeMs, mtimeMs) await assert.fileEquals( diff --git a/tests/drivers/redis_driver.spec.ts b/tests/drivers/redis_driver.spec.ts index 4d447ca..ce3b57c 100644 --- a/tests/drivers/redis_driver.spec.ts +++ b/tests/drivers/redis_driver.spec.ts @@ -10,8 +10,8 @@ import { test } from '@japa/runner' import { defineConfig } from '@adonisjs/redis' import { setTimeout } from 'node:timers/promises' -import { RedisService } from '@adonisjs/redis/types' import { RedisManagerFactory } from '@adonisjs/redis/factories' +import type { RedisService, InferConnections } from '@adonisjs/redis/types' import { RedisDriver } from '../../src/drivers/redis.js' @@ -27,7 +27,7 @@ const redisConfig = defineConfig({ }) const redis = new RedisManagerFactory(redisConfig).create() as RedisService declare module '@adonisjs/redis/types' { - interface RedisConnections extends InferConnections {} + export interface RedisConnections extends InferConnections {} } test.group('Redis driver', (group) => { From 3a0a59234dc45954fac087e3c9936018b26d8f35 Mon Sep 17 00:00:00 2001 From: Romain Lanz <2793951+RomainLanz@users.noreply.github.com> Date: Thu, 19 Oct 2023 06:10:34 +0200 Subject: [PATCH 093/112] fix(cookie): change default config (#81) --- stubs/config.stub | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/stubs/config.stub b/stubs/config.stub index 5a20de4..6cc5644 100644 --- a/stubs/config.stub +++ b/stubs/config.stub @@ -2,6 +2,7 @@ exports({ to: app.configPath('session.ts') }) }}} import env from '#start/env' +import app from '@adonisjs/core/services/app'; import { defineConfig } from '@adonisjs/session' export default defineConfig({ @@ -30,7 +31,8 @@ export default defineConfig({ cookie: { path: '/', httpOnly: true, - sameSite: false, + secure: app.inProduction, + sameSite: 'lax', }, /** From 3e967ac7aa1ca9e8f83b29c98b2331441267507b Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 19 Oct 2023 12:11:29 +0530 Subject: [PATCH 094/112] refactor: use session stores and get rid of drivers collection BREAKING CHANGE: In pursuit of using config providers, we have refactored the configuration to use stores for storing session data --- factories/session_middleware_factory.ts | 25 ++- index.ts | 5 +- package.json | 2 +- providers/session_provider.ts | 60 ++++--- src/client.ts | 30 ++-- src/define_config.ts | 143 ++++++++++++--- src/drivers_collection.ts | 54 ------ src/helpers.ts | 59 ------ src/plugins/japa/api_client.ts | 25 ++- src/plugins/japa/browser_client.ts | 23 +-- src/session.ts | 83 +++++---- src/session_middleware.ts | 26 ++- src/{drivers => stores}/cookie.ts | 19 +- src/{drivers => stores}/file.ts | 22 +-- src/{drivers => stores}/memory.ts | 12 +- src/{drivers => stores}/redis.ts | 36 ++-- src/{types/main.ts => types.ts} | 48 ++--- src/types/extended.ts | 21 --- src/{store.ts => values_store.ts} | 7 +- stubs/config.stub | 28 ++- test_helpers/index.ts | 2 +- tests/concurrent_session.spec.ts | 38 ++-- tests/configure.spec.ts | 2 +- tests/define_config.spec.ts | 168 ++++++++++++++++-- tests/drivers_collection.spec.ts | 32 ---- tests/plugins/api_client.spec.ts | 54 ++---- tests/plugins/browser_client.spec.ts | 53 ++---- tests/session.spec.ts | 111 ++++-------- tests/session_client.spec.ts | 10 +- tests/session_middleware.spec.ts | 31 +++- tests/session_provider.spec.ts | 150 +--------------- .../cookie_store.spec.ts} | 16 +- .../file_store.spec.ts} | 22 +-- .../memory_store.spec.ts} | 36 ++-- .../redis_store.spec.ts} | 22 +-- tests/{store.spec.ts => values_store.spec.ts} | 34 ++-- 36 files changed, 687 insertions(+), 822 deletions(-) delete mode 100644 src/drivers_collection.ts delete mode 100644 src/helpers.ts rename src/{drivers => stores}/cookie.ts (69%) rename src/{drivers => stores}/file.ts (82%) rename src/{drivers => stores}/memory.ts (63%) rename src/{drivers => stores}/redis.ts (52%) rename src/{types/main.ts => types.ts} (65%) delete mode 100644 src/types/extended.ts rename src/{store.ts => values_store.ts} (95%) delete mode 100644 tests/drivers_collection.spec.ts rename tests/{drivers/cookie_driver.spec.ts => stores/cookie_store.spec.ts} (92%) rename tests/{drivers/file_driver.spec.ts => stores/file_store.spec.ts} (85%) rename tests/{drivers/memory.spec.ts => stores/memory_store.spec.ts} (57%) rename tests/{drivers/redis_driver.spec.ts => stores/redis_store.spec.ts} (75%) rename tests/{store.spec.ts => values_store.spec.ts} (86%) diff --git a/factories/session_middleware_factory.ts b/factories/session_middleware_factory.ts index faf8053..87be293 100644 --- a/factories/session_middleware_factory.ts +++ b/factories/session_middleware_factory.ts @@ -8,20 +8,26 @@ */ import { Emitter } from '@adonisjs/core/events' -import { ApplicationService, EventsList } from '@adonisjs/core/types' import { AppFactory } from '@adonisjs/core/factories/app' +import type { ApplicationService, EventsList } from '@adonisjs/core/types' import { defineConfig } from '../index.js' -import { SessionConfig } from '../src/types/main.js' -import { registerSessionDriver } from '../src/helpers.js' import SessionMiddleware from '../src/session_middleware.js' +import type { SessionConfig, SessionStoreFactory } from '../src/types.js' /** * Exposes the API to create an instance of the session middleware * without additional plumbing */ export class SessionMiddlewareFactory { - #config: Partial = { driver: 'memory' } + #config: Partial & { + store: string + stores: Record + } = { + store: 'memory', + stores: {}, + } + #emitter?: Emitter #getApp() { @@ -35,7 +41,13 @@ export class SessionMiddlewareFactory { /** * Merge custom options */ - merge(options: { config?: Partial; emitter?: Emitter }) { + merge(options: { + config?: Partial & { + store: string + stores: Record + } + emitter?: Emitter + }) { if (options.config) { this.#config = options.config } @@ -51,8 +63,7 @@ export class SessionMiddlewareFactory { * Creates an instance of the session middleware */ async create() { - const config = defineConfig(this.#config) - await registerSessionDriver(this.#getApp(), config.driver) + const config = await defineConfig(this.#config).resolver(this.#getApp()) return new SessionMiddleware(config, this.#getEmitter()) } } diff --git a/index.ts b/index.ts index 0b8c636..a0a549e 100644 --- a/index.ts +++ b/index.ts @@ -7,10 +7,7 @@ * file that was distributed with this source code. */ -import './src/types/extended.js' - export * as errors from './src/errors.js' export { configure } from './configure.js' export { stubsRoot } from './stubs/main.js' -export { defineConfig } from './src/define_config.js' -export { default as driversList } from './src/drivers_collection.js' +export { defineConfig, stores } from './src/define_config.js' diff --git a/package.json b/package.json index aab2f1e..cec0215 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "@japa/browser-client": "^2.0.0", "@japa/file-system": "^2.0.0", "@japa/plugin-adonisjs": "^2.0.0-3", - "@japa/runner": "^3.0.3", + "@japa/runner": "^3.0.4", "@japa/snapshot": "^2.0.0", "@swc/core": "1.3.82", "@types/node": "^20.8.7", diff --git a/providers/session_provider.ts b/providers/session_provider.ts index 2beb18a..f6b3739 100644 --- a/providers/session_provider.ts +++ b/providers/session_provider.ts @@ -8,12 +8,24 @@ */ import type { Edge } from 'edge.js' +import { configProvider } from '@adonisjs/core' +import { RuntimeException } from '@poppinss/utils' import type { ApplicationService } from '@adonisjs/core/types' -import debug from '../src/debug.js' -import { registerSessionDriver } from '../src/helpers.js' +import type { Session } from '../src/session.js' import SessionMiddleware from '../src/session_middleware.js' +/** + * Events emitted by the session class + */ +declare module '@adonisjs/core/types' { + interface EventsList { + 'session:initiated': { session: Session } + 'session:committed': { session: Session } + 'session:migrated': { fromSessionId: string; toSessionId: string; session: Session } + } +} + /** * Session provider configures the session management inside an * AdonisJS application @@ -22,15 +34,19 @@ export default class SessionProvider { constructor(protected app: ApplicationService) {} /** - * Returns edge when it's installed + * Registers edge plugin when edge is installed + * in the user application. */ - protected async getEdge(): Promise { + protected async registerEdgePlugin() { + let edge: Edge | null = null try { - const { default: edge } = await import('edge.js') - debug('Detected edge.js package. Adding session primitives to it') - return edge - } catch { - return null + const edgeExports = await import('edge.js') + edge = edgeExports.default + } catch {} + + if (edge) { + const { edgePluginSession } = await import('../src/plugins/edge.js') + edge.use(edgePluginSession) } } @@ -39,27 +55,27 @@ export default class SessionProvider { */ register() { this.app.container.singleton(SessionMiddleware, async (resolver) => { - const config = this.app.config.get('session', {}) + const sessionConfigProvider = this.app.config.get('session', {}) + + /** + * Resolve config from the provider + */ + const config = await configProvider.resolve(this.app, sessionConfigProvider) + if (!config) { + throw new RuntimeException( + 'Invalid "config/session.ts" file. Make sure you are using the "defineConfig" method' + ) + } + const emitter = await resolver.make('emitter') return new SessionMiddleware(config, emitter) }) } /** - * Registering the active driver when middleware is used - * + * Adding edge tags (if edge is installed) */ async boot() { - this.app.container.resolving(SessionMiddleware, async () => { - const config = this.app.config.get('session') - await registerSessionDriver(this.app, config.driver) - }) - - const edge = await this.getEdge() - if (edge) { - const { edgePluginSession } = await import('../src/plugins/edge.js') - edge.use(edgePluginSession) - } + await this.registerEdgePlugin() } } diff --git a/src/client.ts b/src/client.ts index 4de56ce..f3b939b 100644 --- a/src/client.ts +++ b/src/client.ts @@ -10,8 +10,8 @@ import { cuid } from '@adonisjs/core/helpers' import debug from './debug.js' -import { Store } from './store.js' -import type { SessionData, SessionDriverContract } from './types/main.js' +import { ValuesStore } from './values_store.js' +import type { SessionData, SessionStoreContract } from './types.js' /** * Session client exposes the API to set session data as a client @@ -20,17 +20,17 @@ export class SessionClient { /** * Data store */ - #store = new Store({}) + #valuesStore = new ValuesStore({}) /** * Flash messages store */ - #flashMessagesStore = new Store({}) + #flashMessagesStore = new ValuesStore({}) /** - * The session driver to use for reading and writing session data + * The session store to use for reading and writing session data */ - #driver: SessionDriverContract + #store: SessionStoreContract /** * Session key for setting flash messages @@ -43,15 +43,15 @@ export class SessionClient { */ sessionId = cuid() - constructor(driver: SessionDriverContract) { - this.#driver = driver + constructor(store: SessionStoreContract) { + this.#store = store } /** * Merge session data */ merge(values: SessionData) { - this.#store.merge(values) + this.#valuesStore.merge(values) return this } @@ -68,12 +68,12 @@ export class SessionClient { */ async commit() { if (!this.#flashMessagesStore.isEmpty) { - this.#store.set(this.flashKey, this.#flashMessagesStore.toJSON()) + this.#valuesStore.set(this.flashKey, this.#flashMessagesStore.toJSON()) } debug('committing session data during api request') - if (!this.#store.isEmpty) { - this.#driver.write(this.sessionId, this.#store.toJSON()) + if (!this.#valuesStore.isEmpty) { + this.#store.write(this.sessionId, this.#valuesStore.toJSON()) } } @@ -82,15 +82,15 @@ export class SessionClient { */ async destroy(sessionId?: string) { debug('destroying session data during api request') - this.#driver.destroy(sessionId || this.sessionId) + this.#store.destroy(sessionId || this.sessionId) } /** * Loads session data from the session store */ async load(sessionId?: string) { - const contents = await this.#driver.read(sessionId || this.sessionId) - const store = new Store(contents) + const contents = await this.#store.read(sessionId || this.sessionId) + const store = new ValuesStore(contents) const flashMessages = store.pull(this.flashKey, {}) return { diff --git a/src/define_config.ts b/src/define_config.ts index 66b4577..a0546fa 100644 --- a/src/define_config.ts +++ b/src/define_config.ts @@ -7,46 +7,145 @@ * file that was distributed with this source code. */ +/// + import string from '@poppinss/utils/string' +import { configProvider } from '@adonisjs/core' +import type { ConfigProvider } from '@adonisjs/core/types' import { InvalidArgumentsException } from '@poppinss/utils' import type { CookieOptions } from '@adonisjs/core/types/http' -import type { SessionConfig } from './types/main.js' import debug from './debug.js' +import { MemoryStore } from './stores/memory.js' +import type { + SessionConfig, + FileStoreConfig, + RedisStoreConfig, + SessionStoreFactory, +} from './types.js' + +/** + * Resolved config with stores + */ +type ResolvedConfig> = SessionConfig & { + store: keyof KnownStores + stores: KnownStores + cookie: Partial +} /** * Helper to normalize session config */ -export function defineConfig( - config: Partial -): SessionConfig & { cookie: Partial } { +export function defineConfig< + KnownStores extends Record>, +>( + config: Partial & { + store: keyof KnownStores | 'memory' + stores: KnownStores + } +): ConfigProvider< + ResolvedConfig<{ + [K in keyof KnownStores]: SessionStoreFactory + }> +> { + debug('processing session config %O', config) + /** - * Make sure a driver is defined + * Make sure a store is defined */ - if (!config.driver) { - throw new InvalidArgumentsException('Missing "driver" property inside the session config') + if (!config.store) { + throw new InvalidArgumentsException('Missing "store" property inside the session config') + } + + /** + * Destructuring config with the default values. We pull out + * stores and cookie values, since we have to transform + * them in the output value. + */ + const { stores, cookie, ...rest } = { + enabled: true, + age: '2h', + cookieName: 'adonis_session', + clearWithBrowser: false, + ...config, } - const age = config.age || '2h' - const clearWithBrowser = config.clearWithBrowser ?? false - const cookieOptions: Partial = { ...config.cookie } + const cookieOptions: Partial = { ...cookie } /** * Define maxAge property when session id cookie is * not a session cookie. */ - if (!clearWithBrowser) { - debug('computing maxAge for session id cookie') - cookieOptions.maxAge = string.seconds.parse(config.age || age) + if (!rest.clearWithBrowser) { + cookieOptions.maxAge = string.seconds.parse(rest.age) + debug('computing maxAge "%s" for session id cookie', cookieOptions.maxAge) } - return { - enabled: true, - age, - clearWithBrowser, - cookieName: 'adonis_session', - cookie: cookieOptions, - driver: config.driver!, - ...config, - } + return configProvider.create(async (app) => { + const storesNames = Object.keys(config.stores) + + /** + * List of stores with memory store always configured + */ + const storesList = { + memory: () => new MemoryStore(), + } as Record + + /** + * Looping for stores and resolving them + */ + for (let storeName of storesNames) { + const store = config.stores[storeName] + if (typeof store === 'function') { + storesList[storeName] = store + } else { + storesList[storeName] = await store.resolver(app) + } + } + + const transformedConfig = { + ...rest, + cookie: cookieOptions, + stores: storesList as { [K in keyof KnownStores]: SessionStoreFactory }, + } + + debug('transformed session config %O', transformedConfig) + return transformedConfig + }) +} + +/** + * Inbuilt stores to store the session data. + */ +export const stores: { + file: (config: FileStoreConfig) => ConfigProvider + redis: (config: RedisStoreConfig) => ConfigProvider + cookie: () => ConfigProvider +} = { + file: (config) => { + return configProvider.create(async () => { + const { FileStore } = await import('./stores/file.js') + return (_, sessionConfig: SessionConfig) => { + return new FileStore(config, sessionConfig.age) + } + }) + }, + redis: (config) => { + return configProvider.create(async (app) => { + const { RedisStore } = await import('./stores/redis.js') + const redis = await app.container.make('redis') + + return (_, sessionConfig: SessionConfig) => { + return new RedisStore(redis.connection(config.connection), sessionConfig.age) + } + }) + }, + cookie: () => { + return configProvider.create(async () => { + const { CookieStore } = await import('./stores/cookie.js') + return (ctx, sessionConfig: SessionConfig) => { + return new CookieStore(sessionConfig.cookie, ctx) + } + }) + }, } diff --git a/src/drivers_collection.ts b/src/drivers_collection.ts deleted file mode 100644 index 85f931d..0000000 --- a/src/drivers_collection.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * @adonisjs/session - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { RuntimeException } from '@poppinss/utils' - -import type { SessionDriversList } from './types/main.js' - -/** - * A global collection of session drivers - */ -class SessionDriversCollection { - /** - * List of registered drivers - */ - list: Partial = {} - - /** - * Extend drivers collection and add a custom - * driver to it. - */ - extend( - driverName: Name, - factoryCallback: SessionDriversList[Name] - ): this { - this.list[driverName] = factoryCallback - return this - } - - /** - * Creates the driver instance with config - */ - create( - name: Name, - ...args: Parameters - ): ReturnType { - const driverFactory = this.list[name] - if (!driverFactory) { - throw new RuntimeException( - `Unknown session driver "${String(name)}". Make sure the driver is registered` - ) - } - - return driverFactory(args[0], args[1]!) as ReturnType - } -} - -const sessionDriversList = new SessionDriversCollection() -export default sessionDriversList diff --git a/src/helpers.ts b/src/helpers.ts deleted file mode 100644 index 80846ae..0000000 --- a/src/helpers.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * @adonisjs/session - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import type { ApplicationService } from '@adonisjs/core/types' - -import debug from './debug.js' -import sessionDriversList from './drivers_collection.js' -import type { SessionDriversList } from './types/main.js' - -/** - * Lazily imports and registers a driver with the sessionDriversList - */ -export async function registerSessionDriver( - app: ApplicationService, - driverInUse: keyof SessionDriversList -) { - /** - * Noop when the driver is already registered - */ - if (sessionDriversList.list[driverInUse]) { - return - } - - debug('registering %s driver', driverInUse) - - if (driverInUse === 'cookie') { - const { CookieDriver } = await import('../src/drivers/cookie.js') - sessionDriversList.extend('cookie', (config, ctx) => new CookieDriver(config.cookie, ctx)) - return - } - - if (driverInUse === 'memory') { - const { MemoryDriver } = await import('../src/drivers/memory.js') - sessionDriversList.extend('memory', () => new MemoryDriver()) - return - } - - if (driverInUse === 'file') { - const { FileDriver } = await import('../src/drivers/file.js') - sessionDriversList.extend('file', (config) => new FileDriver(config.file!, config.age)) - return - } - - if (driverInUse === 'redis') { - const { RedisDriver } = await import('../src/drivers/redis.js') - const redis = await app.container.make('redis') - sessionDriversList.extend( - 'redis', - (config) => new RedisDriver(redis, config.redis!, config.age) - ) - return - } -} diff --git a/src/plugins/japa/api_client.ts b/src/plugins/japa/api_client.ts index ec05a04..c7f72e6 100644 --- a/src/plugins/japa/api_client.ts +++ b/src/plugins/japa/api_client.ts @@ -8,15 +8,14 @@ */ import lodash from '@poppinss/utils/lodash' -import type { PluginFn } from '@japa/runner/types' +import { configProvider } from '@adonisjs/core' import { RuntimeException } from '@poppinss/utils' +import type { PluginFn } from '@japa/runner/types' import type { ApplicationService } from '@adonisjs/core/types' import { ApiClient, ApiRequest, ApiResponse } from '@japa/api-client' import { SessionClient } from '../../client.js' -import { registerSessionDriver } from '../../helpers.js' -import sessionDriversList from '../../drivers_collection.js' -import type { SessionConfig, SessionData } from '../../types/main.js' +import type { SessionData } from '../../types.js' declare module '@japa/api-client' { export interface ApiRequest { @@ -100,27 +99,23 @@ declare module '@japa/api-client' { } /** - * Hooks AdonisJS Session with the Japa API Client + * Hooks AdonisJS Session with the Japa API client * plugin */ export const sessionApiClient = (app: ApplicationService) => { const pluginFn: PluginFn = async function () { - const config = app.config.get('session') + const sessionConfigProvider = app.config.get('session', {}) /** - * Disallow usage of driver other than memory during testing + * Resolve config from the provider */ - if (config.driver !== 'memory') { + const config = await configProvider.resolve(app, sessionConfigProvider) + if (!config) { throw new RuntimeException( - `Cannot use session driver "${config.driver}" during testing. Switch to memory driver` + 'Invalid "config/session.ts" file. Make sure you are using the "defineConfig" method' ) } - /** - * Register the memory driver if not already registered - */ - await registerSessionDriver(app, 'memory') - /** * Stick an singleton session client to APIRequest. The session * client is used to keep a track of session data we have @@ -129,7 +124,7 @@ export const sessionApiClient = (app: ApplicationService) => { ApiRequest.getter( 'sessionClient', function () { - return new SessionClient(sessionDriversList.create('memory', config)) + return new SessionClient(config.stores.memory()) }, true ) diff --git a/src/plugins/japa/browser_client.ts b/src/plugins/japa/browser_client.ts index c4cf53a..acdfe9a 100644 --- a/src/plugins/japa/browser_client.ts +++ b/src/plugins/japa/browser_client.ts @@ -7,6 +7,7 @@ * file that was distributed with this source code. */ +import { configProvider } from '@adonisjs/core' import { RuntimeException } from '@poppinss/utils' import type { PluginFn } from '@japa/runner/types' import { decoratorsCollection } from '@japa/browser-client' @@ -14,9 +15,7 @@ import type { ApplicationService } from '@adonisjs/core/types' import type { CookieOptions as AdonisCookieOptions } from '@adonisjs/core/types/http' import { SessionClient } from '../../client.js' -import { registerSessionDriver } from '../../helpers.js' -import sessionDriversList from '../../drivers_collection.js' -import type { SessionConfig, SessionData } from '../../types/main.js' +import type { SessionConfig, SessionData } from '../../types.js' declare module 'playwright' { export interface BrowserContext { @@ -88,33 +87,29 @@ function getSessionCookieOptions( } /** - * Hooks AdonisJS Session with the Japa API Client + * Hooks AdonisJS Session with the Japa browser client * plugin */ export const sessionBrowserClient = (app: ApplicationService) => { const pluginFn: PluginFn = async function () { - const config = app.config.get('session') + const sessionConfigProvider = app.config.get('session', {}) /** - * Disallow usage of driver other than memory during testing + * Resolve config from the provider */ - if (config.driver !== 'memory') { + const config = await configProvider.resolve(app, sessionConfigProvider) + if (!config) { throw new RuntimeException( - `Cannot use session driver "${config.driver}" during testing. Switch to memory driver` + 'Invalid "config/session.ts" file. Make sure you are using the "defineConfig" method' ) } - /** - * Register the memory driver if not already registered - */ - await registerSessionDriver(app, 'memory') - decoratorsCollection.register({ context(context) { /** * Reference to session client per browser context */ - context.sessionClient = new SessionClient(sessionDriversList.create('memory', config)) + context.sessionClient = new SessionClient(config.stores.memory()) /** * Initiating session store diff --git a/src/session.ts b/src/session.ts index 848e598..3b53581 100644 --- a/src/session.ts +++ b/src/session.ts @@ -15,13 +15,14 @@ import type { HttpError } from '@adonisjs/core/types/http' import debug from './debug.js' import * as errors from './errors.js' -import { ReadOnlyStore, Store } from './store.js' +import { ReadOnlyValuesStore, ValuesStore } from './values_store.js' import type { SessionData, SessionConfig, + SessionStoreFactory, AllowedSessionValues, - SessionDriverContract, -} from './types/main.js' + SessionStoreContract, +} from './types.js' /** * The session class exposes the API to read and write values to @@ -32,11 +33,15 @@ import type { */ export class Session { #config: SessionConfig - #driver: SessionDriverContract + #store: SessionStoreContract #emitter: EmitterService #ctx: HttpContext #readonly: boolean = false - #store?: Store + + /** + * Session values store + */ + #valuesStore?: ValuesStore /** * Session id refers to the session id that will be committed @@ -58,12 +63,12 @@ export class Session { * Store of flash messages that be written during the * HTTP request */ - responseFlashMessages = new Store({}) + responseFlashMessages = new ValuesStore({}) /** * Store of flash messages for the current HTTP request. */ - flashMessages = new Store({}) + flashMessages = new ValuesStore({}) /** * The key to use for storing flash messages inside @@ -98,7 +103,7 @@ export class Session { * A boolean to know if session store has been initiated */ get initiated() { - return !!this.#store + return !!this.#valuesStore } /** @@ -113,7 +118,7 @@ export class Session { * A boolean to know if the session store is empty */ get isEmpty() { - return this.#store?.isEmpty ?? true + return this.#valuesStore?.isEmpty ?? true } /** @@ -121,19 +126,19 @@ export class Session { * modified */ get hasBeenModified() { - return this.#store?.hasBeenModified ?? false + return this.#valuesStore?.hasBeenModified ?? false } constructor( config: SessionConfig, - driver: SessionDriverContract, + storeFactory: SessionStoreFactory, emitter: EmitterService, ctx: HttpContext ) { this.#ctx = ctx this.#config = config - this.#driver = driver this.#emitter = emitter + this.#store = storeFactory(ctx, config) this.#sessionIdFromCookie = ctx.request.cookie(config.cookieName, undefined) this.#sessionId = this.#sessionIdFromCookie || cuid() } @@ -142,8 +147,8 @@ export class Session { * Returns the flash messages store for a given * mode */ - #getFlashStore(mode: 'write' | 'read'): Store { - if (!this.#store) { + #getFlashStore(mode: 'write' | 'read'): ValuesStore { + if (!this.#valuesStore) { throw new errors.E_SESSION_NOT_READY() } @@ -157,8 +162,8 @@ export class Session { /** * Returns the store instance for a given mode */ - #getStore(mode: 'write' | 'read'): Store { - if (!this.#store) { + #getValuesStore(mode: 'write' | 'read'): ValuesStore { + if (!this.#valuesStore) { throw new errors.E_SESSION_NOT_READY() } @@ -166,7 +171,7 @@ export class Session { throw new errors.E_SESSION_NOT_MUTABLE() } - return this.#store + return this.#valuesStore } /** @@ -174,15 +179,15 @@ export class Session { * when called multiple times */ async initiate(readonly: boolean): Promise { - if (this.#store) { + if (this.#valuesStore) { return } debug('initiating session (readonly: %s)', readonly) this.#readonly = readonly - const contents = await this.#driver.read(this.#sessionId) - this.#store = new Store(contents) + const contents = await this.#store.read(this.#sessionId) + this.#valuesStore = new ValuesStore(contents) /** * Extract flash messages from the store and keep a local @@ -203,8 +208,8 @@ export class Session { */ if ('view' in this.#ctx) { this.#ctx.view.share({ - session: new ReadOnlyStore(this.#store.all()), - flashMessages: new ReadOnlyStore(this.flashMessages.all()), + session: new ReadOnlyValuesStore(this.#valuesStore.all()), + flashMessages: new ReadOnlyValuesStore(this.flashMessages.all()), old: function (key: string, defaultValue?: any) { return this.flashMessages.get(key, defaultValue) }, @@ -218,14 +223,14 @@ export class Session { * Put a key-value pair to the session data store */ put(key: string, value: AllowedSessionValues) { - this.#getStore('write').set(key, value) + this.#getValuesStore('write').set(key, value) } /** * Check if a key exists inside the datastore */ has(key: string): boolean { - return this.#getStore('read').has(key) + return this.#getValuesStore('read').has(key) } /** @@ -234,21 +239,21 @@ export class Session { * does not exists or has undefined value. */ get(key: string, defaultValue?: any) { - return this.#getStore('read').get(key, defaultValue) + return this.#getValuesStore('read').get(key, defaultValue) } /** * Get everything from the session store */ all() { - return this.#getStore('read').all() + return this.#getValuesStore('read').all() } /** * Remove a key from the session datastore */ forget(key: string) { - return this.#getStore('write').unset(key) + return this.#getValuesStore('write').unset(key) } /** @@ -256,7 +261,7 @@ export class Session { * and remove it simultaneously. */ pull(key: string, defaultValue?: any) { - return this.#getStore('write').pull(key, defaultValue) + return this.#getValuesStore('write').pull(key, defaultValue) } /** @@ -267,7 +272,7 @@ export class Session { * The value of a new key will be 1 */ increment(key: string, steps: number = 1) { - return this.#getStore('write').increment(key, steps) + return this.#getValuesStore('write').increment(key, steps) } /** @@ -278,14 +283,14 @@ export class Session { * The value of a new key will be -1 */ decrement(key: string, steps: number = 1) { - return this.#getStore('write').decrement(key, steps) + return this.#getValuesStore('write').decrement(key, steps) } /** * Empty the session store */ clear() { - return this.#getStore('write').clear() + return this.#getValuesStore('write').clear() } /** @@ -377,7 +382,7 @@ export class Session { * allowed after commit. */ async commit() { - if (!this.#store || this.readonly) { + if (!this.#valuesStore || this.readonly) { return } @@ -407,7 +412,7 @@ export class Session { */ if (this.isEmpty) { if (this.#sessionIdFromCookie) { - await this.#driver.destroy(this.#sessionIdFromCookie) + await this.#store.destroy(this.#sessionIdFromCookie) } this.#emitter.emit('session:committed', { session: this }) return @@ -419,15 +424,15 @@ export class Session { */ if (!this.hasBeenModified) { if (this.#sessionIdFromCookie && this.#sessionIdFromCookie !== this.#sessionId) { - await this.#driver.destroy(this.#sessionIdFromCookie) - await this.#driver.write(this.#sessionId, this.#store.toJSON()) + await this.#store.destroy(this.#sessionIdFromCookie) + await this.#store.write(this.#sessionId, this.#valuesStore.toJSON()) this.#emitter.emit('session:migrated', { fromSessionId: this.#sessionIdFromCookie, toSessionId: this.sessionId, session: this, }) } else { - await this.#driver.touch(this.#sessionId) + await this.#store.touch(this.#sessionId) } this.#emitter.emit('session:committed', { session: this }) return @@ -437,15 +442,15 @@ export class Session { * Otherwise commit to the session store */ if (this.#sessionIdFromCookie && this.#sessionIdFromCookie !== this.#sessionId) { - await this.#driver.destroy(this.#sessionIdFromCookie) - await this.#driver.write(this.#sessionId, this.#store.toJSON()) + await this.#store.destroy(this.#sessionIdFromCookie) + await this.#store.write(this.#sessionId, this.#valuesStore.toJSON()) this.#emitter.emit('session:migrated', { fromSessionId: this.#sessionIdFromCookie, toSessionId: this.sessionId, session: this, }) } else { - await this.#driver.write(this.#sessionId, this.#store.toJSON()) + await this.#store.write(this.#sessionId, this.#valuesStore.toJSON()) } this.#emitter.emit('session:committed', { session: this }) diff --git a/src/session_middleware.ts b/src/session_middleware.ts index d6f50c4..11ba503 100644 --- a/src/session_middleware.ts +++ b/src/session_middleware.ts @@ -12,8 +12,7 @@ import type { NextFn } from '@adonisjs/core/types/http' import { ExceptionHandler, HttpContext } from '@adonisjs/core/http' import { Session } from './session.js' -import type { SessionConfig } from './types/main.js' -import sessionDriversList from './drivers_collection.js' +import type { SessionConfig, SessionStoreFactory } from './types.js' /** * HttpContext augmentations @@ -41,11 +40,20 @@ ExceptionHandler.macro('renderValidationErrorAsHTML', async function (error, ctx * Session middleware is used to initiate the session store * and commit its values during an HTTP request */ -export default class SessionMiddleware { - #config: SessionConfig +export default class SessionMiddleware> { + #config: SessionConfig & { + store: keyof KnownStores + stores: KnownStores + } #emitter: EmitterService - constructor(config: SessionConfig, emitter: EmitterService) { + constructor( + config: SessionConfig & { + store: keyof KnownStores + stores: KnownStores + }, + emitter: EmitterService + ) { this.#config = config this.#emitter = emitter } @@ -55,8 +63,12 @@ export default class SessionMiddleware { return next() } - const driver = sessionDriversList.create(this.#config.driver, this.#config, ctx) - ctx.session = new Session(this.#config, driver, this.#emitter, ctx) + ctx.session = new Session( + this.#config, + this.#config.stores[this.#config.store], // reference to store factory + this.#emitter, + ctx + ) /** * Initiate session store diff --git a/src/drivers/cookie.ts b/src/stores/cookie.ts similarity index 69% rename from src/drivers/cookie.ts rename to src/stores/cookie.ts index 87f2fdd..54455d4 100644 --- a/src/drivers/cookie.ts +++ b/src/stores/cookie.ts @@ -8,29 +8,30 @@ */ import type { HttpContext } from '@adonisjs/core/http' -import { CookieOptions } from '@adonisjs/core/types/http' -import type { SessionData, SessionDriverContract } from '../types/main.js' +import type { CookieOptions } from '@adonisjs/core/types/http' + import debug from '../debug.js' +import type { SessionData, SessionStoreContract } from '../types.js' /** - * Cookie driver stores the session data inside an encrypted + * Cookie store stores the session data inside an encrypted * cookie. */ -export class CookieDriver implements SessionDriverContract { +export class CookieStore implements SessionStoreContract { #ctx: HttpContext #config: Partial constructor(config: Partial, ctx: HttpContext) { this.#config = config this.#ctx = ctx - debug('initiating cookie driver %O', this.#config) + debug('initiating cookie store %O', this.#config) } /** * Read session value from the cookie */ read(sessionId: string): SessionData | null { - debug('cookie driver: reading session data %s', sessionId) + debug('cookie store: reading session data %s', sessionId) const cookieValue = this.#ctx.request.encryptedCookie(sessionId) if (typeof cookieValue !== 'object') { @@ -44,7 +45,7 @@ export class CookieDriver implements SessionDriverContract { * Write session values to the cookie */ write(sessionId: string, values: SessionData): void { - debug('cookie driver: writing session data %s: %O', sessionId, values) + debug('cookie store: writing session data %s: %O', sessionId, values) this.#ctx.response.encryptedCookie(sessionId, values, this.#config) } @@ -52,7 +53,7 @@ export class CookieDriver implements SessionDriverContract { * Removes the session cookie */ destroy(sessionId: string): void { - debug('cookie driver: destroying session data %s', sessionId) + debug('cookie store: destroying session data %s', sessionId) if (this.#ctx.request.cookiesList()[sessionId]) { this.#ctx.response.clearCookie(sessionId) } @@ -63,7 +64,7 @@ export class CookieDriver implements SessionDriverContract { */ touch(sessionId: string): void { const value = this.read(sessionId) - debug('cookie driver: touching session data %s', sessionId) + debug('cookie store: touching session data %s', sessionId) if (!value) { return } diff --git a/src/drivers/file.ts b/src/stores/file.ts similarity index 82% rename from src/drivers/file.ts rename to src/stores/file.ts index 1def419..e085c70 100644 --- a/src/drivers/file.ts +++ b/src/stores/file.ts @@ -13,21 +13,21 @@ import string from '@poppinss/utils/string' import { MessageBuilder } from '@adonisjs/core/helpers' import { access, mkdir, readFile, rm, writeFile, utimes, stat } from 'node:fs/promises' -import type { FileDriverConfig, SessionData, SessionDriverContract } from '../types/main.js' import debug from '../debug.js' +import type { FileStoreConfig, SessionData, SessionStoreContract } from '../types.js' /** - * File driver writes the session data on the file system as. Each session + * File store writes the session data on the file system as. Each session * id gets its own file. */ -export class FileDriver implements SessionDriverContract { - #config: FileDriverConfig +export class FileStore implements SessionStoreContract { + #config: FileStoreConfig #age: string | number - constructor(config: FileDriverConfig, age: string | number) { + constructor(config: FileStoreConfig, age: string | number) { this.#config = config this.#age = age - debug('initiating file driver %O', this.#config) + debug('initiating file store %O', this.#config) } /** @@ -81,7 +81,7 @@ export class FileDriver implements SessionDriverContract { */ async read(sessionId: string): Promise { const filePath = this.#getFilePath(sessionId) - debug('file driver: reading session data %', sessionId) + debug('file store: reading session data %', sessionId) /** * Return null when no session id file exists in first @@ -97,7 +97,7 @@ export class FileDriver implements SessionDriverContract { */ const sessionWillExpireAt = stats.mtimeMs + string.milliseconds.parse(this.#age) if (Date.now() > sessionWillExpireAt) { - debug('file driver: expired session data %s', sessionId) + debug('file store: expired session data %s', sessionId) return null } @@ -125,7 +125,7 @@ export class FileDriver implements SessionDriverContract { * Writes the session data to the disk as a string */ async write(sessionId: string, values: SessionData): Promise { - debug('file driver: writing session data %s: %O', sessionId, values) + debug('file store: writing session data %s: %O', sessionId, values) const filePath = this.#getFilePath(sessionId) const message = new MessageBuilder().build(values, undefined, sessionId) @@ -137,7 +137,7 @@ export class FileDriver implements SessionDriverContract { * Removes the session file from the disk */ async destroy(sessionId: string): Promise { - debug('file driver: destroying session data %s', sessionId) + debug('file store: destroying session data %s', sessionId) await rm(this.#getFilePath(sessionId), { force: true }) } @@ -146,7 +146,7 @@ export class FileDriver implements SessionDriverContract { * persistence store */ async touch(sessionId: string): Promise { - debug('file driver: touching session data %s', sessionId) + debug('file store: touching session data %s', sessionId) await utimes(this.#getFilePath(sessionId), new Date(), new Date()) } } diff --git a/src/drivers/memory.ts b/src/stores/memory.ts similarity index 63% rename from src/drivers/memory.ts rename to src/stores/memory.ts index e9605dc..3c4890a 100644 --- a/src/drivers/memory.ts +++ b/src/stores/memory.ts @@ -7,33 +7,33 @@ * file that was distributed with this source code. */ -import type { SessionData, SessionDriverContract } from '../types/main.js' +import type { SessionData, SessionStoreContract } from '../types.js' /** - * Memory driver is meant to be used for writing tests. + * Memory store is meant to be used for writing tests. */ -export class MemoryDriver implements SessionDriverContract { +export class MemoryStore implements SessionStoreContract { static sessions: Map = new Map() /** * Read session id value from the memory */ read(sessionId: string): SessionData | null { - return MemoryDriver.sessions.get(sessionId) || null + return MemoryStore.sessions.get(sessionId) || null } /** * Save in memory value for a given session id */ write(sessionId: string, values: SessionData): void { - MemoryDriver.sessions.set(sessionId, values) + MemoryStore.sessions.set(sessionId, values) } /** * Cleanup for a single session */ destroy(sessionId: string): void { - MemoryDriver.sessions.delete(sessionId) + MemoryStore.sessions.delete(sessionId) } touch(): void {} diff --git a/src/drivers/redis.ts b/src/stores/redis.ts similarity index 52% rename from src/drivers/redis.ts rename to src/stores/redis.ts index f6d02a6..3be9ba4 100644 --- a/src/drivers/redis.ts +++ b/src/stores/redis.ts @@ -9,24 +9,22 @@ import string from '@poppinss/utils/string' import { MessageBuilder } from '@adonisjs/core/helpers' -import type { RedisService } from '@adonisjs/redis/types' +import type { Connection } from '@adonisjs/redis/types' -import type { SessionDriverContract, RedisDriverConfig, SessionData } from '../types/main.js' import debug from '../debug.js' +import type { SessionStoreContract, SessionData } from '../types.js' /** - * File driver to read/write session to filesystem + * File store to read/write session to filesystem */ -export class RedisDriver implements SessionDriverContract { - #config: RedisDriverConfig - #redis: RedisService +export class RedisStore implements SessionStoreContract { + #connection: Connection #ttlSeconds: number - constructor(redis: RedisService, config: RedisDriverConfig, age: string | number) { - this.#config = config - this.#redis = redis + constructor(connection: Connection, age: string | number) { + this.#connection = connection this.#ttlSeconds = string.seconds.parse(age) - debug('initiating redis driver %O', this.#config) + debug('initiating redis store') } /** @@ -34,9 +32,9 @@ export class RedisDriver implements SessionDriverContract { * missing. */ async read(sessionId: string): Promise { - debug('redis driver: reading session data %s', sessionId) + debug('redis store: reading session data %s', sessionId) - const contents = await this.#redis.connection(this.#config.connection).get(sessionId) + const contents = await this.#connection.get(sessionId) if (!contents) { return null } @@ -56,27 +54,25 @@ export class RedisDriver implements SessionDriverContract { * Write session values to a file */ async write(sessionId: string, values: Object): Promise { - debug('redis driver: writing session data %s, %O', sessionId, values) + debug('redis store: writing session data %s, %O', sessionId, values) const message = new MessageBuilder().build(values, undefined, sessionId) - await this.#redis - .connection(this.#config.connection) - .setex(sessionId, this.#ttlSeconds, message) + await this.#connection.setex(sessionId, this.#ttlSeconds, message) } /** * Cleanup session file by removing it */ async destroy(sessionId: string): Promise { - debug('redis driver: destroying session data %s', sessionId) - await this.#redis.connection(this.#config.connection).del(sessionId) + debug('redis store: destroying session data %s', sessionId) + await this.#connection.del(sessionId) } /** * Updates the value expiry */ async touch(sessionId: string): Promise { - debug('redis driver: touching session data %s', sessionId) - await this.#redis.connection(this.#config.connection).expire(sessionId, this.#ttlSeconds) + debug('redis store: touching session data %s', sessionId) + await this.#connection.expire(sessionId, this.#ttlSeconds) } } diff --git a/src/types/main.ts b/src/types.ts similarity index 65% rename from src/types/main.ts rename to src/types.ts index de8e3ce..dc747c3 100644 --- a/src/types/main.ts +++ b/src/types.ts @@ -7,15 +7,10 @@ * file that was distributed with this source code. */ -import type { HttpContext } from '@adonisjs/core/http' +import { HttpContext } from '@adonisjs/core/http' import { RedisConnections } from '@adonisjs/redis/types' import type { CookieOptions } from '@adonisjs/core/types/http' -import type { FileDriver } from '../drivers/file.js' -import type { RedisDriver } from '../drivers/redis.js' -import type { MemoryDriver } from '../drivers/memory.js' -import type { CookieDriver } from '../drivers/cookie.js' - /** * The values allowed by the `session.put` method */ @@ -23,9 +18,9 @@ export type AllowedSessionValues = string | boolean | number | object | Date | A export type SessionData = Record /** - * Session drivers must implement the session driver contract. + * Session stores must implement the session store contract. */ -export interface SessionDriverContract { +export interface SessionStoreContract { /** * The read method is used to read the data from the persistence * store and return it back as an object @@ -52,7 +47,8 @@ export interface SessionDriverContract { } /** - * Shape of session config. + * Base configuration for managing sessions without + * stores. */ export interface SessionConfig { /** @@ -60,11 +56,6 @@ export interface SessionConfig { */ enabled: boolean - /** - * The drivers to use - */ - driver: keyof SessionDriversList - /** * The name of the cookie for storing the session id. */ @@ -98,34 +89,23 @@ export interface SessionConfig { } /** - * Configuration used by the file driver. + * Configuration used by the file store. */ -export type FileDriverConfig = { +export type FileStoreConfig = { location: string } /** - * Configuration used by the redis driver. + * Configuration used by the redis store. */ -export type RedisDriverConfig = { +export type RedisStoreConfig = { connection: keyof RedisConnections } /** - * Extending session config with the drivers config - */ -export interface SessionConfig { - file?: FileDriverConfig - redis?: RedisDriverConfig -} - -/** - * List of the session drivers. The list can be extended using - * declaration merging + * Factory function to instantiate session store */ -export interface SessionDriversList { - file: (config: SessionConfig, ctx: HttpContext) => FileDriver - cookie: (config: SessionConfig, ctx: HttpContext) => CookieDriver - redis: (config: SessionConfig, ctx: HttpContext) => RedisDriver - memory: (config: SessionConfig, ctx?: HttpContext) => MemoryDriver -} +export type SessionStoreFactory = ( + ctx: HttpContext, + sessionConfig: SessionConfig +) => SessionStoreContract diff --git a/src/types/extended.ts b/src/types/extended.ts deleted file mode 100644 index 02e7a58..0000000 --- a/src/types/extended.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * @adonisjs/session - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import type { Session } from '../session.js' - -/** - * Events emitted by the session class - */ -declare module '@adonisjs/core/types' { - interface EventsList { - 'session:initiated': { session: Session } - 'session:committed': { session: Session } - 'session:migrated': { fromSessionId: string; toSessionId: string; session: Session } - } -} diff --git a/src/store.ts b/src/values_store.ts similarity index 95% rename from src/store.ts rename to src/values_store.ts index 660a332..ac014d7 100644 --- a/src/store.ts +++ b/src/values_store.ts @@ -9,13 +9,12 @@ import lodash from '@poppinss/utils/lodash' import { RuntimeException } from '@poppinss/utils' - -import type { AllowedSessionValues, SessionData } from './types/main.js' +import type { AllowedSessionValues, SessionData } from './types.js' /** * Readonly session store */ -export class ReadOnlyStore { +export class ReadOnlyValuesStore { /** * Underlying store values */ @@ -90,7 +89,7 @@ export class ReadOnlyStore { * Session store encapsulates the session data and offers a * declarative API to mutate it. */ -export class Store extends ReadOnlyStore { +export class ValuesStore extends ReadOnlyValuesStore { /** * A boolean to know if store has been * modified diff --git a/stubs/config.stub b/stubs/config.stub index 6cc5644..1a7a9d4 100644 --- a/stubs/config.stub +++ b/stubs/config.stub @@ -2,8 +2,8 @@ exports({ to: app.configPath('session.ts') }) }}} import env from '#start/env' -import app from '@adonisjs/core/services/app'; -import { defineConfig } from '@adonisjs/session' +import app from '@adonisjs/core/services/app' +import { defineConfig, stores } from '@adonisjs/session' export default defineConfig({ enabled: true, @@ -22,12 +22,9 @@ export default defineConfig({ age: '2h', /** - * The driver to use. Make sure to validate the environment - * variable in order to infer the driver name without any - * errors. + * Configuration for session cookie and the + * cookie store */ - driver: env.get('SESSION_DRIVER'), - cookie: { path: '/', httpOnly: true, @@ -36,16 +33,17 @@ export default defineConfig({ }, /** - * Settings for the file driver + * The store to use. Make sure to validate the environment + * variable in order to infer the store name without any + * errors. */ - // file: { - // location: app.tmpPath('sessions'), - // }, + store: env.get('SESSION_DRIVER'), /** - * Settings for the redis driver + * List of configured stores. Refer documentation to see + * list of available stores and their config. */ - // redis: { - // connection: 'main' - // }, + stores: { + cookie: stores.cookie(), + } }) diff --git a/test_helpers/index.ts b/test_helpers/index.ts index a717437..03420f0 100644 --- a/test_helpers/index.ts +++ b/test_helpers/index.ts @@ -7,8 +7,8 @@ * file that was distributed with this source code. */ -import type { Test } from '@japa/runner/core' import { getActiveTest } from '@japa/runner' +import type { Test } from '@japa/runner/core' import { browserClient } from '@japa/browser-client' import { pluginAdonisJS } from '@japa/plugin-adonisjs' import { ApiClient, apiClient } from '@japa/api-client' diff --git a/tests/concurrent_session.spec.ts b/tests/concurrent_session.spec.ts index 0844c55..68d0b71 100644 --- a/tests/concurrent_session.spec.ts +++ b/tests/concurrent_session.spec.ts @@ -20,15 +20,14 @@ import { IncomingMessage, ServerResponse } from 'node:http' import { RedisManagerFactory } from '@adonisjs/redis/factories' import { CookieClient, HttpContext } from '@adonisjs/core/http' import { EncryptionFactory } from '@adonisjs/core/factories/encryption' -import { RedisService, InferConnections } from '@adonisjs/redis/types' import { HttpContextFactory, RequestFactory, ResponseFactory } from '@adonisjs/core/factories/http' import { Session } from '../src/session.js' -import { FileDriver } from '../src/drivers/file.js' +import { FileStore } from '../src/stores/file.js' +import { RedisStore } from '../src/stores/redis.js' import { httpServer } from '../test_helpers/index.js' -import { CookieDriver } from '../src/drivers/cookie.js' -import type { SessionConfig, SessionDriverContract } from '../src/types/main.js' -import { RedisDriver } from '../src/drivers/redis.js' +import { CookieStore } from '../src/stores/cookie.js' +import type { SessionConfig, SessionStoreContract } from '../src/types.js' const app = new AppFactory().create(new URL('./', import.meta.url), () => {}) const emitter = new Emitter(app) @@ -39,7 +38,6 @@ const sessionConfig: SessionConfig = { age: '2 hours', clearWithBrowser: false, cookieName: 'adonis_session', - driver: 'cookie', cookie: {}, } @@ -52,10 +50,8 @@ const redisConfig = defineConfig({ }, }, }) -const redis = new RedisManagerFactory(redisConfig).create() as RedisService -declare module '@adonisjs/redis/types' { - interface RedisConnections extends InferConnections {} -} + +const redis = new RedisManagerFactory(redisConfig).create() /** * Re-usable request handler that creates different session scanerios @@ -64,14 +60,14 @@ declare module '@adonisjs/redis/types' { async function requestHandler( req: IncomingMessage, res: ServerResponse, - driver: (ctx: HttpContext) => SessionDriverContract + driver: (ctx: HttpContext) => SessionStoreContract ) { try { const request = new RequestFactory().merge({ req, res, encryption }).create() const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const session = new Session(sessionConfig, driver(ctx), emitter, ctx) + const session = new Session(sessionConfig, driver, emitter, ctx) await session.initiate(false) if (req.url === '/read-data') { @@ -118,7 +114,7 @@ test.group('Concurrency | cookie driver', () => { let sessionId = cuid() const server = httpServer.create((req, res) => - requestHandler(req, res, (ctx) => new CookieDriver(sessionConfig.cookie, ctx)) + requestHandler(req, res, (ctx) => new CookieStore(sessionConfig.cookie, ctx)) ) const sessionIdCookie = `adonis_session=${cookieClient.sign('adonis_session', sessionId)}` @@ -151,7 +147,7 @@ test.group('Concurrency | cookie driver', () => { let sessionId = cuid() const server = httpServer.create((req, res) => - requestHandler(req, res, (ctx) => new CookieDriver(sessionConfig.cookie, ctx)) + requestHandler(req, res, (ctx) => new CookieStore(sessionConfig.cookie, ctx)) ) const sessionIdCookie = `adonis_session=${cookieClient.sign('adonis_session', sessionId)}` @@ -188,7 +184,7 @@ test.group('Concurrency | cookie driver', () => { let sessionId = cuid() const server = httpServer.create((req, res) => - requestHandler(req, res, (ctx) => new CookieDriver(sessionConfig.cookie, ctx)) + requestHandler(req, res, (ctx) => new CookieStore(sessionConfig.cookie, ctx)) ) const sessionIdCookie = `adonis_session=${cookieClient.sign('adonis_session', sessionId)}` @@ -231,7 +227,7 @@ test.group('Concurrency | file driver', () => { test('concurrently read and read slowly', async ({ fs, assert }) => { let sessionId = cuid() - const fileDriver = new FileDriver({ location: fs.basePath }, sessionConfig.age) + const fileDriver = new FileStore({ location: fs.basePath }, sessionConfig.age) await fileDriver.write(sessionId, { age: 22 }) const server = httpServer.create((req, res) => requestHandler(req, res, () => fileDriver)) @@ -257,7 +253,7 @@ test.group('Concurrency | file driver', () => { test('concurrently write and read slowly', async ({ fs, assert }) => { let sessionId = cuid() - const fileDriver = new FileDriver({ location: fs.basePath }, sessionConfig.age) + const fileDriver = new FileStore({ location: fs.basePath }, sessionConfig.age) await fileDriver.write(sessionId, { age: 22 }) const server = httpServer.create((req, res) => requestHandler(req, res, () => fileDriver)) @@ -280,7 +276,7 @@ test.group('Concurrency | file driver', () => { test('HAS RACE CONDITON: concurrently write and write slowly', async ({ fs, assert }) => { let sessionId = cuid() - const fileDriver = new FileDriver({ location: fs.basePath }, sessionConfig.age) + const fileDriver = new FileStore({ location: fs.basePath }, sessionConfig.age) await fileDriver.write(sessionId, { age: 22 }) const server = httpServer.create((req, res) => requestHandler(req, res, () => fileDriver)) @@ -313,7 +309,7 @@ test.group('Concurrency | redis driver', (group) => { await redisDriver.destroy(sessionId) }) - const redisDriver = new RedisDriver(redis, { connection: 'main' }, sessionConfig.age) + const redisDriver = new RedisStore(redis.connection('main'), sessionConfig.age) await redisDriver.write(sessionId, { age: 22 }) const server = httpServer.create((req, res) => requestHandler(req, res, () => redisDriver)) @@ -338,7 +334,7 @@ test.group('Concurrency | redis driver', (group) => { await redisDriver.destroy(sessionId) }) - const redisDriver = new RedisDriver(redis, { connection: 'main' }, sessionConfig.age) + const redisDriver = new RedisStore(redis.connection('main'), sessionConfig.age) await redisDriver.write(sessionId, { age: 22 }) const server = httpServer.create((req, res) => requestHandler(req, res, () => redisDriver)) @@ -358,7 +354,7 @@ test.group('Concurrency | redis driver', (group) => { await redisDriver.destroy(sessionId) }) - const redisDriver = new RedisDriver(redis, { connection: 'main' }, sessionConfig.age) + const redisDriver = new RedisStore(redis.connection('main'), sessionConfig.age) await redisDriver.write(sessionId, { age: 22 }) const server = httpServer.create((req, res) => requestHandler(req, res, () => redisDriver)) diff --git a/tests/configure.spec.ts b/tests/configure.spec.ts index a796554..8a429fe 100644 --- a/tests/configure.spec.ts +++ b/tests/configure.spec.ts @@ -57,5 +57,5 @@ test.group('Configure', (group) => { 'start/env.ts', `SESSION_DRIVER: Env.schema.enum(['cookie', 'redis', 'file', 'memory'] as const)` ) - }).timeout(10000) + }).timeout(60 * 1000) }) diff --git a/tests/define_config.spec.ts b/tests/define_config.spec.ts index b992674..f55f096 100644 --- a/tests/define_config.spec.ts +++ b/tests/define_config.spec.ts @@ -8,24 +8,161 @@ */ import { test } from '@japa/runner' -import { defineConfig } from '../src/define_config.js' +import { fileURLToPath } from 'node:url' +import { AppFactory } from '@adonisjs/core/factories/app' +import { defineConfig as redisConfig } from '@adonisjs/redis' +import type { ApplicationService } from '@adonisjs/core/types' +import { HttpContextFactory } from '@adonisjs/core/factories/http' + +import { FileStore } from '../src/stores/file.js' +import { RedisStore } from '../src/stores/redis.js' +import { CookieStore } from '../src/stores/cookie.js' +import { defineConfig, stores } from '../src/define_config.js' + +const BASE_URL = new URL('./', import.meta.url) +const app = new AppFactory().create(BASE_URL, () => {}) as ApplicationService test.group('Define config', () => { - test('throw error when driver is not defined', () => { - defineConfig({}) - }).throws('Missing "driver" property inside the session config') + test('throw error when store is not defined', async () => { + await defineConfig({} as any).resolver(app) + }).throws('Missing "store" property inside the session config') + + test('define maxAge when clearWithBrowser is not defined', async ({ assert }) => { + const config = await defineConfig({ store: 'memory', stores: {} }).resolver(app) + assert.equal(config.cookie.maxAge, 7200) + }) + + test('define maxAge when clearWithBrowser is not enabled', async ({ assert }) => { + const config = await defineConfig({ + clearWithBrowser: false, + store: 'memory', + stores: {}, + }).resolver(app) + assert.equal(config.cookie.maxAge, 7200) + }) + + test('define maxAge when clearWithBrowser is enabled', async ({ assert }) => { + const config = await defineConfig({ + clearWithBrowser: true, + store: 'memory', + stores: {}, + }).resolver(app) + + assert.isUndefined(config.cookie.maxAge) + }) + + test('transform config with no stores', async ({ assert }) => { + const config = await defineConfig({ store: 'memory', stores: {} }).resolver(app) + assert.snapshot(config).matchInline(` + { + "age": "2h", + "clearWithBrowser": false, + "cookie": { + "maxAge": 7200, + }, + "cookieName": "adonis_session", + "enabled": true, + "store": "memory", + "stores": { + "memory": [Function], + }, + } + `) + }) + + test('transform config with file store', async ({ assert }) => { + const config = await defineConfig({ + store: 'file', + stores: { + file: stores.file({ location: fileURLToPath(new URL('./sessions', BASE_URL)) }), + }, + }).resolver(app) + + assert.snapshot(config).matchInline(` + { + "age": "2h", + "clearWithBrowser": false, + "cookie": { + "maxAge": 7200, + }, + "cookieName": "adonis_session", + "enabled": true, + "store": "file", + "stores": { + "file": [Function], + "memory": [Function], + }, + } + `) - test('define maxAge when clearWithBrowser is not enabled', ({ assert }) => { - assert.equal(defineConfig({ driver: 'cookie' }).cookie.maxAge, 7200) - assert.equal(defineConfig({ driver: 'cookie', clearWithBrowser: false }).cookie.maxAge, 7200) + const ctx = new HttpContextFactory().create() + assert.instanceOf(config.stores.file(ctx, config), FileStore) }) - test('do not define maxAge when clearWithBrowser is true', ({ assert }) => { - assert.isUndefined(defineConfig({ driver: 'cookie', clearWithBrowser: true }).cookie.maxAge) + test('transform config with redis store', async ({ assert }) => { + const appForRedis = new AppFactory().create(BASE_URL, () => {}) as ApplicationService + appForRedis.rcContents({ + providers: [ + () => import('@adonisjs/core/providers/app_provider'), + () => import('@adonisjs/redis/redis_provider'), + ], + }) + appForRedis.useConfig({ + logger: { + default: 'main', + loggers: { + main: {}, + }, + }, + redis: redisConfig({ + connection: 'main', + connections: { + main: {}, + }, + }), + }) + await appForRedis.init() + await appForRedis.boot() + + const config = await defineConfig({ + store: 'redis', + stores: { + redis: stores.redis({ + connection: 'main', + } as any), + }, + }).resolver(appForRedis) + + assert.snapshot(config).matchInline(` + { + "age": "2h", + "clearWithBrowser": false, + "cookie": { + "maxAge": 7200, + }, + "cookieName": "adonis_session", + "enabled": true, + "store": "redis", + "stores": { + "memory": [Function], + "redis": [Function], + }, + } + `) + + const ctx = new HttpContextFactory().create() + assert.instanceOf(config.stores.redis(ctx, config), RedisStore) }) - test('normalize config', ({ assert }) => { - assert.snapshot(defineConfig({ driver: 'cookie' })).matchInline(` + test('transform config with cookie store', async ({ assert }) => { + const config = await defineConfig({ + store: 'cookie', + stores: { + cookie: stores.cookie(), + }, + }).resolver(app) + + assert.snapshot(config).matchInline(` { "age": "2h", "clearWithBrowser": false, @@ -33,9 +170,16 @@ test.group('Define config', () => { "maxAge": 7200, }, "cookieName": "adonis_session", - "driver": "cookie", "enabled": true, + "store": "cookie", + "stores": { + "cookie": [Function], + "memory": [Function], + }, } `) + + const ctx = new HttpContextFactory().create() + assert.instanceOf(config.stores.cookie(ctx, config), CookieStore) }) }) diff --git a/tests/drivers_collection.spec.ts b/tests/drivers_collection.spec.ts deleted file mode 100644 index 89d3861..0000000 --- a/tests/drivers_collection.spec.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * @adonisjs/session - * - * (c) AdonisJS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { test } from '@japa/runner' -import { ApplicationService } from '@adonisjs/core/types' -import { AppFactory } from '@adonisjs/core/factories/app' - -import { MemoryDriver } from '../src/drivers/memory.js' -import { registerSessionDriver } from '../src/helpers.js' -import sessionDriversList from '../src/drivers_collection.js' - -test.group('Drivers collection', () => { - test('raise error when trying to access a non-existing driver', () => { - sessionDriversList.create('cookie', {} as any, {} as any) - }).throws('Unknown session driver "cookie". Make sure the driver is registered') - - test('create driver instance when exists', async ({ assert }) => { - const app = new AppFactory().create( - new URL('./', import.meta.url), - () => {} - ) as ApplicationService - - await registerSessionDriver(app, 'memory') - assert.instanceOf(sessionDriversList.create('memory', {} as any, {} as any), MemoryDriver) - }) -}) diff --git a/tests/plugins/api_client.spec.ts b/tests/plugins/api_client.spec.ts index 082c10a..27b1842 100644 --- a/tests/plugins/api_client.spec.ts +++ b/tests/plugins/api_client.spec.ts @@ -16,10 +16,9 @@ import { EncryptionFactory } from '@adonisjs/core/factories/encryption' import { HttpContextFactory, RequestFactory, ResponseFactory } from '@adonisjs/core/factories/http' import { Session } from '../../src/session.js' -import { SessionConfig } from '../../src/types/main.js' +import { SessionConfig } from '../../src/types.js' import { defineConfig } from '../../src/define_config.js' -import { MemoryDriver } from '../../src/drivers/memory.js' -import sessionDriversList from '../../src/drivers_collection.js' +import { MemoryStore } from '../../src/stores/memory.js' import { httpServer, runJapaTest } from '../../test_helpers/index.js' const app = new AppFactory().create(new URL('./', import.meta.url), () => {}) as ApplicationService @@ -32,7 +31,6 @@ const sessionConfig: SessionConfig = { age: '2 hours', clearWithBrowser: false, cookieName: 'adonis_session', - driver: 'cookie', cookie: {}, } @@ -40,7 +38,8 @@ test.group('Api client', (group) => { group.setup(async () => { app.useConfig({ session: defineConfig({ - driver: 'memory', + store: 'memory', + stores: {}, }), }) await app.init() @@ -54,12 +53,7 @@ test.group('Api client', (group) => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const session = new Session( - sessionConfig, - sessionDriversList.create('memory', sessionConfig), - emitter, - ctx - ) + const session = new Session(sessionConfig, () => new MemoryStore(), emitter, ctx) await session.initiate(false) assert.deepEqual(session.all(), { username: 'virk' }) @@ -74,7 +68,7 @@ test.group('Api client', (group) => { await runJapaTest(app, async ({ client }) => { await client.get(url).withSession({ username: 'virk' }) - assert.lengthOf(MemoryDriver.sessions, 0) + assert.lengthOf(MemoryStore.sessions, 0) }) }) @@ -84,12 +78,7 @@ test.group('Api client', (group) => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const session = new Session( - sessionConfig, - sessionDriversList.create('memory', sessionConfig), - emitter, - ctx - ) + const session = new Session(sessionConfig, () => new MemoryStore(), emitter, ctx) await session.initiate(false) assert.deepEqual(session.flashMessages.all(), { username: 'virk' }) @@ -104,7 +93,7 @@ test.group('Api client', (group) => { await runJapaTest(app, async ({ client }) => { await client.get(url).withFlashMessages({ username: 'virk' }) - assert.lengthOf(MemoryDriver.sessions, 0) + assert.lengthOf(MemoryStore.sessions, 0) }) }) @@ -114,12 +103,7 @@ test.group('Api client', (group) => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const session = new Session( - sessionConfig, - sessionDriversList.create('memory', sessionConfig), - emitter, - ctx - ) + const session = new Session(sessionConfig, () => new MemoryStore(), emitter, ctx) await session.initiate(false) session.put('name', 'virk') @@ -135,7 +119,7 @@ test.group('Api client', (group) => { await runJapaTest(app, async ({ client }) => { const response = await client.get(url) assert.deepEqual(response.session(), { name: 'virk' }) - assert.lengthOf(MemoryDriver.sessions, 0) + assert.lengthOf(MemoryStore.sessions, 0) }) }) @@ -145,12 +129,7 @@ test.group('Api client', (group) => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const session = new Session( - sessionConfig, - sessionDriversList.create('memory', sessionConfig), - emitter, - ctx - ) + const session = new Session(sessionConfig, () => new MemoryStore(), emitter, ctx) await session.initiate(false) session.flash('name', 'virk') @@ -166,7 +145,7 @@ test.group('Api client', (group) => { await runJapaTest(app, async ({ client }) => { const response = await client.get(url) assert.deepEqual(response.flashMessages(), { name: 'virk' }) - assert.lengthOf(MemoryDriver.sessions, 0) + assert.lengthOf(MemoryStore.sessions, 0) }) }) @@ -176,12 +155,7 @@ test.group('Api client', (group) => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const session = new Session( - sessionConfig, - sessionDriversList.create('memory', sessionConfig), - emitter, - ctx - ) + const session = new Session(sessionConfig, () => new MemoryStore(), emitter, ctx) await session.initiate(false) session.put('name', 'virk') @@ -201,7 +175,7 @@ test.group('Api client', (group) => { await runJapaTest(app, async ({ client }) => { const response = await client.get(url) - assert.lengthOf(MemoryDriver.sessions, 0) + assert.lengthOf(MemoryStore.sessions, 0) response.assertSession('name') response.assertSession('name', 'virk') diff --git a/tests/plugins/browser_client.spec.ts b/tests/plugins/browser_client.spec.ts index dfd3011..d5b72fb 100644 --- a/tests/plugins/browser_client.spec.ts +++ b/tests/plugins/browser_client.spec.ts @@ -16,10 +16,9 @@ import { EncryptionFactory } from '@adonisjs/core/factories/encryption' import { HttpContextFactory, RequestFactory, ResponseFactory } from '@adonisjs/core/factories/http' import { Session } from '../../src/session.js' -import { SessionConfig } from '../../src/types/main.js' +import { SessionConfig } from '../../src/types.js' +import { MemoryStore } from '../../src/stores/memory.js' import { defineConfig } from '../../src/define_config.js' -import { MemoryDriver } from '../../src/drivers/memory.js' -import sessionDriversList from '../../src/drivers_collection.js' import { httpServer, runJapaTest } from '../../test_helpers/index.js' const app = new AppFactory().create(new URL('./', import.meta.url), () => {}) as ApplicationService @@ -32,7 +31,6 @@ const sessionConfig: SessionConfig = { age: '2 hours', clearWithBrowser: false, cookieName: 'adonis_session', - driver: 'cookie', cookie: {}, } @@ -40,7 +38,8 @@ test.group('Browser client', (group) => { group.setup(async () => { app.useConfig({ session: defineConfig({ - driver: 'memory', + store: 'memory', + stores: {}, }), }) await app.init() @@ -54,12 +53,7 @@ test.group('Browser client', (group) => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const session = new Session( - sessionConfig, - sessionDriversList.create('memory', sessionConfig), - emitter, - ctx - ) + const session = new Session(sessionConfig, () => new MemoryStore(), emitter, ctx) await session.initiate(false) assert.deepEqual(session.all(), { username: 'virk' }) @@ -77,9 +71,9 @@ test.group('Browser client', (group) => { await browserContext.setSession({ username: 'virk' }) await visit(url) - assert.lengthOf(MemoryDriver.sessions, 1) + assert.lengthOf(MemoryStore.sessions, 1) await browserContext.close() - assert.lengthOf(MemoryDriver.sessions, 0) + assert.lengthOf(MemoryStore.sessions, 0) }) }) @@ -89,12 +83,7 @@ test.group('Browser client', (group) => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const session = new Session( - sessionConfig, - sessionDriversList.create('memory', sessionConfig), - emitter, - ctx - ) + const session = new Session(sessionConfig, () => new MemoryStore(), emitter, ctx) await session.initiate(false) assert.deepEqual(session.flashMessages.all(), { username: 'virk' }) @@ -117,9 +106,9 @@ test.group('Browser client', (group) => { * reading the flash messages, the store * should be empty post visit */ - assert.lengthOf(MemoryDriver.sessions, 0) + assert.lengthOf(MemoryStore.sessions, 0) await browserContext.close() - assert.lengthOf(MemoryDriver.sessions, 0) + assert.lengthOf(MemoryStore.sessions, 0) }) }) @@ -129,12 +118,7 @@ test.group('Browser client', (group) => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const session = new Session( - sessionConfig, - sessionDriversList.create('memory', sessionConfig), - emitter, - ctx - ) + const session = new Session(sessionConfig, () => new MemoryStore(), emitter, ctx) await session.initiate(false) session.put('name', 'virk') @@ -154,9 +138,9 @@ test.group('Browser client', (group) => { assert.deepEqual(await browserContext.getSession(), { name: 'virk' }) - assert.lengthOf(MemoryDriver.sessions, 1) + assert.lengthOf(MemoryStore.sessions, 1) await browserContext.close() - assert.lengthOf(MemoryDriver.sessions, 0) + assert.lengthOf(MemoryStore.sessions, 0) }) }) @@ -166,12 +150,7 @@ test.group('Browser client', (group) => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const session = new Session( - sessionConfig, - sessionDriversList.create('memory', sessionConfig), - emitter, - ctx - ) + const session = new Session(sessionConfig, () => new MemoryStore(), emitter, ctx) await session.initiate(false) session.flash('name', 'virk') @@ -191,9 +170,9 @@ test.group('Browser client', (group) => { assert.deepEqual(await browserContext.getFlashMessages(), { name: 'virk' }) - assert.lengthOf(MemoryDriver.sessions, 1) + assert.lengthOf(MemoryStore.sessions, 1) await browserContext.close() - assert.lengthOf(MemoryDriver.sessions, 0) + assert.lengthOf(MemoryStore.sessions, 0) }) }) }) diff --git a/tests/session.spec.ts b/tests/session.spec.ts index 4fe92e5..d0f6338 100644 --- a/tests/session.spec.ts +++ b/tests/session.spec.ts @@ -29,9 +29,9 @@ import { import { Session } from '../src/session.js' import { httpServer } from '../test_helpers/index.js' -import { CookieDriver } from '../src/drivers/cookie.js' -import type { SessionConfig } from '../src/types/main.js' +import { CookieStore } from '../src/stores/cookie.js' import SessionProvider from '../providers/session_provider.js' +import type { SessionConfig, SessionStoreFactory } from '../src/types.js' const app = new AppFactory().create(new URL('./', import.meta.url), () => {}) as ApplicationService const emitter = new Emitter(app) @@ -42,9 +42,11 @@ const sessionConfig: SessionConfig = { age: '2 hours', clearWithBrowser: false, cookieName: 'adonis_session', - driver: 'cookie', cookie: {}, } +const cookieDriver: SessionStoreFactory = (ctx, config) => { + return new CookieStore(config.cookie, ctx) +} test.group('Session', (group) => { group.setup(async () => { @@ -54,7 +56,6 @@ test.group('Session', (group) => { app.container.singleton('router', () => router) await new EdgeServiceProvider(app).boot() - await new SessionProvider(app).boot() }) test('do not define session id cookie when not initiated', async ({ assert }) => { @@ -63,8 +64,7 @@ test.group('Session', (group) => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new CookieDriver(sessionConfig.cookie, ctx) - const session = new Session(sessionConfig, driver, emitter, ctx) + const session = new Session(sessionConfig, cookieDriver, emitter, ctx) assert.isFalse(session.initiated) @@ -85,8 +85,7 @@ test.group('Session', (group) => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new CookieDriver(sessionConfig.cookie, ctx) - const session = new Session(sessionConfig, driver, emitter, ctx) + const session = new Session(sessionConfig, cookieDriver, emitter, ctx) await session.initiate(false) assert.isTrue(session.fresh) @@ -107,8 +106,7 @@ test.group('Session', (group) => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new CookieDriver(sessionConfig.cookie, ctx) - const session = new Session(sessionConfig, driver, emitter, ctx) + const session = new Session(sessionConfig, cookieDriver, emitter, ctx) await session.initiate(false) assert.isTrue(session.fresh) @@ -132,8 +130,7 @@ test.group('Session', (group) => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new CookieDriver(sessionConfig.cookie, ctx) - const session = new Session(sessionConfig, driver, emitter, ctx) + const session = new Session(sessionConfig, cookieDriver, emitter, ctx) await session.initiate(false) session.put('username', 'virk') @@ -162,8 +159,7 @@ test.group('Session', (group) => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new CookieDriver(sessionConfig.cookie, ctx) - const session = new Session(sessionConfig, driver, emitter, ctx) + const session = new Session(sessionConfig, cookieDriver, emitter, ctx) await session.initiate(false) session.put('username', 'virk') @@ -201,8 +197,7 @@ test.group('Session', (group) => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new CookieDriver(sessionConfig.cookie, ctx) - const session = new Session(sessionConfig, driver, emitter, ctx) + const session = new Session(sessionConfig, cookieDriver, emitter, ctx) await session.initiate(false) session.forget('age') @@ -232,8 +227,7 @@ test.group('Session', (group) => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new CookieDriver(sessionConfig.cookie, ctx) - const session = new Session(sessionConfig, driver, emitter, ctx) + const session = new Session(sessionConfig, cookieDriver, emitter, ctx) await session.initiate(false) assert.equal(session.pull('age'), 22) @@ -263,8 +257,7 @@ test.group('Session', (group) => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new CookieDriver(sessionConfig.cookie, ctx) - const session = new Session(sessionConfig, driver, emitter, ctx) + const session = new Session(sessionConfig, cookieDriver, emitter, ctx) await session.initiate(false) session.increment('visits') @@ -291,8 +284,7 @@ test.group('Session', (group) => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new CookieDriver(sessionConfig.cookie, ctx) - const session = new Session(sessionConfig, driver, emitter, ctx) + const session = new Session(sessionConfig, cookieDriver, emitter, ctx) await session.initiate(false) session.decrement('visits') @@ -319,8 +311,7 @@ test.group('Session', (group) => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new CookieDriver(sessionConfig.cookie, ctx) - const session = new Session(sessionConfig, driver, emitter, ctx) + const session = new Session(sessionConfig, cookieDriver, emitter, ctx) await session.initiate(false) await session.commit() @@ -352,8 +343,7 @@ test.group('Session', (group) => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new CookieDriver(sessionConfig.cookie, ctx) - const session = new Session(sessionConfig, driver, emitter, ctx) + const session = new Session(sessionConfig, cookieDriver, emitter, ctx) await session.initiate(false) session.clear() @@ -377,8 +367,7 @@ test.group('Session', (group) => { test('throw error when trying to read from uninitiated store', async () => { const ctx = new HttpContextFactory().create() - const driver = new CookieDriver(sessionConfig.cookie, ctx) - const session = new Session(sessionConfig, driver, emitter, ctx) + const session = new Session(sessionConfig, cookieDriver, emitter, ctx) session.get('username') }).throws( 'Session store has not been initiated. Make sure you have registered the session middleware' @@ -386,8 +375,7 @@ test.group('Session', (group) => { test('throw error when trying to write to a read only store', async ({ assert }) => { const ctx = new HttpContextFactory().create() - const driver = new CookieDriver(sessionConfig.cookie, ctx) - const session = new Session(sessionConfig, driver, emitter, ctx) + const session = new Session(sessionConfig, cookieDriver, emitter, ctx) await session.initiate(true) assert.isUndefined(session.get('username')) @@ -407,8 +395,7 @@ test.group('Session', (group) => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new CookieDriver(sessionConfig.cookie, ctx) - const session = new Session(sessionConfig, driver, emitter, ctx) + const session = new Session(sessionConfig, cookieDriver, emitter, ctx) await session.initiate(false) try { @@ -441,8 +428,7 @@ test.group('Session | Regenerate', () => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new CookieDriver(sessionConfig.cookie, ctx) - const session = new Session(sessionConfig, driver, emitter, ctx) + const session = new Session(sessionConfig, cookieDriver, emitter, ctx) await session.initiate(false) session.regenerate() @@ -466,8 +452,7 @@ test.group('Session | Regenerate', () => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new CookieDriver(sessionConfig.cookie, ctx) - const session = new Session(sessionConfig, driver, emitter, ctx) + const session = new Session(sessionConfig, cookieDriver, emitter, ctx) await session.initiate(false) session.regenerate() @@ -494,8 +479,7 @@ test.group('Session | Regenerate', () => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new CookieDriver(sessionConfig.cookie, ctx) - const session = new Session(sessionConfig, driver, emitter, ctx) + const session = new Session(sessionConfig, cookieDriver, emitter, ctx) await session.initiate(false) session.regenerate() @@ -528,8 +512,7 @@ test.group('Session | Regenerate', () => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new CookieDriver(sessionConfig.cookie, ctx) - const session = new Session(sessionConfig, driver, emitter, ctx) + const session = new Session(sessionConfig, cookieDriver, emitter, ctx) await session.initiate(false) session.put('username', 'virk') @@ -571,8 +554,7 @@ test.group('Session | Regenerate', () => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new CookieDriver(sessionConfig.cookie, ctx) - const session = new Session(sessionConfig, driver, emitter, ctx) + const session = new Session(sessionConfig, cookieDriver, emitter, ctx) await session.initiate(false) session.forget('age') @@ -610,8 +592,7 @@ test.group('Session | Regenerate', () => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new CookieDriver(sessionConfig.cookie, ctx) - const session = new Session(sessionConfig, driver, emitter, ctx) + const session = new Session(sessionConfig, cookieDriver, emitter, ctx) await session.initiate(false) session.regenerate() newSessionId = session.sessionId @@ -661,8 +642,7 @@ test.group('Session | Flash', (group) => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new CookieDriver(sessionConfig.cookie, ctx) - const session = new Session(sessionConfig, driver, emitter, ctx) + const session = new Session(sessionConfig, cookieDriver, emitter, ctx) await session.initiate(false) session.flash('status', 'Task created successfully') @@ -690,8 +670,7 @@ test.group('Session | Flash', (group) => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new CookieDriver(sessionConfig.cookie, ctx) - const session = new Session(sessionConfig, driver, emitter, ctx) + const session = new Session(sessionConfig, cookieDriver, emitter, ctx) await session.initiate(false) session.flash({ status: 'Task created successfully' }) @@ -722,8 +701,7 @@ test.group('Session | Flash', (group) => { username: 'virk', }) - const driver = new CookieDriver(sessionConfig.cookie, ctx) - const session = new Session(sessionConfig, driver, emitter, ctx) + const session = new Session(sessionConfig, cookieDriver, emitter, ctx) await session.initiate(false) session.flash({ status: 'Task created successfully' }) @@ -757,8 +735,7 @@ test.group('Session | Flash', (group) => { age: 22, }) - const driver = new CookieDriver(sessionConfig.cookie, ctx) - const session = new Session(sessionConfig, driver, emitter, ctx) + const session = new Session(sessionConfig, cookieDriver, emitter, ctx) await session.initiate(false) session.flash({ status: 'Task created successfully' }) @@ -795,8 +772,7 @@ test.group('Session | Flash', (group) => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new CookieDriver(sessionConfig.cookie, ctx) - const session = new Session(sessionConfig, driver, emitter, ctx) + const session = new Session(sessionConfig, cookieDriver, emitter, ctx) await session.initiate(false) sessionId = session.sessionId @@ -836,8 +812,7 @@ test.group('Session | Flash', (group) => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new CookieDriver(sessionConfig.cookie, ctx) - const session = new Session(sessionConfig, driver, emitter, ctx) + const session = new Session(sessionConfig, cookieDriver, emitter, ctx) await session.initiate(false) sessionId = session.sessionId @@ -890,8 +865,7 @@ test.group('Session | Flash', (group) => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new CookieDriver(sessionConfig.cookie, ctx) - const session = new Session(sessionConfig, driver, emitter, ctx) + const session = new Session(sessionConfig, cookieDriver, emitter, ctx) await session.initiate(false) sessionId = session.sessionId @@ -941,8 +915,7 @@ test.group('Session | Flash', (group) => { test('throw error when trying to write to flash messages without initialization', async () => { const ctx = new HttpContextFactory().create() - const driver = new CookieDriver(sessionConfig.cookie, ctx) - const session = new Session(sessionConfig, driver, emitter, ctx) + const session = new Session(sessionConfig, cookieDriver, emitter, ctx) session.flash('username', 'virk') }).throws( 'Session store has not been initiated. Make sure you have registered the session middleware' @@ -950,8 +923,7 @@ test.group('Session | Flash', (group) => { test('throw error when trying to write flash messages to a read only store', async () => { const ctx = new HttpContextFactory().create() - const driver = new CookieDriver(sessionConfig.cookie, ctx) - const session = new Session(sessionConfig, driver, emitter, ctx) + const session = new Session(sessionConfig, cookieDriver, emitter, ctx) await session.initiate(true) session.flash('username', 'foo') @@ -965,8 +937,7 @@ test.group('Session | Flash', (group) => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new CookieDriver(sessionConfig.cookie, ctx) - const session = new Session(sessionConfig, driver, emitter, ctx) + const session = new Session(sessionConfig, cookieDriver, emitter, ctx) await session.initiate(false) const errorReporter = new SimpleErrorReporter() @@ -1011,8 +982,7 @@ test.group('Session | Flash', (group) => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new CookieDriver(sessionConfig.cookie, ctx) - const session = new Session(sessionConfig, driver, emitter, ctx) + const session = new Session(sessionConfig, cookieDriver, emitter, ctx) await session.initiate(false) sessionId = session.sessionId @@ -1061,8 +1031,7 @@ test.group('Session | Flash', (group) => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new CookieDriver(sessionConfig.cookie, ctx) - const session = new Session(sessionConfig, driver, emitter, ctx) + const session = new Session(sessionConfig, cookieDriver, emitter, ctx) await session.initiate(false) sessionId = session.sessionId @@ -1112,8 +1081,7 @@ test.group('Session | Flash', (group) => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new CookieDriver(sessionConfig.cookie, ctx) - const session = new Session(sessionConfig, driver, emitter, ctx) + const session = new Session(sessionConfig, cookieDriver, emitter, ctx) await session.initiate(false) response.send(await ctx.view.render('flash_no_errors_messages')) @@ -1149,8 +1117,7 @@ test.group('Session | Flash', (group) => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const driver = new CookieDriver(sessionConfig.cookie, ctx) - const session = new Session(sessionConfig, driver, emitter, ctx) + const session = new Session(sessionConfig, cookieDriver, emitter, ctx) await session.initiate(false) sessionId = session.sessionId diff --git a/tests/session_client.spec.ts b/tests/session_client.spec.ts index a3d7e6b..cf758d9 100644 --- a/tests/session_client.spec.ts +++ b/tests/session_client.spec.ts @@ -9,15 +9,15 @@ import { test } from '@japa/runner' import { SessionClient } from '../src/client.js' -import { MemoryDriver } from '../src/drivers/memory.js' +import { MemoryStore } from '../src/stores/memory.js' test.group('Session Client', (group) => { group.each.teardown(async () => { - MemoryDriver.sessions.clear() + MemoryStore.sessions.clear() }) test('define session data using session id', async ({ assert }) => { - const driver = new MemoryDriver() + const driver = new MemoryStore() const client = new SessionClient(driver) client.merge({ foo: 'bar' }) @@ -33,7 +33,7 @@ test.group('Session Client', (group) => { }) test('load data from the store', async ({ assert }) => { - const driver = new MemoryDriver() + const driver = new MemoryStore() const client = new SessionClient(driver) client.merge({ foo: 'bar' }) @@ -51,7 +51,7 @@ test.group('Session Client', (group) => { }) test('destroy session', async ({ assert }) => { - const driver = new MemoryDriver() + const driver = new MemoryStore() const client = new SessionClient(driver) client.merge({ foo: 'bar' }) diff --git a/tests/session_middleware.spec.ts b/tests/session_middleware.spec.ts index fb6deb3..99636cb 100644 --- a/tests/session_middleware.spec.ts +++ b/tests/session_middleware.spec.ts @@ -15,9 +15,8 @@ import { EncryptionFactory } from '@adonisjs/core/factories/encryption' import { HttpContextFactory, RequestFactory, ResponseFactory } from '@adonisjs/core/factories/http' import { httpServer } from '../test_helpers/index.js' -import { CookieDriver } from '../src/drivers/cookie.js' -import type { SessionConfig } from '../src/types/main.js' -import sessionDriversList from '../src/drivers_collection.js' +import { CookieStore } from '../src/stores/cookie.js' +import type { SessionConfig } from '../src/types.js' import { SessionMiddlewareFactory } from '../factories/session_middleware_factory.js' const encryption = new EncryptionFactory().create() @@ -27,7 +26,6 @@ const sessionConfig: SessionConfig = { age: '2 hours', clearWithBrowser: false, cookieName: 'adonis_session', - driver: 'cookie', cookie: {}, } @@ -42,7 +40,15 @@ test.group('Session middleware', () => { const middleware = await new SessionMiddlewareFactory() .merge({ - config: sessionConfig, + config: Object.assign( + { + store: 'cookie', + stores: { + cookie: () => new CookieStore(sessionConfig.cookie, ctx), + }, + }, + sessionConfig + ), }) .create() @@ -67,8 +73,6 @@ test.group('Session middleware', () => { }) test('do not initiate session when not enabled', async ({ assert }) => { - sessionDriversList.extend('cookie', (config, ctx) => new CookieDriver(config.cookie, ctx)) - const server = httpServer.create(async (req, res) => { const request = new RequestFactory().merge({ req, res, encryption }).create() const response = new ResponseFactory().merge({ req, res, encryption }).create() @@ -76,7 +80,18 @@ test.group('Session middleware', () => { const middleware = await new SessionMiddlewareFactory() .merge({ - config: { ...sessionConfig, enabled: false }, + config: Object.assign( + { + store: 'cookie', + stores: { + cookie: () => new CookieStore(sessionConfig.cookie, ctx), + }, + }, + sessionConfig, + { + enabled: false, + } + ), }) .create() diff --git a/tests/session_provider.spec.ts b/tests/session_provider.spec.ts index eed8e38..aa10a8d 100644 --- a/tests/session_provider.spec.ts +++ b/tests/session_provider.spec.ts @@ -12,7 +12,6 @@ import { IgnitorFactory } from '@adonisjs/core/factories' import { defineConfig } from '../index.js' import SessionMiddleware from '../src/session_middleware.js' -import sessionDriversList from '../src/drivers_collection.js' const BASE_URL = new URL('./tmp/', import.meta.url) const IMPORTER = (filePath: string) => { @@ -22,13 +21,7 @@ const IMPORTER = (filePath: string) => { return import(filePath) } -test.group('Session Provider', (group) => { - group.each.setup(() => { - return () => { - sessionDriversList.list = {} - } - }) - +test.group('Session Provider', () => { test('register session provider', async ({ assert }) => { const ignitor = new IgnitorFactory() .merge({ @@ -41,70 +34,8 @@ test.group('Session Provider', (group) => { .merge({ config: { session: defineConfig({ - driver: 'cookie', - }), - }, - }) - .create(BASE_URL, { - importer: IMPORTER, - }) - - const app = ignitor.createApp('web') - await app.init() - await app.boot() - - assert.instanceOf(await app.container.make(SessionMiddleware), SessionMiddleware) - }) - - test('register cookie driver with the driversCollection', async ({ assert }) => { - const ignitor = new IgnitorFactory() - .merge({ - rcFileContents: { - providers: ['../../providers/session_provider.js'], - }, - }) - .withCoreConfig() - .withCoreProviders() - .merge({ - config: { - session: defineConfig({ - driver: 'cookie', - }), - }, - }) - .create(BASE_URL, { - importer: IMPORTER, - }) - - const app = ignitor.createApp('web') - await app.init() - await app.boot() - - assert.deepEqual(sessionDriversList.list, {}) - assert.instanceOf(await app.container.make(SessionMiddleware), SessionMiddleware) - - assert.property(sessionDriversList.list, 'cookie') - assert.notProperty(sessionDriversList.list, 'file') - assert.notProperty(sessionDriversList.list, 'memory') - assert.notProperty(sessionDriversList.list, 'redis') - }) - - test('register file driver with the driversCollection', async ({ fs, assert }) => { - const ignitor = new IgnitorFactory() - .merge({ - rcFileContents: { - providers: ['../../providers/session_provider.js'], - }, - }) - .withCoreConfig() - .withCoreProviders() - .merge({ - config: { - session: defineConfig({ - driver: 'file', - file: { - location: fs.basePath, - }, + store: 'memory', + stores: {}, }), }, }) @@ -116,81 +47,6 @@ test.group('Session Provider', (group) => { await app.init() await app.boot() - assert.deepEqual(sessionDriversList.list, {}) assert.instanceOf(await app.container.make(SessionMiddleware), SessionMiddleware) - - assert.property(sessionDriversList.list, 'file') - assert.notProperty(sessionDriversList.list, 'cookie') - assert.notProperty(sessionDriversList.list, 'memory') - assert.notProperty(sessionDriversList.list, 'redis') - }) - - test('register redis driver with the driversCollection', async ({ assert }) => { - const ignitor = new IgnitorFactory() - .merge({ - rcFileContents: { - providers: ['../../providers/session_provider.js', '@adonisjs/redis/redis_provider'], - }, - }) - .withCoreConfig() - .withCoreProviders() - .merge({ - config: { - session: defineConfig({ - driver: 'redis', - redis: { - connection: 'main', - }, - }), - }, - }) - .create(BASE_URL, { - importer: IMPORTER, - }) - - const app = ignitor.createApp('web') - await app.init() - await app.boot() - - assert.deepEqual(sessionDriversList.list, {}) - assert.instanceOf(await app.container.make(SessionMiddleware), SessionMiddleware) - - assert.property(sessionDriversList.list, 'redis') - assert.notProperty(sessionDriversList.list, 'cookie') - assert.notProperty(sessionDriversList.list, 'memory') - assert.notProperty(sessionDriversList.list, 'file') - }) - - test('register memory driver with the driversCollection', async ({ assert }) => { - const ignitor = new IgnitorFactory() - .merge({ - rcFileContents: { - providers: ['../../providers/session_provider.js'], - }, - }) - .withCoreConfig() - .withCoreProviders() - .merge({ - config: { - session: defineConfig({ - driver: 'memory', - }), - }, - }) - .create(BASE_URL, { - importer: IMPORTER, - }) - - const app = ignitor.createApp('web') - await app.init() - await app.boot() - - assert.deepEqual(sessionDriversList.list, {}) - assert.instanceOf(await app.container.make(SessionMiddleware), SessionMiddleware) - - assert.property(sessionDriversList.list, 'memory') - assert.notProperty(sessionDriversList.list, 'cookie') - assert.notProperty(sessionDriversList.list, 'redis') - assert.notProperty(sessionDriversList.list, 'file') }) }) diff --git a/tests/drivers/cookie_driver.spec.ts b/tests/stores/cookie_store.spec.ts similarity index 92% rename from tests/drivers/cookie_driver.spec.ts rename to tests/stores/cookie_store.spec.ts index ada21b1..188ddcb 100644 --- a/tests/drivers/cookie_driver.spec.ts +++ b/tests/stores/cookie_store.spec.ts @@ -16,7 +16,7 @@ import { EncryptionFactory } from '@adonisjs/core/factories/encryption' import { HttpContextFactory, RequestFactory, ResponseFactory } from '@adonisjs/core/factories/http' import { httpServer } from '../../test_helpers/index.js' -import { CookieDriver } from '../../src/drivers/cookie.js' +import { CookieStore } from '../../src/stores/cookie.js' const encryption = new EncryptionFactory().create() const cookieClient = new CookieClient(encryption) @@ -25,7 +25,7 @@ const cookieConfig: Partial = { maxAge: '5mins', } -test.group('Cookie driver', () => { +test.group('Cookie store', () => { test('return null when session data cookie does not exists', async ({ assert }) => { const sessionId = '1234' @@ -34,7 +34,7 @@ test.group('Cookie driver', () => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const session = new CookieDriver(cookieConfig, ctx) + const session = new CookieStore(cookieConfig, ctx) const value = session.read(sessionId) response.json(value) response.finish() @@ -53,7 +53,7 @@ test.group('Cookie driver', () => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const session = new CookieDriver(cookieConfig, ctx) + const session = new CookieStore(cookieConfig, ctx) const value = session.read(sessionId) response.json(value) response.finish() @@ -74,7 +74,7 @@ test.group('Cookie driver', () => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const session = new CookieDriver(cookieConfig, ctx) + const session = new CookieStore(cookieConfig, ctx) session.write(sessionId, { visits: 0 }) response.finish() }) @@ -94,7 +94,7 @@ test.group('Cookie driver', () => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const session = new CookieDriver(cookieConfig, ctx) + const session = new CookieStore(cookieConfig, ctx) session.touch(sessionId) response.finish() }) @@ -119,7 +119,7 @@ test.group('Cookie driver', () => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const session = new CookieDriver(cookieConfig, ctx) + const session = new CookieStore(cookieConfig, ctx) response.json(session.read(sessionId)) response.finish() }) @@ -141,7 +141,7 @@ test.group('Cookie driver', () => { const response = new ResponseFactory().merge({ req, res, encryption }).create() const ctx = new HttpContextFactory().merge({ request, response }).create() - const session = new CookieDriver(cookieConfig, ctx) + const session = new CookieStore(cookieConfig, ctx) response.json(session.read(sessionId)) session.destroy(sessionId) response.finish() diff --git a/tests/drivers/file_driver.spec.ts b/tests/stores/file_store.spec.ts similarity index 85% rename from tests/drivers/file_driver.spec.ts rename to tests/stores/file_store.spec.ts index 72ebd52..e9a44ca 100644 --- a/tests/drivers/file_driver.spec.ts +++ b/tests/stores/file_store.spec.ts @@ -12,12 +12,12 @@ import { test } from '@japa/runner' import { stat } from 'node:fs/promises' import { setTimeout } from 'node:timers/promises' -import { FileDriver } from '../../src/drivers/file.js' +import { FileStore } from '../../src/stores/file.js' -test.group('File driver', () => { +test.group('File store', () => { test('do not create file for a new session', async ({ fs, assert }) => { const sessionId = '1234' - const session = new FileDriver({ location: fs.basePath }, '2 hours') + const session = new FileStore({ location: fs.basePath }, '2 hours') const value = await session.read(sessionId) assert.isNull(value) @@ -27,7 +27,7 @@ test.group('File driver', () => { test('create intermediate directories when missing', async ({ fs, assert }) => { const sessionId = '1234' - const session = new FileDriver( + const session = new FileStore( { location: join(fs.basePath, 'app/sessions'), }, @@ -45,7 +45,7 @@ test.group('File driver', () => { test('update existing session', async ({ fs, assert }) => { const sessionId = '1234' - const session = new FileDriver( + const session = new FileStore( { location: fs.basePath, }, @@ -67,7 +67,7 @@ test.group('File driver', () => { test('get session existing value', async ({ assert, fs }) => { const sessionId = '1234' - const session = new FileDriver({ location: fs.basePath }, '2 hours') + const session = new FileStore({ location: fs.basePath }, '2 hours') await session.write(sessionId, { message: 'hello-world' }) const value = await session.read(sessionId) @@ -76,7 +76,7 @@ test.group('File driver', () => { test('return null when session data is expired', async ({ assert, fs }) => { const sessionId = '1234' - const session = new FileDriver({ location: fs.basePath }, 1000) + const session = new FileStore({ location: fs.basePath }, 1000) await session.write(sessionId, { message: 'hello-world' }) await setTimeout(2000) @@ -87,7 +87,7 @@ test.group('File driver', () => { test('ignore malformed file contents', async ({ fs, assert }) => { const sessionId = '1234' - const session = new FileDriver({ location: fs.basePath }, '2 hours') + const session = new FileStore({ location: fs.basePath }, '2 hours') await fs.create('1234.txt', '') assert.isNull(await session.read(sessionId)) @@ -102,7 +102,7 @@ test.group('File driver', () => { test('remove file on destroy', async ({ assert, fs }) => { const sessionId = '1234' - const session = new FileDriver({ location: fs.basePath }, '2 hours') + const session = new FileStore({ location: fs.basePath }, '2 hours') await session.write(sessionId, { message: 'hello-world' }) await session.destroy(sessionId) @@ -114,7 +114,7 @@ test.group('File driver', () => { await assert.fileNotExists('1234.txt') - const session = new FileDriver({ location: fs.basePath }, '2 hours') + const session = new FileStore({ location: fs.basePath }, '2 hours') await session.destroy(sessionId) await assert.fileNotExists('1234.txt') @@ -123,7 +123,7 @@ test.group('File driver', () => { test('update session expiry on touch', async ({ assert, fs }) => { const sessionId = '1234' - const session = new FileDriver({ location: fs.basePath }, '2 hours') + const session = new FileStore({ location: fs.basePath }, '2 hours') await session.write(sessionId, { message: 'hello-world' }) /** diff --git a/tests/drivers/memory.spec.ts b/tests/stores/memory_store.spec.ts similarity index 57% rename from tests/drivers/memory.spec.ts rename to tests/stores/memory_store.spec.ts index 34ef317..3e75419 100644 --- a/tests/drivers/memory.spec.ts +++ b/tests/stores/memory_store.spec.ts @@ -8,68 +8,68 @@ */ import { test } from '@japa/runner' -import { MemoryDriver } from '../../src/drivers/memory.js' +import { MemoryStore } from '../../src/stores/memory.js' -test.group('Memory driver', (group) => { +test.group('Memory store', (group) => { group.each.setup(() => { - return () => MemoryDriver.sessions.clear() + return () => MemoryStore.sessions.clear() }) test('return null when session does not exists', async ({ assert }) => { const sessionId = '1234' - const session = new MemoryDriver() + const session = new MemoryStore() assert.isNull(session.read(sessionId)) }) test('write to session store', async ({ assert }) => { const sessionId = '1234' - const session = new MemoryDriver() + const session = new MemoryStore() session.write(sessionId, { message: 'hello-world' }) - assert.isTrue(MemoryDriver.sessions.has(sessionId)) - assert.deepEqual(MemoryDriver.sessions.get(sessionId), { message: 'hello-world' }) + assert.isTrue(MemoryStore.sessions.has(sessionId)) + assert.deepEqual(MemoryStore.sessions.get(sessionId), { message: 'hello-world' }) }) test('update existing session', async ({ assert }) => { const sessionId = '1234' - const session = new MemoryDriver() + const session = new MemoryStore() session.write(sessionId, { message: 'hello-world' }) - assert.isTrue(MemoryDriver.sessions.has(sessionId)) - assert.deepEqual(MemoryDriver.sessions.get(sessionId), { message: 'hello-world' }) + assert.isTrue(MemoryStore.sessions.has(sessionId)) + assert.deepEqual(MemoryStore.sessions.get(sessionId), { message: 'hello-world' }) session.write(sessionId, { foo: 'bar' }) - assert.isTrue(MemoryDriver.sessions.has(sessionId)) - assert.deepEqual(MemoryDriver.sessions.get(sessionId), { foo: 'bar' }) + assert.isTrue(MemoryStore.sessions.has(sessionId)) + assert.deepEqual(MemoryStore.sessions.get(sessionId), { foo: 'bar' }) }) test('get session existing value', async ({ assert }) => { const sessionId = '1234' - const session = new MemoryDriver() + const session = new MemoryStore() session.write(sessionId, { message: 'hello-world' }) - assert.isTrue(MemoryDriver.sessions.has(sessionId)) + assert.isTrue(MemoryStore.sessions.has(sessionId)) assert.deepEqual(session.read(sessionId), { message: 'hello-world' }) }) test('remove session on destroy', async ({ assert }) => { const sessionId = '1234' - const session = new MemoryDriver() + const session = new MemoryStore() session.write(sessionId, { message: 'hello-world' }) session.destroy(sessionId) - assert.isFalse(MemoryDriver.sessions.has(sessionId)) + assert.isFalse(MemoryStore.sessions.has(sessionId)) }) test('noop on touch', async ({ assert }) => { const sessionId = '1234' - const session = new MemoryDriver() + const session = new MemoryStore() session.write(sessionId, { message: 'hello-world' }) session.touch() - assert.deepEqual(MemoryDriver.sessions.get(sessionId), { message: 'hello-world' }) + assert.deepEqual(MemoryStore.sessions.get(sessionId), { message: 'hello-world' }) }) }) diff --git a/tests/drivers/redis_driver.spec.ts b/tests/stores/redis_store.spec.ts similarity index 75% rename from tests/drivers/redis_driver.spec.ts rename to tests/stores/redis_store.spec.ts index ce3b57c..bc7fdb5 100644 --- a/tests/drivers/redis_driver.spec.ts +++ b/tests/stores/redis_store.spec.ts @@ -11,9 +11,8 @@ import { test } from '@japa/runner' import { defineConfig } from '@adonisjs/redis' import { setTimeout } from 'node:timers/promises' import { RedisManagerFactory } from '@adonisjs/redis/factories' -import type { RedisService, InferConnections } from '@adonisjs/redis/types' -import { RedisDriver } from '../../src/drivers/redis.js' +import { RedisStore } from '../../src/stores/redis.js' const sessionId = '1234' const redisConfig = defineConfig({ @@ -25,12 +24,9 @@ const redisConfig = defineConfig({ }, }, }) -const redis = new RedisManagerFactory(redisConfig).create() as RedisService -declare module '@adonisjs/redis/types' { - export interface RedisConnections extends InferConnections {} -} +const redis = new RedisManagerFactory(redisConfig).create() -test.group('Redis driver', (group) => { +test.group('Redis store', (group) => { group.tap((t) => { t.skip(!!process.env.NO_REDIS, 'Redis not available in windows env') }) @@ -42,13 +38,13 @@ test.group('Redis driver', (group) => { }) test('return null when value is missing', async ({ assert }) => { - const session = new RedisDriver(redis, { connection: 'main' }, '2 hours') + const session = new RedisStore(redis.connection('main'), '2 hours') const value = await session.read(sessionId) assert.isNull(value) }) test('save session data in a set', async ({ assert }) => { - const session = new RedisDriver(redis, { connection: 'main' }, '2 hours') + const session = new RedisStore(redis.connection('main'), '2 hours') await session.write(sessionId, { message: 'hello-world' }) assert.equal( @@ -61,7 +57,7 @@ test.group('Redis driver', (group) => { }) test('return null when session data is expired', async ({ assert }) => { - const session = new RedisDriver(redis, { connection: 'main' }, 1) + const session = new RedisStore(redis.connection('main'), 1) await session.write(sessionId, { message: 'hello-world' }) await setTimeout(2000) @@ -71,7 +67,7 @@ test.group('Redis driver', (group) => { }).disableTimeout() test('ignore malformed contents', async ({ assert }) => { - const session = new RedisDriver(redis, { connection: 'main' }, 1) + const session = new RedisStore(redis.connection('main'), 1) await redis.set(sessionId, 'foo') const value = await session.read(sessionId) @@ -79,7 +75,7 @@ test.group('Redis driver', (group) => { }) test('delete key on destroy', async ({ assert }) => { - const session = new RedisDriver(redis, { connection: 'main' }, '2 hours') + const session = new RedisStore(redis.connection('main'), '2 hours') await session.write(sessionId, { message: 'hello-world' }) await session.destroy(sessionId) @@ -88,7 +84,7 @@ test.group('Redis driver', (group) => { }) test('update session expiry on touch', async ({ assert }) => { - const session = new RedisDriver(redis, { connection: 'main' }, 10) + const session = new RedisStore(redis.connection('main'), 10) await session.write(sessionId, { message: 'hello-world' }) /** diff --git a/tests/store.spec.ts b/tests/values_store.spec.ts similarity index 86% rename from tests/store.spec.ts rename to tests/values_store.spec.ts index 55e2ab6..11c9cd5 100644 --- a/tests/store.spec.ts +++ b/tests/values_store.spec.ts @@ -8,23 +8,23 @@ */ import { test } from '@japa/runner' -import { Store } from '../src/store.js' +import { ValuesStore } from '../src/values_store.js' test.group('Store', () => { test('return empty object for empty store', ({ assert }) => { - const store = new Store(null) + const store = new ValuesStore(null) assert.deepEqual(store.toJSON(), {}) assert.isTrue(store.isEmpty) assert.isFalse(store.hasBeenModified) }) test('return default value when original value is null', ({ assert }) => { - const store = new Store({ title: null } as any) + const store = new ValuesStore({ title: null } as any) assert.equal(store.get('title', ''), '') }) test('mutate values inside store', ({ assert }) => { - const store = new Store({}) + const store = new ValuesStore({}) store.set('username', 'virk') assert.isFalse(store.isEmpty) @@ -33,7 +33,7 @@ test.group('Store', () => { }) test('mutate nested values inside store', ({ assert }) => { - const store = new Store({}) + const store = new ValuesStore({}) store.set('user.username', 'virk') assert.isFalse(store.isEmpty) @@ -42,7 +42,7 @@ test.group('Store', () => { }) test('remove value from store', ({ assert }) => { - const store = new Store(null) + const store = new ValuesStore(null) store.set('user.username', 'virk') store.unset('user.username') @@ -52,7 +52,7 @@ test.group('Store', () => { }) test('increment value inside store', ({ assert }) => { - const store = new Store(null) + const store = new ValuesStore(null) store.set('user.age', 22) store.increment('user.age') @@ -62,13 +62,13 @@ test.group('Store', () => { }) test('throw when incrementing a non integer value', () => { - const store = new Store(null) + const store = new ValuesStore(null) store.set('user.age', 'foo') store.increment('user.age') }).throws('Cannot increment "user.age". Existing value is not a number') test('decrement value inside store', ({ assert }) => { - const store = new Store(null) + const store = new ValuesStore(null) store.set('user.age', 22) store.decrement('user.age') @@ -78,13 +78,13 @@ test.group('Store', () => { }) test('throw when decrementing a non integer value', () => { - const store = new Store(null) + const store = new ValuesStore(null) store.set('user.age', 'foo') store.decrement('user.age') }).throws('Cannot decrement "user.age". Existing value is not a number') test('find if value exists in the store', ({ assert }) => { - const store = new Store({}) + const store = new ValuesStore({}) assert.isFalse(store.has('username')) store.update({ username: 'virk' }) @@ -95,7 +95,7 @@ test.group('Store', () => { }) test('check for arrays length', ({ assert }) => { - const store = new Store({}) + const store = new ValuesStore({}) assert.isFalse(store.has('users')) store.update({ users: [] }) @@ -106,7 +106,7 @@ test.group('Store', () => { }) test('do not check for array length when explicitly said no', ({ assert }) => { - const store = new Store({}) + const store = new ValuesStore({}) assert.isFalse(store.has('users')) store.update({ users: [] }) @@ -117,7 +117,7 @@ test.group('Store', () => { }) test('pull key from the store', ({ assert }) => { - const store = new Store({}) + const store = new ValuesStore({}) store.set('username', 'virk') assert.equal(store.pull('username'), 'virk') @@ -128,7 +128,7 @@ test.group('Store', () => { }) test('deep merge with existing values', ({ assert }) => { - const store = new Store({}) + const store = new ValuesStore({}) store.set('user', { profile: { username: 'virk' }, id: 1 }) store.merge({ user: { profile: { age: 32 } } }) @@ -139,7 +139,7 @@ test.group('Store', () => { }) test('clear store', ({ assert }) => { - const store = new Store({}) + const store = new ValuesStore({}) store.set('user', { profile: { username: 'virk' }, id: 1 }) store.clear() @@ -149,7 +149,7 @@ test.group('Store', () => { }) test('stringify store data object', ({ assert }) => { - const store = new Store({}) + const store = new ValuesStore({}) store.set('user', { profile: { username: 'virk' }, id: 1 }) store.merge({ user: { profile: { age: 32 } } }) From 292f140ed39431811434fdb7addd0cf7e6e4b4b1 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 19 Oct 2023 12:20:08 +0530 Subject: [PATCH 095/112] chore(release): 7.0.0-12 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cec0215..e4ed1cf 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@adonisjs/session", "description": "Session provider for AdonisJS", - "version": "7.0.0-11", + "version": "7.0.0-12", "engines": { "node": ">=18.16.0" }, From f6f2202e628c8b5a5cf43f2310e455c0d933b183 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 19 Oct 2023 13:04:01 +0530 Subject: [PATCH 096/112] chore: update dependencies --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e4ed1cf..9205917 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "@japa/assert": "^2.0.0", "@japa/browser-client": "^2.0.0", "@japa/file-system": "^2.0.0", - "@japa/plugin-adonisjs": "^2.0.0-3", + "@japa/plugin-adonisjs": "^2.0.0", "@japa/runner": "^3.0.4", "@japa/snapshot": "^2.0.0", "@swc/core": "1.3.82", From aa8faf3c8b22dd2f8b91ad80db1a831ec6c22868 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 19 Oct 2023 13:04:29 +0530 Subject: [PATCH 097/112] refactor: configure command to only allow cookie and memory drivers --- configure.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ts b/configure.ts index 69c01b9..ab1347e 100644 --- a/configure.ts +++ b/configure.ts @@ -30,7 +30,7 @@ export async function configure(command: Configure) { */ await codemods.defineEnvValidations({ variables: { - SESSION_DRIVER: `Env.schema.enum(['cookie', 'redis', 'file', 'memory'] as const)`, + SESSION_DRIVER: `Env.schema.enum(['cookie', 'memory'] as const)`, }, leadingComment: 'Variables for configuring session package', }) From a5e615446c3bb3148e90d0249edd490d8c84d46d Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 19 Oct 2023 13:06:30 +0530 Subject: [PATCH 098/112] chore(release): 7.0.0-13 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9205917..70ce2de 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@adonisjs/session", "description": "Session provider for AdonisJS", - "version": "7.0.0-12", + "version": "7.0.0-13", "engines": { "node": ">=18.16.0" }, From 4c3250ce2f46bd54734738db7d7c0b2e0e4d973e Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Fri, 24 Nov 2023 14:50:07 +0530 Subject: [PATCH 099/112] chore: update dependencies --- package.json | 58 ++++++++++++++++++++++++++-------------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index 70ce2de..7a67163 100644 --- a/package.json +++ b/package.json @@ -45,50 +45,50 @@ "quick:test": "node --enable-source-maps --loader=ts-node/esm bin/test.ts" }, "devDependencies": { - "@adonisjs/assembler": "^6.1.3-25", - "@adonisjs/core": "^6.1.5-28", - "@adonisjs/eslint-config": "^1.1.8", - "@adonisjs/prettier-config": "^1.1.8", - "@adonisjs/redis": "^8.0.0-11", - "@adonisjs/tsconfig": "^1.1.8", - "@japa/api-client": "^2.0.0", - "@japa/assert": "^2.0.0", - "@japa/browser-client": "^2.0.0", - "@japa/file-system": "^2.0.0", - "@japa/plugin-adonisjs": "^2.0.0", - "@japa/runner": "^3.0.4", - "@japa/snapshot": "^2.0.0", - "@swc/core": "1.3.82", - "@types/node": "^20.8.7", - "@types/set-cookie-parser": "^2.4.4", - "@types/supertest": "^2.0.14", - "@vinejs/vine": "^1.6.0", + "@adonisjs/assembler": "^6.1.3-28", + "@adonisjs/core": "^6.1.5-32", + "@adonisjs/eslint-config": "^1.1.9", + "@adonisjs/prettier-config": "^1.1.9", + "@adonisjs/redis": "^8.0.0-13", + "@adonisjs/tsconfig": "^1.1.9", + "@japa/api-client": "^2.0.1", + "@japa/assert": "^2.0.1", + "@japa/browser-client": "^2.0.1", + "@japa/file-system": "^2.0.1", + "@japa/plugin-adonisjs": "^2.0.1", + "@japa/runner": "^3.1.0", + "@japa/snapshot": "^2.0.3", + "@swc/core": "^1.3.99", + "@types/node": "^20.9.5", + "@types/set-cookie-parser": "^2.4.7", + "@types/supertest": "^2.0.16", + "@vinejs/vine": "^1.7.0", "c8": "^8.0.0", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", "del-cli": "^5.0.0", - "edge.js": "^6.0.0-8", - "eslint": "^8.51.0", + "edge.js": "^6.0.0", + "eslint": "^8.54.0", "get-port": "^7.0.0", "github-label-sync": "^2.3.1", "husky": "^8.0.3", "np": "^8.0.4", - "playwright": "^1.38.0", - "prettier": "^3.0.2", + "playwright": "^1.40.0", + "prettier": "^3.1.0", "set-cookie-parser": "^2.6.0", "supertest": "^6.3.3", "ts-node": "^10.9.1", - "typescript": "^5.1.6" + "typescript": "^5.3.2" }, "dependencies": { - "@poppinss/utils": "^6.5.0" + "@poppinss/utils": "^6.5.1" }, "peerDependencies": { - "@adonisjs/core": "^6.1.5-28", - "@adonisjs/redis": "^8.0.0-11", - "@japa/api-client": "^2.0.0", - "@japa/browser-client": "^2.0.0", - "edge.js": "^6.0.0-8" + "@adonisjs/core": "^6.1.5-32", + "@adonisjs/redis": "^8.0.0-13", + "@japa/api-client": "^2.0.1", + "@japa/browser-client": "^2.0.1", + "edge.js": "^6.0.0" }, "peerDependenciesMeta": { "@adonisjs/redis": { From c5197f81317ebe650099f08c8a64643e30132e59 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Sat, 25 Nov 2023 09:35:57 +0530 Subject: [PATCH 100/112] chore: publish source maps and use tsc for generating types --- package.json | 37 +++++++++++++++++------- tests/concurrent_session.spec.ts | 2 +- tests/plugins/api_client.spec.ts | 2 +- tests/plugins/browser_client.spec.ts | 2 +- tests/session.spec.ts | 2 +- tests/session_middleware.spec.ts | 2 +- tests/stores/cookie_store.spec.ts | 2 +- {test_helpers => tests_helpers}/index.ts | 0 8 files changed, 33 insertions(+), 16 deletions(-) rename {test_helpers => tests_helpers}/index.ts (100%) diff --git a/package.json b/package.json index 7a67163..4597c64 100644 --- a/package.json +++ b/package.json @@ -8,14 +8,10 @@ "main": "build/index.js", "type": "module", "files": [ - "build/src", - "build/stubs", - "build/providers", - "build/factories", - "build/configure.d.ts", - "build/configure.js", - "build/index.d.ts", - "build/index.js" + "build", + "!build/bin", + "!build/tests", + "!build/tests_helpers" ], "exports": { ".": "./build/index.js", @@ -34,8 +30,10 @@ "clean": "del-cli build", "typecheck": "tsc --noEmit", "copy:templates": "copyfiles \"stubs/**/*.stub\" build", - "compile": "npm run lint && npm run clean && tsc", - "build": "npm run compile && npm run copy:templates", + "precompile": "npm run lint && npm run clean", + "compile": "tsup-node && tsc --emitDeclarationOnly --declaration", + "postcompile": "npm run copy:templates", + "build": "npm run compile", "prepublishOnly": "npm run build", "lint": "eslint . --ext=.ts", "format": "prettier --write .", @@ -78,6 +76,7 @@ "set-cookie-parser": "^2.6.0", "supertest": "^6.3.3", "ts-node": "^10.9.1", + "tsup": "^8.0.1", "typescript": "^5.3.2" }, "dependencies": { @@ -148,5 +147,23 @@ "factories/**", "bin/**" ] + }, + "tsup": { + "entry": [ + "./index.ts", + "./factories/main.ts", + "./providers/session_provider.ts", + "./src/session_middleware.ts", + "./src/plugins/edge.ts", + "./src/plugins/japa/api_client.ts", + "./src/plugins/japa/browser_client.ts", + "./src/client.ts" + ], + "outDir": "./build", + "clean": true, + "format": "esm", + "dts": false, + "sourcemap": true, + "target": "esnext" } } diff --git a/tests/concurrent_session.spec.ts b/tests/concurrent_session.spec.ts index 68d0b71..3365504 100644 --- a/tests/concurrent_session.spec.ts +++ b/tests/concurrent_session.spec.ts @@ -25,7 +25,7 @@ import { HttpContextFactory, RequestFactory, ResponseFactory } from '@adonisjs/c import { Session } from '../src/session.js' import { FileStore } from '../src/stores/file.js' import { RedisStore } from '../src/stores/redis.js' -import { httpServer } from '../test_helpers/index.js' +import { httpServer } from '../tests_helpers/index.js' import { CookieStore } from '../src/stores/cookie.js' import type { SessionConfig, SessionStoreContract } from '../src/types.js' diff --git a/tests/plugins/api_client.spec.ts b/tests/plugins/api_client.spec.ts index 27b1842..05b15b7 100644 --- a/tests/plugins/api_client.spec.ts +++ b/tests/plugins/api_client.spec.ts @@ -19,7 +19,7 @@ import { Session } from '../../src/session.js' import { SessionConfig } from '../../src/types.js' import { defineConfig } from '../../src/define_config.js' import { MemoryStore } from '../../src/stores/memory.js' -import { httpServer, runJapaTest } from '../../test_helpers/index.js' +import { httpServer, runJapaTest } from '../../tests_helpers/index.js' const app = new AppFactory().create(new URL('./', import.meta.url), () => {}) as ApplicationService diff --git a/tests/plugins/browser_client.spec.ts b/tests/plugins/browser_client.spec.ts index d5b72fb..c6e4b77 100644 --- a/tests/plugins/browser_client.spec.ts +++ b/tests/plugins/browser_client.spec.ts @@ -19,7 +19,7 @@ import { Session } from '../../src/session.js' import { SessionConfig } from '../../src/types.js' import { MemoryStore } from '../../src/stores/memory.js' import { defineConfig } from '../../src/define_config.js' -import { httpServer, runJapaTest } from '../../test_helpers/index.js' +import { httpServer, runJapaTest } from '../../tests_helpers/index.js' const app = new AppFactory().create(new URL('./', import.meta.url), () => {}) as ApplicationService diff --git a/tests/session.spec.ts b/tests/session.spec.ts index d0f6338..76b5899 100644 --- a/tests/session.spec.ts +++ b/tests/session.spec.ts @@ -28,7 +28,7 @@ import { } from '@adonisjs/core/factories/http' import { Session } from '../src/session.js' -import { httpServer } from '../test_helpers/index.js' +import { httpServer } from '../tests_helpers/index.js' import { CookieStore } from '../src/stores/cookie.js' import SessionProvider from '../providers/session_provider.js' import type { SessionConfig, SessionStoreFactory } from '../src/types.js' diff --git a/tests/session_middleware.spec.ts b/tests/session_middleware.spec.ts index 99636cb..f44d54f 100644 --- a/tests/session_middleware.spec.ts +++ b/tests/session_middleware.spec.ts @@ -14,7 +14,7 @@ import { CookieClient } from '@adonisjs/core/http' import { EncryptionFactory } from '@adonisjs/core/factories/encryption' import { HttpContextFactory, RequestFactory, ResponseFactory } from '@adonisjs/core/factories/http' -import { httpServer } from '../test_helpers/index.js' +import { httpServer } from '../tests_helpers/index.js' import { CookieStore } from '../src/stores/cookie.js' import type { SessionConfig } from '../src/types.js' import { SessionMiddlewareFactory } from '../factories/session_middleware_factory.js' diff --git a/tests/stores/cookie_store.spec.ts b/tests/stores/cookie_store.spec.ts index 188ddcb..43f00a8 100644 --- a/tests/stores/cookie_store.spec.ts +++ b/tests/stores/cookie_store.spec.ts @@ -15,7 +15,7 @@ import type { CookieOptions } from '@adonisjs/core/types/http' import { EncryptionFactory } from '@adonisjs/core/factories/encryption' import { HttpContextFactory, RequestFactory, ResponseFactory } from '@adonisjs/core/factories/http' -import { httpServer } from '../../test_helpers/index.js' +import { httpServer } from '../../tests_helpers/index.js' import { CookieStore } from '../../src/stores/cookie.js' const encryption = new EncryptionFactory().create() diff --git a/test_helpers/index.ts b/tests_helpers/index.ts similarity index 100% rename from test_helpers/index.ts rename to tests_helpers/index.ts From 0ca5d227046ebadbd418e00809adcd001c1bcc3e Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Sat, 25 Nov 2023 09:36:48 +0530 Subject: [PATCH 101/112] chore: update config stub to create a config var and then export it --- stubs/config.stub | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/stubs/config.stub b/stubs/config.stub index 1a7a9d4..6414b49 100644 --- a/stubs/config.stub +++ b/stubs/config.stub @@ -5,7 +5,7 @@ import env from '#start/env' import app from '@adonisjs/core/services/app' import { defineConfig, stores } from '@adonisjs/session' -export default defineConfig({ +const sessionConfig = defineConfig({ enabled: true, cookieName: 'adonis-session', @@ -47,3 +47,5 @@ export default defineConfig({ cookie: stores.cookie(), } }) + +export default sessionConfig From 55399e0b0af92eccc032d3d544c4363bf2179c17 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Sat, 25 Nov 2023 09:52:00 +0530 Subject: [PATCH 102/112] refactor: use app.usingEdgeJS boolean to register edge plugin --- package.json | 4 ++-- providers/session_provider.ts | 12 +++--------- tests/configure.spec.ts | 2 +- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 4597c64..2b80d25 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "test": "cross-env NODE_DEBUG=adonisjs:session c8 npm run quick:test", "clean": "del-cli build", "typecheck": "tsc --noEmit", - "copy:templates": "copyfiles \"stubs/**/*.stub\" build", + "copy:templates": "copyfiles \"stubs/**/*.stub\" --up=\"1\" build", "precompile": "npm run lint && npm run clean", "compile": "tsup-node && tsc --emitDeclarationOnly --declaration", "postcompile": "npm run copy:templates", @@ -77,7 +77,7 @@ "supertest": "^6.3.3", "ts-node": "^10.9.1", "tsup": "^8.0.1", - "typescript": "^5.3.2" + "typescript": "5.2.2" }, "dependencies": { "@poppinss/utils": "^6.5.1" diff --git a/providers/session_provider.ts b/providers/session_provider.ts index f6b3739..6a62360 100644 --- a/providers/session_provider.ts +++ b/providers/session_provider.ts @@ -7,7 +7,6 @@ * file that was distributed with this source code. */ -import type { Edge } from 'edge.js' import { configProvider } from '@adonisjs/core' import { RuntimeException } from '@poppinss/utils' import type { ApplicationService } from '@adonisjs/core/types' @@ -38,15 +37,10 @@ export default class SessionProvider { * in the user application. */ protected async registerEdgePlugin() { - let edge: Edge | null = null - try { - const edgeExports = await import('edge.js') - edge = edgeExports.default - } catch {} - - if (edge) { + if (this.app.usingEdgeJS) { + const edge = await import('edge.js') const { edgePluginSession } = await import('../src/plugins/edge.js') - edge.use(edgePluginSession) + edge.default.use(edgePluginSession) } } diff --git a/tests/configure.spec.ts b/tests/configure.spec.ts index 8a429fe..9d69a30 100644 --- a/tests/configure.spec.ts +++ b/tests/configure.spec.ts @@ -55,7 +55,7 @@ test.group('Configure', (group) => { await assert.fileContains('.env', 'SESSION_DRIVER=cookie') await assert.fileContains( 'start/env.ts', - `SESSION_DRIVER: Env.schema.enum(['cookie', 'redis', 'file', 'memory'] as const)` + `SESSION_DRIVER: Env.schema.enum(['cookie', 'memory'] as const)` ) }).timeout(60 * 1000) }) From 6481870b3e2acf29224d8abced1f756f7258fe2f Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Sat, 25 Nov 2023 09:54:30 +0530 Subject: [PATCH 103/112] ci: update node versions --- .github/workflows/checks.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 98b0520..3b149a7 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [18.16.0, 20.x] + node-version: [20.0.10, 21.x] services: redis: image: redis @@ -44,7 +44,7 @@ jobs: runs-on: windows-latest strategy: matrix: - node-version: [18.16.0, 20.x] + node-version: [20.0.10, 21.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} From ca94473f8ba002abcb81d69ebfb767e796bdf7b2 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Sat, 25 Nov 2023 09:57:52 +0530 Subject: [PATCH 104/112] ci: update node versions --- .github/workflows/checks.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 3b149a7..8c26a40 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [20.0.10, 21.x] + node-version: [20.10.0, 21.x] services: redis: image: redis @@ -44,7 +44,7 @@ jobs: runs-on: windows-latest strategy: matrix: - node-version: [20.0.10, 21.x] + node-version: [20.10.0, 21.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} From a3d2750c246f93191f4185555a0009ab50e21267 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Sat, 25 Nov 2023 10:01:59 +0530 Subject: [PATCH 105/112] chore(release): 7.0.0-14 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2b80d25..2702341 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@adonisjs/session", "description": "Session provider for AdonisJS", - "version": "7.0.0-13", + "version": "7.0.0-14", "engines": { "node": ">=18.16.0" }, From 244cef75c8ff1301a49abf95a23aff5b138bf8b0 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 26 Dec 2023 08:54:52 +0530 Subject: [PATCH 106/112] chore: update dependencies --- package.json | 52 ++++++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index 2702341..6a23d1f 100644 --- a/package.json +++ b/package.json @@ -43,50 +43,50 @@ "quick:test": "node --enable-source-maps --loader=ts-node/esm bin/test.ts" }, "devDependencies": { - "@adonisjs/assembler": "^6.1.3-28", - "@adonisjs/core": "^6.1.5-32", - "@adonisjs/eslint-config": "^1.1.9", - "@adonisjs/prettier-config": "^1.1.9", - "@adonisjs/redis": "^8.0.0-13", - "@adonisjs/tsconfig": "^1.1.9", - "@japa/api-client": "^2.0.1", - "@japa/assert": "^2.0.1", - "@japa/browser-client": "^2.0.1", - "@japa/file-system": "^2.0.1", + "@adonisjs/assembler": "^7.0.0-1", + "@adonisjs/core": "^6.1.5-36", + "@adonisjs/eslint-config": "^1.2.0", + "@adonisjs/prettier-config": "^1.2.0", + "@adonisjs/redis": "^8.0.0-14", + "@adonisjs/tsconfig": "^1.2.0", + "@japa/api-client": "^2.0.2", + "@japa/assert": "^2.1.0", + "@japa/browser-client": "^2.0.2", + "@japa/file-system": "^2.1.1", "@japa/plugin-adonisjs": "^2.0.1", - "@japa/runner": "^3.1.0", - "@japa/snapshot": "^2.0.3", - "@swc/core": "^1.3.99", - "@types/node": "^20.9.5", + "@japa/runner": "^3.1.1", + "@japa/snapshot": "^2.0.4", + "@swc/core": "^1.3.101", + "@types/node": "^20.10.5", "@types/set-cookie-parser": "^2.4.7", - "@types/supertest": "^2.0.16", + "@types/supertest": "^6.0.1", "@vinejs/vine": "^1.7.0", "c8": "^8.0.0", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", "del-cli": "^5.0.0", "edge.js": "^6.0.0", - "eslint": "^8.54.0", + "eslint": "^8.56.0", "get-port": "^7.0.0", "github-label-sync": "^2.3.1", "husky": "^8.0.3", - "np": "^8.0.4", - "playwright": "^1.40.0", - "prettier": "^3.1.0", + "np": "^9.2.0", + "playwright": "^1.40.1", + "prettier": "^3.1.1", "set-cookie-parser": "^2.6.0", "supertest": "^6.3.3", - "ts-node": "^10.9.1", + "ts-node": "^10.9.2", "tsup": "^8.0.1", - "typescript": "5.2.2" + "typescript": "^5.3.3" }, "dependencies": { - "@poppinss/utils": "^6.5.1" + "@poppinss/utils": "^6.7.0" }, "peerDependencies": { - "@adonisjs/core": "^6.1.5-32", - "@adonisjs/redis": "^8.0.0-13", - "@japa/api-client": "^2.0.1", - "@japa/browser-client": "^2.0.1", + "@adonisjs/core": "^6.1.5-36", + "@adonisjs/redis": "^8.0.0-14", + "@japa/api-client": "^2.0.2", + "@japa/browser-client": "^2.0.2", "edge.js": "^6.0.0" }, "peerDependenciesMeta": { From 9a98c1fae45bf1bd9f594a3c28d234379e1dc656 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 26 Dec 2023 08:59:24 +0530 Subject: [PATCH 107/112] fix: stubs to use latests APIs --- configure.ts | 7 ++++--- index.ts | 1 - stubs/{config.stub => config/session.stub} | 0 3 files changed, 4 insertions(+), 4 deletions(-) rename stubs/{config.stub => config/session.stub} (100%) diff --git a/configure.ts b/configure.ts index ab1347e..028e0d7 100644 --- a/configure.ts +++ b/configure.ts @@ -8,17 +8,18 @@ */ import type Configure from '@adonisjs/core/commands/configure' +import { stubsRoot } from './stubs/main.js' /** * Configures the package */ export async function configure(command: Configure) { + const codemods = await command.createCodemods() + /** * Publish config file */ - await command.publishStub('config.stub') - - const codemods = await command.createCodemods() + await codemods.makeUsingStub(stubsRoot, 'config/session.stub', {}) /** * Define environment variables diff --git a/index.ts b/index.ts index a0a549e..cb0bab2 100644 --- a/index.ts +++ b/index.ts @@ -9,5 +9,4 @@ export * as errors from './src/errors.js' export { configure } from './configure.js' -export { stubsRoot } from './stubs/main.js' export { defineConfig, stores } from './src/define_config.js' diff --git a/stubs/config.stub b/stubs/config/session.stub similarity index 100% rename from stubs/config.stub rename to stubs/config/session.stub From 8d895c16ad5e69f03149e2851e4395d58036db2f Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 26 Dec 2023 09:49:04 +0530 Subject: [PATCH 108/112] feat: add @inputError tag and change how other edge tags work The @error tag is used to read generic error message The @inputError tag should be used to read validation error messages --- src/plugins/edge.ts | 68 ++++++++++- src/session.ts | 55 ++++++--- src/values_store.ts | 14 +-- tests/session.spec.ts | 278 ++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 379 insertions(+), 36 deletions(-) diff --git a/src/plugins/edge.ts b/src/plugins/edge.ts index f4ed69c..157be41 100644 --- a/src/plugins/edge.ts +++ b/src/plugins/edge.ts @@ -43,7 +43,7 @@ export const edgePluginSession: PluginFn = (edge) => { * Define a local variable */ buffer.writeExpression( - `let message = state.flashMessages.get(${key})`, + `let $message = state.flashMessages.get(${key})`, token.filename, token.loc.start.line ) @@ -53,7 +53,65 @@ export const edgePluginSession: PluginFn = (edge) => { * the existence of the "message" variable */ parser.stack.defineScope() - parser.stack.defineVariable('message') + parser.stack.defineVariable('$message') + + /** + * Process component children using the parser + */ + token.children.forEach((child) => { + parser.processToken(child, buffer) + }) + + /** + * Clear the scope of the local variables before we + * close the if statement + */ + parser.stack.clearScope() + + /** + * Close if statement + */ + buffer.writeStatement(`}`, token.filename, token.loc.start.line) + }, + }) + + edge.registerTag({ + tagName: 'inputError', + seekable: true, + block: true, + compile(parser, buffer, token) { + const expression = parser.utils.transformAst( + parser.utils.generateAST(token.properties.jsArg, token.loc, token.filename), + token.filename, + parser + ) + + const key = parser.utils.stringify(expression) + + /** + * Write an if statement + */ + buffer.writeStatement( + `if (!!state.flashMessages.get('inputErrorsBag', {})[${key}]) {`, + token.filename, + token.loc.start.line + ) + + /** + * Define a local variable + */ + buffer.writeExpression( + `let $messages = state.flashMessages.get('inputErrorsBag', {})[${key}]`, + token.filename, + token.loc.start.line + ) + + /** + * Create a local variables scope and tell the parser about + * the existence of the "messages" variable + */ + parser.stack.defineScope() + parser.stack.defineVariable('$messages') /** * Process component children using the parser @@ -92,7 +150,7 @@ export const edgePluginSession: PluginFn = (edge) => { * Write an if statement */ buffer.writeStatement( - `if (!!state.flashMessages.get('errors', {})[${key}]) {`, + `if (state.flashMessages.has(['errorsBag', ${key}])) {`, token.filename, token.loc.start.line ) @@ -101,7 +159,7 @@ export const edgePluginSession: PluginFn = (edge) => { * Define a local variable */ buffer.writeExpression( - `let messages = state.flashMessages.get('errors', {})[${key}]`, + `let $message = state.flashMessages.get(['errorsBag', ${key}])`, token.filename, token.loc.start.line ) @@ -111,7 +169,7 @@ export const edgePluginSession: PluginFn = (edge) => { * the existence of the "messages" variable */ parser.stack.defineScope() - parser.stack.defineVariable('messages') + parser.stack.defineVariable('$message') /** * Process component children using the parser diff --git a/src/session.ts b/src/session.ts index 3b53581..7749354 100644 --- a/src/session.ts +++ b/src/session.ts @@ -293,9 +293,36 @@ export class Session { return this.#getValuesStore('write').clear() } + /** + * Add a key-value pair to flash messages + */ + flash(key: string, value: AllowedSessionValues): void + flash(keyValue: SessionData): void + flash(key: string | SessionData, value?: AllowedSessionValues): void { + if (typeof key === 'string') { + if (value) { + this.#getFlashStore('write').set(key, value) + } + } else { + this.#getFlashStore('write').merge(key) + } + } + + /** + * Flash errors to the errorsBag. You can read these + * errors via the "@error" tag. + * + * Appends new messages to the existing collection. + */ + flashErrors(errorsCollection: Record) { + this.flash({ errorsBag: errorsCollection }) + } + /** * Flash validation error messages. Make sure the error - * is an instance of VineJS ValidationException + * is an instance of VineJS ValidationException. + * + * Overrides existing inputErrors */ flashValidationErrors(error: HttpError) { const errorsBag = error.messages.reduce((result: Record, message: any) => { @@ -308,22 +335,18 @@ export class Session { }, {}) this.flashExcept(['_csrf', '_method']) - this.flash('errors', errorsBag) - } - /** - * Add a key-value pair to flash messages - */ - flash(key: string, value: AllowedSessionValues): void - flash(keyValue: SessionData): void - flash(key: string | SessionData, value?: AllowedSessionValues): void { - if (typeof key === 'string') { - if (value) { - this.#getFlashStore('write').set(key, value) - } - } else { - this.#getFlashStore('write').merge(key) - } + /** + * Adding to inputErrorsBag for "@inputError" tag + * to read validation errors + */ + this.flash('inputErrorsBag', errorsBag) + + /** + * For legacy support and not to break apps using + * the older version of @adonisjs/session package + */ + this.flash('errors', errorsBag) } /** diff --git a/src/values_store.ts b/src/values_store.ts index ac014d7..a65bf71 100644 --- a/src/values_store.ts +++ b/src/values_store.ts @@ -34,7 +34,7 @@ export class ReadOnlyValuesStore { /** * Get value for a given key */ - get(key: string, defaultValue?: any): any { + get(key: string | string[], defaultValue?: any): any { const value = lodash.get(this.values, key) if (defaultValue !== undefined && (value === null || value === undefined)) { return defaultValue @@ -47,7 +47,7 @@ export class ReadOnlyValuesStore { * A boolean to know if value exists. Extra guards to check * arrays for it's length as well. */ - has(key: string, checkForArraysLength: boolean = true): boolean { + has(key: string | string[], checkForArraysLength: boolean = true): boolean { const value = this.get(key) if (!Array.isArray(value)) { return !!value @@ -110,7 +110,7 @@ export class ValuesStore extends ReadOnlyValuesStore { /** * Set key/value pair */ - set(key: string, value: AllowedSessionValues): void { + set(key: string | string[], value: AllowedSessionValues): void { this.#modified = true lodash.set(this.values, key, value) } @@ -118,7 +118,7 @@ export class ValuesStore extends ReadOnlyValuesStore { /** * Remove key */ - unset(key: string): void { + unset(key: string | string[]): void { this.#modified = true lodash.unset(this.values, key) } @@ -127,7 +127,7 @@ export class ValuesStore extends ReadOnlyValuesStore { * Pull value from the store. It is same as calling * store.get and then store.unset */ - pull(key: string, defaultValue?: any): any { + pull(key: string | string[], defaultValue?: any): any { return ((value): any => { this.unset(key) return value @@ -138,7 +138,7 @@ export class ValuesStore extends ReadOnlyValuesStore { * Increment number. The method raises an error when * nderlying value is not a number */ - increment(key: string, steps: number = 1): void { + increment(key: string | string[], steps: number = 1): void { const value = this.get(key, 0) if (typeof value !== 'number') { throw new RuntimeException(`Cannot increment "${key}". Existing value is not a number`) @@ -151,7 +151,7 @@ export class ValuesStore extends ReadOnlyValuesStore { * Increment number. The method raises an error when * nderlying value is not a number */ - decrement(key: string, steps: number = 1): void { + decrement(key: string | string[], steps: number = 1): void { const value = this.get(key, 0) if (typeof value !== 'number') { throw new RuntimeException(`Cannot decrement "${key}". Existing value is not a number`) diff --git a/tests/session.spec.ts b/tests/session.spec.ts index 76b5899..b5fae14 100644 --- a/tests/session.spec.ts +++ b/tests/session.spec.ts @@ -634,6 +634,13 @@ test.group('Session | Flash', (group) => { await new SessionProvider(app).boot() }) + group.each.setup(() => { + return () => { + edge.removeTemplate('flash_no_errors_messages') + edge.removeTemplate('flash_errors_messages') + } + }) + test('flash data using the session store', async ({ assert }) => { let sessionId: string | undefined @@ -966,6 +973,94 @@ test.group('Session | Flash', (group) => { email: ['Invalid email'], username: ['Invalid username', 'Username is required'], }, + inputErrorsBag: { + email: ['Invalid email'], + username: ['Invalid username', 'Username is required'], + }, + }, + }) + }) + + test("multiple calls to flashValidationErrors should keep the last one's", async ({ assert }) => { + let sessionId: string | undefined + + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() + + const session = new Session(sessionConfig, cookieDriver, emitter, ctx) + await session.initiate(false) + + const errorReporter = new SimpleErrorReporter() + const errorReporter1 = new SimpleErrorReporter() + errorReporter.report('Invalid username', 'alpha', fieldContext.create('username', ''), {}) + errorReporter.report( + 'Username is required', + 'required', + fieldContext.create('username', ''), + {} + ) + errorReporter.report('Invalid email', 'email', fieldContext.create('email', ''), {}) + + errorReporter1.report('Invalid name', 'alpha', fieldContext.create('name', ''), {}) + + session.flashValidationErrors(errorReporter.createError()) + session.flashValidationErrors(errorReporter1.createError()) + sessionId = session.sessionId + + await session.commit() + response.finish() + }) + + const { headers } = await supertest(server).get('/') + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) + + assert.deepEqual(cookieClient.decrypt(sessionId!, cookies[sessionId!].value), { + __flash__: { + errors: { + name: ['Invalid name'], + }, + inputErrorsBag: { + name: ['Invalid name'], + }, + }, + }) + }) + + test('flash collection of errors', async ({ assert }) => { + let sessionId: string | undefined + + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() + + const session = new Session(sessionConfig, cookieDriver, emitter, ctx) + await session.initiate(false) + + session.flashErrors({ + E_AUTHORIZATION_FAILED: 'Cannot access route', + }) + session.flashErrors({ + E_ACCESS_DENIED: 'Cannot access resource', + }) + + sessionId = session.sessionId + + await session.commit() + response.finish() + }) + + const { headers } = await supertest(server).get('/') + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) + + assert.deepEqual(cookieClient.decrypt(sessionId!, cookies[sessionId!].value), { + __flash__: { + errorsBag: { + E_AUTHORIZATION_FAILED: 'Cannot access route', + E_ACCESS_DENIED: 'Cannot access resource', + }, }, }) }) @@ -1016,10 +1111,10 @@ test.group('Session | Flash', (group) => { edge.registerTemplate('flash_messages_via_tag', { template: `@flashMessage('status') -

{{ message }}

+

{{ $message }}

@end @flashMessage('success') -

{{ message }}

+

{{ $message }}

@else

No success message

@end @@ -1063,11 +1158,11 @@ test.group('Session | Flash', (group) => { ) }) - test('use error tag when there are no error message', async ({ assert }) => { + test('use inputError tag when there are no error message', async ({ assert }) => { edge.registerTemplate('flash_no_errors_messages', { template: ` - @error('username') - @each(message in messages) + @inputError('username') + @each(message in $messages)

{{ message }}

@end @else @@ -1097,13 +1192,13 @@ test.group('Session | Flash', (group) => { ) }) - test('access flash error messages using the @error tag', async ({ assert }) => { + test('access input error messages using the @inputError tag', async ({ assert }) => { let sessionId: string | undefined edge.registerTemplate('flash_errors_messages', { template: ` - @error('username') - @each(message in messages) + @inputError('username') + @each(message in $messages)

{{ message }}

@end @else @@ -1157,4 +1252,171 @@ test.group('Session | Flash', (group) => { ['', '

Invalid username

', '

Username is required

', ''] ) }) + + test('define @inputError key as a variable', async ({ assert }) => { + let sessionId: string | undefined + + edge.registerTemplate('flash_errors_messages', { + template: ` + @inputError(field) + @each(message in $messages) +

{{ message }}

+ @end + @else +

No error message

+ @end + `, + }) + + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() + + const session = new Session(sessionConfig, cookieDriver, emitter, ctx) + await session.initiate(false) + sessionId = session.sessionId + + if (request.url() === '/prg') { + response.send(await ctx.view.render('flash_errors_messages', { field: 'username' })) + await session.commit() + response.finish() + } else { + const errorReporter = new SimpleErrorReporter() + errorReporter.report('Invalid username', 'alpha', fieldContext.create('username', ''), {}) + errorReporter.report( + 'Username is required', + 'required', + fieldContext.create('username', ''), + {} + ) + + session.flashValidationErrors(errorReporter.createError()) + await session.commit() + } + + response.finish() + }) + + const { headers } = await supertest(server).get('/') + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) + + const { text } = await supertest(server) + .get('/prg') + .set( + 'Cookie', + `adonis_session=${cookies.adonis_session.value}; ${sessionId}=${cookies[sessionId!].value}` + ) + + assert.deepEqual( + text.split('\n').map((line) => line.trim()), + ['', '

Invalid username

', '

Username is required

', ''] + ) + }) + + test('access error messages using the @error tag', async ({ assert }) => { + let sessionId: string | undefined + + edge.registerTemplate('flash_errors_messages', { + template: ` + @error('E_ACCESS_DENIED') +

{{ $message }}

+ @else +

No error message

+ @end + `, + }) + + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() + + const session = new Session(sessionConfig, cookieDriver, emitter, ctx) + await session.initiate(false) + sessionId = session.sessionId + + if (request.url() === '/prg') { + response.send(await ctx.view.render('flash_errors_messages')) + await session.commit() + response.finish() + } else { + session.flashErrors({ + E_ACCESS_DENIED: 'Access denied', + }) + await session.commit() + } + + response.finish() + }) + + const { headers } = await supertest(server).get('/') + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) + + const { text } = await supertest(server) + .get('/prg') + .set( + 'Cookie', + `adonis_session=${cookies.adonis_session.value}; ${sessionId}=${cookies[sessionId!].value}` + ) + + assert.deepEqual( + text.split('\n').map((line) => line.trim()), + ['

Access denied

', ''] + ) + }) + + test('define @error key from a variable', async ({ assert }) => { + let sessionId: string | undefined + + edge.registerTemplate('flash_errors_messages', { + template: ` + @error(errorCode) +

{{ $message }}

+ @else +

No error message

+ @end + `, + }) + + const server = httpServer.create(async (req, res) => { + const request = new RequestFactory().merge({ req, res, encryption }).create() + const response = new ResponseFactory().merge({ req, res, encryption }).create() + const ctx = new HttpContextFactory().merge({ request, response }).create() + + const session = new Session(sessionConfig, cookieDriver, emitter, ctx) + await session.initiate(false) + sessionId = session.sessionId + + if (request.url() === '/prg') { + response.send( + await ctx.view.render('flash_errors_messages', { errorCode: 'E_ACCESS_DENIED' }) + ) + await session.commit() + response.finish() + } else { + session.flashErrors({ + E_ACCESS_DENIED: 'Access denied', + }) + await session.commit() + } + + response.finish() + }) + + const { headers } = await supertest(server).get('/') + const cookies = setCookieParser.parse(headers['set-cookie'], { map: true }) + + const { text } = await supertest(server) + .get('/prg') + .set( + 'Cookie', + `adonis_session=${cookies.adonis_session.value}; ${sessionId}=${cookies[sessionId!].value}` + ) + + assert.deepEqual( + text.split('\n').map((line) => line.trim()), + ['

Access denied

', ''] + ) + }) }) From d0001e5fb26acbcbf510cab147b2f030ef49794e Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 26 Dec 2023 09:56:52 +0530 Subject: [PATCH 109/112] chore(release): 7.0.0-15 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6a23d1f..ccc9f1f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@adonisjs/session", "description": "Session provider for AdonisJS", - "version": "7.0.0-14", + "version": "7.0.0-15", "engines": { "node": ">=18.16.0" }, From 8c99a2a3de08c36e452f0ee4b05a9ea9f02ba97d Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 8 Jan 2024 16:43:16 +0530 Subject: [PATCH 110/112] chore: update dependencies --- package.json | 30 +++++++++++++++--------------- tests/session_provider.spec.ts | 12 ++---------- 2 files changed, 17 insertions(+), 25 deletions(-) diff --git a/package.json b/package.json index ccc9f1f..398c149 100644 --- a/package.json +++ b/package.json @@ -43,29 +43,29 @@ "quick:test": "node --enable-source-maps --loader=ts-node/esm bin/test.ts" }, "devDependencies": { - "@adonisjs/assembler": "^7.0.0-1", - "@adonisjs/core": "^6.1.5-36", - "@adonisjs/eslint-config": "^1.2.0", - "@adonisjs/prettier-config": "^1.2.0", - "@adonisjs/redis": "^8.0.0-14", - "@adonisjs/tsconfig": "^1.2.0", + "@adonisjs/assembler": "^7.0.0", + "@adonisjs/core": "^6.2.0", + "@adonisjs/eslint-config": "^1.2.1", + "@adonisjs/prettier-config": "^1.2.1", + "@adonisjs/redis": "^8.0.0", + "@adonisjs/tsconfig": "^1.2.1", "@japa/api-client": "^2.0.2", "@japa/assert": "^2.1.0", "@japa/browser-client": "^2.0.2", "@japa/file-system": "^2.1.1", - "@japa/plugin-adonisjs": "^2.0.1", + "@japa/plugin-adonisjs": "^3.0.0", "@japa/runner": "^3.1.1", "@japa/snapshot": "^2.0.4", - "@swc/core": "^1.3.101", - "@types/node": "^20.10.5", + "@swc/core": "^1.3.102", + "@types/node": "^20.10.7", "@types/set-cookie-parser": "^2.4.7", - "@types/supertest": "^6.0.1", + "@types/supertest": "^6.0.2", "@vinejs/vine": "^1.7.0", - "c8": "^8.0.0", + "c8": "^9.0.0", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", "del-cli": "^5.0.0", - "edge.js": "^6.0.0", + "edge.js": "^6.0.1", "eslint": "^8.56.0", "get-port": "^7.0.0", "github-label-sync": "^2.3.1", @@ -83,11 +83,11 @@ "@poppinss/utils": "^6.7.0" }, "peerDependencies": { - "@adonisjs/core": "^6.1.5-36", - "@adonisjs/redis": "^8.0.0-14", + "@adonisjs/core": "^6.2.0", + "@adonisjs/redis": "^8.0.0", "@japa/api-client": "^2.0.2", "@japa/browser-client": "^2.0.2", - "edge.js": "^6.0.0" + "edge.js": "^6.0.1" }, "peerDependenciesMeta": { "@adonisjs/redis": { diff --git a/tests/session_provider.spec.ts b/tests/session_provider.spec.ts index aa10a8d..3f3214c 100644 --- a/tests/session_provider.spec.ts +++ b/tests/session_provider.spec.ts @@ -14,19 +14,13 @@ import { defineConfig } from '../index.js' import SessionMiddleware from '../src/session_middleware.js' const BASE_URL = new URL('./tmp/', import.meta.url) -const IMPORTER = (filePath: string) => { - if (filePath.startsWith('./') || filePath.startsWith('../')) { - return import(new URL(filePath, BASE_URL).href) - } - return import(filePath) -} test.group('Session Provider', () => { test('register session provider', async ({ assert }) => { const ignitor = new IgnitorFactory() .merge({ rcFileContents: { - providers: ['../../providers/session_provider.js'], + providers: [() => import('../providers/session_provider.js')], }, }) .withCoreConfig() @@ -39,9 +33,7 @@ test.group('Session Provider', () => { }), }, }) - .create(BASE_URL, { - importer: IMPORTER, - }) + .create(BASE_URL) const app = ignitor.createApp('web') await app.init() From 5a3e1ff755938b4a2d01e9ff7cce76e29860de20 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 8 Jan 2024 16:44:35 +0530 Subject: [PATCH 111/112] chore: bundle types.ts file via tsup as well --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 398c149..204f604 100644 --- a/package.json +++ b/package.json @@ -154,6 +154,7 @@ "./factories/main.ts", "./providers/session_provider.ts", "./src/session_middleware.ts", + "./src/types.ts", "./src/plugins/edge.ts", "./src/plugins/japa/api_client.ts", "./src/plugins/japa/browser_client.ts", From f64df34076c32f3c447c6f1888ebf669fb450ad4 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 9 Jan 2024 09:04:23 +0530 Subject: [PATCH 112/112] refactor: export stubsRoot path --- index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/index.ts b/index.ts index cb0bab2..a0a549e 100644 --- a/index.ts +++ b/index.ts @@ -9,4 +9,5 @@ export * as errors from './src/errors.js' export { configure } from './configure.js' +export { stubsRoot } from './stubs/main.js' export { defineConfig, stores } from './src/define_config.js'