From 0314f975bd186f74099697cf978fd20d2053758e Mon Sep 17 00:00:00 2001 From: Ihor Masechko Date: Mon, 9 Mar 2026 12:34:18 +0200 Subject: [PATCH 1/3] feat: make Redis session store optional --- docker-compose.yml | 21 --------------------- website/app.js | 28 +++++++++++++++++----------- website/app.test.js | 10 ++++++++++ website/utils/env.js | 9 +++++++++ website/utils/env.test.js | 29 ++++++++++++++++++++++++++++- 5 files changed, 64 insertions(+), 33 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 4dec8c5b..b4897a38 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -37,7 +37,6 @@ services: ports: - "3000:3000" environment: - - REDIS_URI=redis://redis:6379 - BASE_URL=http://localhost:3000 - NODE_ENV=development - APOS_MONGODB_URI=mongodb://mongodb:27017/apostrophe @@ -61,7 +60,6 @@ services: depends_on: - mongodb - localstack - - redis restart: unless-stopped healthcheck: test: @@ -118,25 +116,6 @@ services: networks: - proxynet - # Redis for caching (optional, but recommended for production) - redis: - image: redis:7-alpine - container_name: apostrophe-redis - ports: - - "6379:6379" - volumes: - - redis_data:/data - command: redis-server --appendonly yes - restart: unless-stopped - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 5s - retries: 3 - networks: - - proxynet - volumes: mongodb_data: - redis_data: localstack_data: diff --git a/website/app.js b/website/app.js index 90a860cb..e79ca3dd 100644 --- a/website/app.js +++ b/website/app.js @@ -1,8 +1,23 @@ const apostrophe = require('apostrophe'); require('dotenv').config({ path: '../.env' }); -const { getEnv } = require('./utils/env'); +const { getEnv, getOptionalEnv } = require('./utils/env'); function createAposConfig() { + const redisUri = getOptionalEnv('REDIS_URI'); + + const sessionConfig = { + secret: getEnv('SESSION_SECRET'), + }; + + if (redisUri) { + sessionConfig.store = { + connect: require('connect-redis'), + options: { + url: redisUri, + }, + }; + } + return { shortName: 'apostrophe-site', baseUrl: process.env.BASE_URL || 'https://speedandfunction.com', @@ -13,16 +28,7 @@ function createAposConfig() { '@apostrophecms/security-headers': {}, '@apostrophecms/express': { options: { - session: { - // If using Redis (recommended for production) - secret: getEnv('SESSION_SECRET'), - store: { - connect: require('connect-redis'), - options: { - url: getEnv('REDIS_URI'), - }, - }, - }, + session: sessionConfig, csrf: { cookie: { key: '_csrf', diff --git a/website/app.test.js b/website/app.test.js index 54b9d58d..4315b730 100644 --- a/website/app.test.js +++ b/website/app.test.js @@ -58,6 +58,16 @@ describe('createAposConfig', () => { }); }); + test('uses in-memory session store when REDIS_URI is not set', () => { + delete process.env.REDIS_URI; + + const config = createAposConfig(); + + expect( + config.modules['@apostrophecms/express'].options.session.store, + ).toBeUndefined(); + }); + // Define module categories for verification - moved outside the test const moduleCategories = [ { diff --git a/website/utils/env.js b/website/utils/env.js index 5bc21ad3..10cf3976 100644 --- a/website/utils/env.js +++ b/website/utils/env.js @@ -7,6 +7,15 @@ const getEnv = (name) => { return value; }; +const getOptionalEnv = (name, defaultValue) => { + const value = process.env[name]; + if (value === undefined) { + return defaultValue; + } + return value; +}; + module.exports = { getEnv, + getOptionalEnv, }; diff --git a/website/utils/env.test.js b/website/utils/env.test.js index adad0ce6..d896bff9 100644 --- a/website/utils/env.test.js +++ b/website/utils/env.test.js @@ -1,4 +1,4 @@ -const { getEnv } = require('./env'); +const { getEnv, getOptionalEnv } = require('./env'); describe('getEnv utility', () => { const OLD_ENV = process.env; @@ -30,3 +30,30 @@ describe('getEnv utility', () => { }).toThrow('Environment variable "NON_EXISTENT_VAR" is not defined'); }); }); + +describe('getOptionalEnv utility', () => { + const OLD_ENV = process.env; + + beforeEach(() => { + jest.resetModules(); + process.env = { ...OLD_ENV }; + }); + + afterAll(() => { + process.env = OLD_ENV; + }); + + test('should return environment variable value when it exists', () => { + process.env.OPTIONAL_TEST_VAR = 'optional-test-value'; + + const result = getOptionalEnv('OPTIONAL_TEST_VAR'); + + expect(result).toBe('optional-test-value'); + }); + + test('should return default value when environment variable does not exist', () => { + const result = getOptionalEnv('NON_EXISTENT_OPTIONAL_VAR', 'default'); + + expect(result).toBe('default'); + }); +}); From 34ff9e5f51b54d6a9d8a2d1eb0a775f04e5e11ab Mon Sep 17 00:00:00 2001 From: Ihor Masechko Date: Mon, 9 Mar 2026 13:03:47 +0200 Subject: [PATCH 2/3] refactor: keep env fail-fast while making redis optional --- website/app.js | 4 ++-- website/utils/env.js | 9 --------- website/utils/env.test.js | 29 +---------------------------- 3 files changed, 3 insertions(+), 39 deletions(-) diff --git a/website/app.js b/website/app.js index e79ca3dd..e32b3006 100644 --- a/website/app.js +++ b/website/app.js @@ -1,9 +1,9 @@ const apostrophe = require('apostrophe'); require('dotenv').config({ path: '../.env' }); -const { getEnv, getOptionalEnv } = require('./utils/env'); +const { getEnv } = require('./utils/env'); function createAposConfig() { - const redisUri = getOptionalEnv('REDIS_URI'); + const redisUri = process.env.REDIS_URI; const sessionConfig = { secret: getEnv('SESSION_SECRET'), diff --git a/website/utils/env.js b/website/utils/env.js index 10cf3976..5bc21ad3 100644 --- a/website/utils/env.js +++ b/website/utils/env.js @@ -7,15 +7,6 @@ const getEnv = (name) => { return value; }; -const getOptionalEnv = (name, defaultValue) => { - const value = process.env[name]; - if (value === undefined) { - return defaultValue; - } - return value; -}; - module.exports = { getEnv, - getOptionalEnv, }; diff --git a/website/utils/env.test.js b/website/utils/env.test.js index d896bff9..adad0ce6 100644 --- a/website/utils/env.test.js +++ b/website/utils/env.test.js @@ -1,4 +1,4 @@ -const { getEnv, getOptionalEnv } = require('./env'); +const { getEnv } = require('./env'); describe('getEnv utility', () => { const OLD_ENV = process.env; @@ -30,30 +30,3 @@ describe('getEnv utility', () => { }).toThrow('Environment variable "NON_EXISTENT_VAR" is not defined'); }); }); - -describe('getOptionalEnv utility', () => { - const OLD_ENV = process.env; - - beforeEach(() => { - jest.resetModules(); - process.env = { ...OLD_ENV }; - }); - - afterAll(() => { - process.env = OLD_ENV; - }); - - test('should return environment variable value when it exists', () => { - process.env.OPTIONAL_TEST_VAR = 'optional-test-value'; - - const result = getOptionalEnv('OPTIONAL_TEST_VAR'); - - expect(result).toBe('optional-test-value'); - }); - - test('should return default value when environment variable does not exist', () => { - const result = getOptionalEnv('NON_EXISTENT_OPTIONAL_VAR', 'default'); - - expect(result).toBe('default'); - }); -}); From 7592ab1c40f10946889c842ae2ced08b7363e1f3 Mon Sep 17 00:00:00 2001 From: Ihor Masechko Date: Mon, 9 Mar 2026 14:00:41 +0200 Subject: [PATCH 3/3] Improve docstring coverage for app config --- website/app.js | 7 +++++++ website/app.test.js | 3 +++ 2 files changed, 10 insertions(+) diff --git a/website/app.js b/website/app.js index e32b3006..0ff482f1 100644 --- a/website/app.js +++ b/website/app.js @@ -2,6 +2,13 @@ const apostrophe = require('apostrophe'); require('dotenv').config({ path: '../.env' }); const { getEnv } = require('./utils/env'); +/** + * Creates the Apostrophe CMS configuration object. + * Configures session (with optional Redis store), base URL, CORS, and all + * modules (express, template, widgets, pieces, etc.). + * + * @returns {object} Apostrophe configuration with shortName, baseUrl, and modules. + */ function createAposConfig() { const redisUri = process.env.REDIS_URI; diff --git a/website/app.test.js b/website/app.test.js index 4315b730..ac18a2c0 100644 --- a/website/app.test.js +++ b/website/app.test.js @@ -1,3 +1,6 @@ +/** + * Tests for website/app.js: createAposConfig and module exports. + */ const { createAposConfig } = require('./app'); const mockConnectRedis = jest.fn(); jest.mock('connect-redis', () => mockConnectRedis);