Skip to content

Commit f486ad0

Browse files
ZionApprove1Judah
authored andcommitted
fix(backend): correct csrf test assertion and mock UsageTrackerService in onchain module test
- Fix csrf.middleware.spec.ts: httpOnly should be false (not true) to match implementation - Fix onchain.module.spec.ts: override UsageTrackerService to resolve RedisService dependency - Apply prettier formatting to csrf.middleware.ts
1 parent 337138b commit f486ad0

3 files changed

Lines changed: 90 additions & 20 deletions

File tree

app/backend/src/common/security/__tests__/csrf.middleware.spec.ts

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
1-
import { CsrfMiddleware, generateCsrfToken, CSRF_COOKIE_NAME, CSRF_HEADER_NAME } from '../csrf.middleware';
1+
import {
2+
CsrfMiddleware,
3+
generateCsrfToken,
4+
CSRF_COOKIE_NAME,
5+
CSRF_HEADER_NAME,
6+
} from '../csrf.middleware';
27
import { ConfigService } from '@nestjs/config';
38
import type { Request, Response } from 'express';
49

5-
const createMockConfig = (csrfEnabled: string, nodeEnv = 'test'): jest.Mocked<ConfigService> =>
10+
const createMockConfig = (
11+
csrfEnabled: string,
12+
nodeEnv = 'test',
13+
): jest.Mocked<ConfigService> =>
614
({
715
get: jest.fn(<T = any>(key: string, defaultValue?: T): T | undefined => {
816
if (key === 'CSRF_PROTECTION_ENABLED') return csrfEnabled as unknown as T;
@@ -25,7 +33,12 @@ const createReq = (overrides: Partial<Request> = {}): Partial<Request> => {
2533
const createRes = (): Partial<Response> => {
2634
const res: Partial<Response> & { headers: Record<string, string> } = {
2735
headers: {},
28-
cookie: jest.fn().mockImplementation(function(this: any, name: string, _val: string, _opts?: any) {
36+
cookie: jest.fn().mockImplementation(function (
37+
this: any,
38+
name: string,
39+
_val: string,
40+
_opts?: any,
41+
) {
2942
this.headers[name] = _val;
3043
}),
3144
status: jest.fn().mockReturnThis(),
@@ -56,7 +69,11 @@ describe('CsrfMiddleware', () => {
5669
const res = createRes() as Response;
5770
const next = createNext();
5871
middleware.use(req, res, next);
59-
expect(res.cookie).toHaveBeenCalledWith(CSRF_COOKIE_NAME, expect.any(String), expect.objectContaining({ httpOnly: true, path: '/' }));
72+
expect(res.cookie).toHaveBeenCalledWith(
73+
CSRF_COOKIE_NAME,
74+
expect.any(String),
75+
expect.objectContaining({ httpOnly: false, path: '/' }),
76+
);
6077
expect(next).toHaveBeenCalled();
6178
});
6279
});
@@ -69,51 +86,78 @@ describe('CsrfMiddleware', () => {
6986
const res = createRes() as Response;
7087
const next = createNext();
7188
middleware.use(req, res, next);
72-
expect(res.cookie).toHaveBeenCalledWith(CSRF_COOKIE_NAME, expect.any(String), expect.any(Object));
89+
expect(res.cookie).toHaveBeenCalledWith(
90+
CSRF_COOKIE_NAME,
91+
expect.any(String),
92+
expect.any(Object),
93+
);
7394
expect(next).toHaveBeenCalled();
7495
});
7596

7697
it('should reuse existing token cookie on GET if already present', () => {
7798
const config = createMockConfig('true');
7899
const middleware = new CsrfMiddleware(config as unknown as ConfigService);
79100
const existingToken = generateCsrfToken();
80-
const req = createReq({ method: 'GET', cookies: { [CSRF_COOKIE_NAME]: existingToken } }) as Request;
101+
const req = createReq({
102+
method: 'GET',
103+
cookies: { [CSRF_COOKIE_NAME]: existingToken },
104+
}) as Request;
81105
const res = createRes() as Response;
82106
const next = createNext();
83107
middleware.use(req, res, next);
84-
expect(res.cookie).toHaveBeenCalledWith(CSRF_COOKIE_NAME, existingToken, expect.any(Object));
108+
expect(res.cookie).toHaveBeenCalledWith(
109+
CSRF_COOKIE_NAME,
110+
existingToken,
111+
expect.any(Object),
112+
);
85113
expect(next).toHaveBeenCalled();
86114
});
87115

88116
it('should return 403 on POST when cookie is missing', () => {
89117
const config = createMockConfig('true');
90118
const middleware = new CsrfMiddleware(config as unknown as ConfigService);
91-
const req = createReq({ method: 'POST', cookies: {}, headers: { [CSRF_HEADER_NAME]: 'some-token' } }) as Request;
119+
const req = createReq({
120+
method: 'POST',
121+
cookies: {},
122+
headers: { [CSRF_HEADER_NAME]: 'some-token' },
123+
}) as Request;
92124
const res = createRes() as Response;
93125
const next = createNext();
94126
middleware.use(req, res, next);
95127
expect(res.status).toHaveBeenCalledWith(403);
96-
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ message: 'CSRF token missing' }));
128+
expect(res.json).toHaveBeenCalledWith(
129+
expect.objectContaining({ message: 'CSRF token missing' }),
130+
);
97131
expect(next).not.toHaveBeenCalled();
98132
});
99133

100134
it('should return 403 on POST when header is missing', () => {
101135
const config = createMockConfig('true');
102136
const middleware = new CsrfMiddleware(config as unknown as ConfigService);
103137
const token = generateCsrfToken();
104-
const req = createReq({ method: 'POST', cookies: { [CSRF_COOKIE_NAME]: token }, headers: {} }) as Request;
138+
const req = createReq({
139+
method: 'POST',
140+
cookies: { [CSRF_COOKIE_NAME]: token },
141+
headers: {},
142+
}) as Request;
105143
const res = createRes() as Response;
106144
const next = createNext();
107145
middleware.use(req, res, next);
108146
expect(res.status).toHaveBeenCalledWith(403);
109-
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ message: 'CSRF token missing' }));
147+
expect(res.json).toHaveBeenCalledWith(
148+
expect.objectContaining({ message: 'CSRF token missing' }),
149+
);
110150
expect(next).not.toHaveBeenCalled();
111151
});
112152

113153
it('should return 403 on POST when tokens do not match', () => {
114154
const config = createMockConfig('true');
115155
const middleware = new CsrfMiddleware(config as unknown as ConfigService);
116-
const req = createReq({ method: 'POST', cookies: { [CSRF_COOKIE_NAME]: generateCsrfToken() }, headers: { [CSRF_HEADER_NAME]: 'different-token-value' } }) as Request;
156+
const req = createReq({
157+
method: 'POST',
158+
cookies: { [CSRF_COOKIE_NAME]: generateCsrfToken() },
159+
headers: { [CSRF_HEADER_NAME]: 'different-token-value' },
160+
}) as Request;
117161
const res = createRes() as Response;
118162
const next = createNext();
119163
middleware.use(req, res, next);
@@ -125,7 +169,11 @@ describe('CsrfMiddleware', () => {
125169
const config = createMockConfig('true');
126170
const middleware = new CsrfMiddleware(config as unknown as ConfigService);
127171
const token = generateCsrfToken();
128-
const req = createReq({ method: 'POST', cookies: { [CSRF_COOKIE_NAME]: token }, headers: { [CSRF_HEADER_NAME]: token } }) as Request;
172+
const req = createReq({
173+
method: 'POST',
174+
cookies: { [CSRF_COOKIE_NAME]: token },
175+
headers: { [CSRF_HEADER_NAME]: token },
176+
}) as Request;
129177
const res = createRes() as Response;
130178
const next = createNext();
131179
middleware.use(req, res, next);
@@ -137,7 +185,11 @@ describe('CsrfMiddleware', () => {
137185
const config = createMockConfig('true');
138186
const middleware = new CsrfMiddleware(config as unknown as ConfigService);
139187
const token = generateCsrfToken();
140-
const req = createReq({ method: 'DELETE', cookies: { [CSRF_COOKIE_NAME]: token }, headers: { [CSRF_HEADER_NAME]: token } }) as Request;
188+
const req = createReq({
189+
method: 'DELETE',
190+
cookies: { [CSRF_COOKIE_NAME]: token },
191+
headers: { [CSRF_HEADER_NAME]: token },
192+
}) as Request;
141193
const res = createRes() as Response;
142194
const next = createNext();
143195
middleware.use(req, res, next);

app/backend/src/common/security/csrf.middleware.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,12 @@ export class CsrfMiddleware implements NestMiddleware {
4848
};
4949

5050
constructor(config: ConfigService) {
51-
this.enabled = config.get<string>('CSRF_PROTECTION_ENABLED', 'false') === 'true';
51+
this.enabled =
52+
config.get<string>('CSRF_PROTECTION_ENABLED', 'false') === 'true';
5253
const isProduction = config.get<string>('NODE_ENV') === 'production';
5354

5455
this.cookieOptions = {
55-
httpOnly: false, // Client-side JS must read via document.cookie to set X-CSRF-Token header
56+
httpOnly: false, // Client-side JS must read via document.cookie to set X-CSRF-Token header
5657
sameSite: isProduction ? 'strict' : 'lax',
5758
secure: isProduction,
5859
path: '/',
@@ -81,14 +82,22 @@ export class CsrfMiddleware implements NestMiddleware {
8182
if (SAFE_METHODS.has(method)) {
8283
// Ensure every response carries a token cookie for subsequent
8384
// state-changing requests issued by the same origin.
84-
const existing = (req.cookies as Record<string, string>)?.[CSRF_COOKIE_NAME] as string | undefined;
85-
res.cookie(CSRF_COOKIE_NAME, existing ?? generateCsrfToken(), this.cookieOptions);
85+
const existing = (req.cookies as Record<string, string>)?.[
86+
CSRF_COOKIE_NAME
87+
] as string | undefined;
88+
res.cookie(
89+
CSRF_COOKIE_NAME,
90+
existing ?? generateCsrfToken(),
91+
this.cookieOptions,
92+
);
8693
next();
8794
return;
8895
}
8996

9097
if (STATE_CHANGING_METHODS.has(method)) {
91-
const cookieToken = (req.cookies as Record<string, string>)?.[CSRF_COOKIE_NAME] as string | undefined;
98+
const cookieToken = (req.cookies as Record<string, string>)?.[
99+
CSRF_COOKIE_NAME
100+
] as string | undefined;
92101
const headerToken = req.headers[CSRF_HEADER_NAME] as string | undefined;
93102

94103
if (!cookieToken || !headerToken) {

app/backend/src/onchain/onchain.module.spec.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ import { OnchainAdapter } from './onchain.adapter';
99
import { MockOnchainAdapter } from './onchain.adapter.mock';
1010
import { SorobanAdapter } from './soroban.adapter';
1111
import { PrismaModule } from '../prisma/prisma.module';
12+
import { UsageTrackerService } from '../observability/usage-tracker/usage-tracker.service';
13+
14+
const mockUsageTrackerService = {
15+
recordUsage: jest.fn(),
16+
detectAndReportAnomalies: jest.fn().mockResolvedValue([]),
17+
};
1218

1319
describe('OnchainModule', () => {
1420
let module: TestingModule;
@@ -23,7 +29,10 @@ describe('OnchainModule', () => {
2329
PrismaModule,
2430
OnchainModule,
2531
],
26-
}).compile();
32+
})
33+
.overrideProvider(UsageTrackerService)
34+
.useValue(mockUsageTrackerService)
35+
.compile();
2736

2837
_configService = module.get<ConfigService>(ConfigService);
2938
});

0 commit comments

Comments
 (0)