process.env.DATABASE_PATH = ':memory:'; process.env.THEMES_DIR = './themes'; process.env.FRONTEND_DIST = './frontend/dist'; import { Test } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import { HttpAdapterHost } from '@nestjs/core'; import cookieParser from 'cookie-parser'; import * as express from 'express'; import request from 'supertest'; import { AppModule } from '../../backend/src/app.module'; import { GlobalExceptionFilter } from '../../backend/src/common/filters/global-exception.filter'; import { CsrfMiddleware } from '../../backend/src/common/middleware/csrf.middleware'; import { ConfigService } from '@nestjs/config'; import { initDb } from './db-helper'; import { BlogPostEntity } from '../../backend/src/database/entities/blog-post.entity'; import { getRepositoryToken } from '@nestjs/typeorm'; describe('GET /api/v1/blog/posts (public)', () => { let app: INestApplication; beforeAll(async () => { const moduleRef = await Test.createTestingModule({ imports: [AppModule] }).compile(); app = moduleRef.createNestApplication(); app.use(cookieParser()); app.use(express.json({ limit: '1mb' })); const csrfMw = new CsrfMiddleware(app.get(ConfigService)); app.use((req: any, res: any, next: any) => csrfMw.use(req, res, next)); const httpAdapterHost = app.get(HttpAdapterHost); app.useGlobalFilters(new GlobalExceptionFilter(httpAdapterHost)); await app.init(); await initDb(app); const repo = app.get(getRepositoryToken(BlogPostEntity)); await repo.insert([ { id: 'p1', title: 'Published A', bodyMd: '# Hello', status: 'published', publishedAt: '2026-07-20T10:00:00.000Z', createdAt: '2026-07-19T00:00:00.000Z', updatedAt: '2026-07-20T10:00:00.000Z' }, { id: 'p2', title: 'Draft B', bodyMd: 'should not appear', status: 'draft', publishedAt: null, createdAt: '2026-07-18T00:00:00.000Z', updatedAt: '2026-07-18T00:00:00.000Z' }, { id: 'p3', title: 'Published C', bodyMd: 'safe content', status: 'published', publishedAt: '2026-07-21T10:00:00.000Z', createdAt: '2026-07-21T00:00:00.000Z', updatedAt: '2026-07-21T10:00:00.000Z' }, ]); }); afterAll(async () => { await app.close(); }); it('returns only published posts, ordered by publishedAt DESC', async () => { const server = app.getHttpServer(); const res = await request(server).get('/api/v1/blog/posts').expect(200); expect(res.body.posts).toHaveLength(2); expect(res.body.posts.map((p: any) => p.id)).toEqual(['p3', 'p1']); expect(res.body.posts[0].title).toBe('Published C'); }); it('returns Markdown bodies and does not include HTML fields', async () => { const server = app.getHttpServer(); const res = await request(server).get('/api/v1/blog/posts').expect(200); const c = res.body.posts.find((p: any) => p.id === 'p3'); expect(c.bodyMd).toBe('safe content'); expect(c.bodyHtml).toBeUndefined(); const a = res.body.posts.find((p: any) => p.id === 'p1'); expect(a.bodyMd).toBe('# Hello'); }); it('does not leak non-public fields', async () => { const server = app.getHttpServer(); const res = await request(server).get('/api/v1/blog/posts').expect(200); for (const post of res.body.posts) { expect(post.id).toBeDefined(); expect(post.title).toBeDefined(); expect(post.bodyMd).toBeDefined(); expect(post.publishedAt).toBeDefined(); expect(post.status).toBeUndefined(); } }); it('rejects unrelated endpoints (no admin/scoreboard leakage)', async () => { const server = app.getHttpServer(); await request(server).get('/api/v1/blog/posts/drafts').expect(404); await request(server).get('/api/v1/admin/users').expect(401); }); });