fix: harden auth and investment synchronization
Backend security / security (push) Successful in 8s
Backend verify / verify (push) Successful in 11m9s
Conventional commits / commits (pull_request) Successful in 28s
Backend security / security (pull_request) Successful in 7s
Backend verify / verify (pull_request) Successful in 10m38s
Backend security / security (push) Successful in 8s
Backend verify / verify (push) Successful in 11m9s
Conventional commits / commits (pull_request) Successful in 28s
Backend security / security (pull_request) Successful in 7s
Backend verify / verify (pull_request) Successful in 10m38s
Resolve A01-A10 and C01-C07 from the backend audit. Preserve auth failures, forced candle reconciliation and forecast freshness. Validate MOEX responses and nested HTTP errors; remove unused legacy paths. Verified: 209 unit tests, 262 SQLite e2e tests on Linux, OpenAPI and durability. Record the remaining macOS native SQLite abort in the audit.
This commit is contained in:
+125
-11
@@ -1,6 +1,7 @@
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { PinoLogger } from 'nestjs-pino';
|
||||
import request from 'supertest';
|
||||
import { App } from 'supertest/types';
|
||||
import { AppModule } from '../src/app.module';
|
||||
@@ -28,7 +29,10 @@ describe('Authentication (e2e)', () => {
|
||||
await app.get(PrismaService).user.deleteMany();
|
||||
});
|
||||
|
||||
afterEach(() => app.close());
|
||||
afterEach(async () => {
|
||||
await app.close();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('registers, guards profile, rotates refresh and logs out', async () => {
|
||||
const agent = request.agent(app.getHttpServer());
|
||||
@@ -50,14 +54,94 @@ describe('Authentication (e2e)', () => {
|
||||
);
|
||||
|
||||
const firstCookie = registration.headers['set-cookie'][0];
|
||||
await agent.post('/api/v1/auth/refresh').expect(200);
|
||||
const refreshed = await agent.post('/api/v1/auth/refresh').expect(200);
|
||||
await request(app.getHttpServer())
|
||||
.post('/api/v1/auth/refresh')
|
||||
.set('Cookie', firstCookie)
|
||||
.expect(401)
|
||||
.expect('Content-Type', /application\/problem\+json/);
|
||||
const logout = await agent.post('/api/v1/auth/logout').expect(204);
|
||||
expect(logout.headers['set-cookie'][0]).toContain('refresh_token=;');
|
||||
await request(app.getHttpServer())
|
||||
.post('/api/v1/auth/refresh')
|
||||
.set('Cookie', refreshed.headers['set-cookie'][0])
|
||||
.expect(401)
|
||||
.expect(({ body }: { body: unknown }) =>
|
||||
expect(body).toEqual({ status: 401, code: 'UNAUTHORIZED' }),
|
||||
);
|
||||
await request(app.getHttpServer())
|
||||
.post('/api/v1/auth/logout')
|
||||
.set('Cookie', refreshed.headers['set-cookie'][0])
|
||||
.expect(204);
|
||||
await agent.post('/api/v1/auth/logout').expect(204);
|
||||
await agent.post('/api/v1/auth/logout').expect(204);
|
||||
});
|
||||
|
||||
it('keeps logout idempotent for an invalid refresh token', async () => {
|
||||
const logout = await request(app.getHttpServer())
|
||||
.post('/api/v1/auth/logout')
|
||||
.set('Cookie', 'refresh_token=invalid')
|
||||
.expect(204);
|
||||
|
||||
expect(logout.headers['set-cookie'][0]).toContain('refresh_token=;');
|
||||
});
|
||||
|
||||
it('reports a failed revocation and preserves the refresh cookie for retry', async () => {
|
||||
const registration = await request(app.getHttpServer())
|
||||
.post('/api/v1/auth/register')
|
||||
.send({ email: 'logout@example.test', password: 'a secure password' })
|
||||
.expect(200);
|
||||
const cookie = registration.headers['set-cookie'][0];
|
||||
const revoke = jest
|
||||
.spyOn(app.get(PrismaService).session, 'update')
|
||||
.mockRejectedValueOnce(new Error('Storage unavailable'));
|
||||
|
||||
const failed = await request(app.getHttpServer())
|
||||
.post('/api/v1/auth/logout')
|
||||
.set('Cookie', cookie)
|
||||
.expect(500)
|
||||
.expect('Content-Type', /application\/problem\+json/);
|
||||
revoke.mockRestore();
|
||||
|
||||
expect(failed.body).toEqual({
|
||||
status: 500,
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
});
|
||||
expect(failed.headers['set-cookie']).toBeUndefined();
|
||||
expect(
|
||||
await app.get(PrismaService).session.findFirstOrThrow(),
|
||||
).toMatchObject({ revokedAt: null });
|
||||
await request(app.getHttpServer())
|
||||
.post('/api/v1/auth/refresh')
|
||||
.set('Cookie', cookie)
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it('preserves a valid refresh cookie during a storage failure', async () => {
|
||||
const registration = await request(app.getHttpServer())
|
||||
.post('/api/v1/auth/register')
|
||||
.send({ email: 'refresh@example.test', password: 'a secure password' })
|
||||
.expect(200);
|
||||
const cookie = registration.headers['set-cookie'][0];
|
||||
const read = jest
|
||||
.spyOn(app.get(PrismaService).session, 'findUnique')
|
||||
.mockRejectedValueOnce(new Error('Storage unavailable'));
|
||||
|
||||
const failed = await request(app.getHttpServer())
|
||||
.post('/api/v1/auth/refresh')
|
||||
.set('Cookie', cookie)
|
||||
.expect(500)
|
||||
.expect('Content-Type', /application\/problem\+json/);
|
||||
read.mockRestore();
|
||||
|
||||
expect(failed.body).toEqual({
|
||||
status: 500,
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
});
|
||||
expect(failed.headers['set-cookie']).toBeUndefined();
|
||||
await request(app.getHttpServer())
|
||||
.post('/api/v1/auth/refresh')
|
||||
.set('Cookie', cookie)
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it('rotates one concurrent refresh on SQLite and keeps the winner usable', async () => {
|
||||
@@ -141,6 +225,26 @@ describe('Authentication (e2e)', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('returns field errors for invalid credentials without exposing input', async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.post('/api/v1/auth/register')
|
||||
.send({ email: 'SYNTHETIC_VALIDATION_SECRET' })
|
||||
.expect(400)
|
||||
.expect('Content-Type', /application\/problem\+json/);
|
||||
expect(response.body).toEqual({
|
||||
status: 400,
|
||||
code: 'VALIDATION_ERROR',
|
||||
errors: expect.arrayContaining([
|
||||
{ pointer: '#/email', detail: 'must be a valid email address' },
|
||||
{ pointer: '#/password', detail: 'password must be a string' },
|
||||
]) as unknown,
|
||||
});
|
||||
expect(JSON.stringify(response.body).toLowerCase()).not.toContain(
|
||||
'synthetic_validation_secret',
|
||||
);
|
||||
expect(await app.get(PrismaService).user.count()).toBe(0);
|
||||
});
|
||||
|
||||
it('guards T-Bank connection and never returns a token', async () => {
|
||||
const registration = await request(app.getHttpServer())
|
||||
.post('/api/v1/auth/register')
|
||||
@@ -218,27 +322,37 @@ describe('Authentication (e2e)', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('records the public code and stack for an unhandled error', async () => {
|
||||
const logger = app.get(StructuredLogger);
|
||||
const event = jest.spyOn(logger, 'event');
|
||||
it('logs an unhandled error with safe public context and correlation', async () => {
|
||||
const error = jest.spyOn(PinoLogger.prototype, 'error');
|
||||
const clientRequestId = 'a2dc6fbb-5a48-4c3c-9e3e-4fbf4d638a43';
|
||||
jest
|
||||
.spyOn(app.get(AuthService), 'login')
|
||||
.mockRejectedValue(new Error('password: secret'));
|
||||
.mockRejectedValue(
|
||||
new Error('operation failed\npassword: SYNTHETIC_AUDIT_SECRET'),
|
||||
);
|
||||
|
||||
await request(app.getHttpServer())
|
||||
const response = await request(app.getHttpServer())
|
||||
.post('/api/v1/auth/login')
|
||||
.set('X-Request-ID', clientRequestId)
|
||||
.send({ email: 'user@example.com', password: 'a secure password' })
|
||||
.expect(500)
|
||||
.expect('Content-Type', /application\/problem\+json/)
|
||||
.expect(({ body }: { body: unknown }) =>
|
||||
expect(body).toEqual({ status: 500, code: 'INTERNAL_SERVER_ERROR' }),
|
||||
);
|
||||
|
||||
expect(event).toHaveBeenCalledWith(
|
||||
'error',
|
||||
'http.request.completed',
|
||||
expect(error).toHaveBeenCalledTimes(1);
|
||||
expect(error).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
requestId: response.headers['x-request-id'],
|
||||
clientRequestId,
|
||||
status: 500,
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
}),
|
||||
'http.request.completed',
|
||||
);
|
||||
expect(JSON.stringify(error.mock.calls)).not.toContain(
|
||||
'SYNTHETIC_AUDIT_SECRET',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user