Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions graphql/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"backend"
],
"dependencies": {
"@constructive-io/csrf": "workspace:^",
"@constructive-io/graphql-env": "workspace:^",
"@constructive-io/graphql-types": "workspace:^",
"@constructive-io/s3-utils": "workspace:^",
Expand All @@ -53,6 +54,10 @@
"@pgpmjs/server-utils": "workspace:^",
"@pgpmjs/types": "workspace:^",
"@pgsql/quotes": "^17.1.0",
"@types/cookie": "^1.0.0",
"@types/on-headers": "^1.0.4",
"cookie": "^1.1.1",
"cookie-parser": "^1.4.7",
"cors": "^2.8.6",
"deepmerge": "^4.3.1",
"express": "^5.2.1",
Expand All @@ -69,6 +74,7 @@
"graphql-upload": "^13.0.0",
"lru-cache": "^11.2.7",
"multer": "^2.1.1",
"on-headers": "^1.1.0",
"pg": "^8.20.0",
"pg-cache": "workspace:^",
"pg-env": "workspace:^",
Expand All @@ -79,6 +85,7 @@
},
"devDependencies": {
"@aws-sdk/client-s3": "^3.1009.0",
"@types/cookie-parser": "^1.4.8",
"@types/cors": "^2.8.17",
"@types/express": "^5.0.6",
"@types/graphql-upload": "^8.0.12",
Expand Down
147 changes: 147 additions & 0 deletions graphql/server/src/middleware/__tests__/cookie.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import {
buildCookieOptions,
buildDeviceCookieOptions,
setSessionCookie,
clearSessionCookie,
setDeviceTokenCookie,
SESSION_COOKIE_NAME,
DEVICE_COOKIE_NAME,
AuthCookieSettings,
} from '../cookie';

describe('cookie helpers', () => {
describe('buildCookieOptions', () => {
it('should return default options when no settings provided', () => {
const options = buildCookieOptions();

expect(options.httpOnly).toBe(true);
expect(options.sameSite).toBe('lax');
expect(options.path).toBe('/');
expect(options.maxAge).toBe(3600 * 1000); // 1 hour in ms
});

it('should use settings when provided', () => {
const settings: AuthCookieSettings = {
cookieSecure: true,
cookieSameSite: 'strict',
cookieDomain: '.example.com',
cookiePath: '/api',
defaultSessionDuration: '2 hours',
};

const options = buildCookieOptions(settings);

expect(options.secure).toBe(true);
expect(options.sameSite).toBe('strict');
expect(options.domain).toBe('.example.com');
expect(options.path).toBe('/api');
expect(options.maxAge).toBe(2 * 3600 * 1000); // 2 hours in ms
});

it('should use rememberMeDuration when rememberMe is true', () => {
const settings: AuthCookieSettings = {
defaultSessionDuration: '1 hour',
rememberMeDuration: '30 days',
};

const options = buildCookieOptions(settings, true);

expect(options.maxAge).toBe(30 * 24 * 3600 * 1000); // 30 days in ms
});

it('should parse various interval formats', () => {
expect(buildCookieOptions({ defaultSessionDuration: '30 seconds' }).maxAge).toBe(30 * 1000);
expect(buildCookieOptions({ defaultSessionDuration: '5 minutes' }).maxAge).toBe(5 * 60 * 1000);
expect(buildCookieOptions({ defaultSessionDuration: '2 hours' }).maxAge).toBe(2 * 3600 * 1000);
expect(buildCookieOptions({ defaultSessionDuration: '7 days' }).maxAge).toBe(7 * 86400 * 1000);
expect(buildCookieOptions({ defaultSessionDuration: '1 week' }).maxAge).toBe(604800 * 1000);
});
});

describe('buildDeviceCookieOptions', () => {
it('should return long-lived cookie options', () => {
const options = buildDeviceCookieOptions();

expect(options.httpOnly).toBe(true);
expect(options.sameSite).toBe('lax');
expect(options.maxAge).toBe(90 * 24 * 3600 * 1000); // 90 days in ms
});
});

describe('setSessionCookie', () => {
it('should call res.cookie with correct arguments', () => {
const mockRes = {
cookie: jest.fn(),
} as any;

setSessionCookie(mockRes, 'test-token', undefined, false);

expect(mockRes.cookie).toHaveBeenCalledWith(
SESSION_COOKIE_NAME,
'test-token',
expect.objectContaining({
httpOnly: true,
sameSite: 'lax',
})
);
});

it('should use rememberMe duration when true', () => {
const mockRes = {
cookie: jest.fn(),
} as any;

const settings: AuthCookieSettings = {
rememberMeDuration: '30 days',
};

setSessionCookie(mockRes, 'test-token', settings, true);

expect(mockRes.cookie).toHaveBeenCalledWith(
SESSION_COOKIE_NAME,
'test-token',
expect.objectContaining({
maxAge: 30 * 24 * 3600 * 1000,
})
);
});
});

describe('clearSessionCookie', () => {
it('should call res.clearCookie with correct arguments', () => {
const mockRes = {
clearCookie: jest.fn(),
} as any;

clearSessionCookie(mockRes);

expect(mockRes.clearCookie).toHaveBeenCalledWith(
SESSION_COOKIE_NAME,
expect.objectContaining({
httpOnly: true,
sameSite: 'lax',
path: '/',
})
);
});
});

describe('setDeviceTokenCookie', () => {
it('should call res.cookie with long-lived options', () => {
const mockRes = {
cookie: jest.fn(),
} as any;

setDeviceTokenCookie(mockRes, 'device-token-123');

expect(mockRes.cookie).toHaveBeenCalledWith(
DEVICE_COOKIE_NAME,
'device-token-123',
expect.objectContaining({
httpOnly: true,
maxAge: 90 * 24 * 3600 * 1000,
})
);
});
});
});
Loading
Loading