mirror change from https://github.com/binhkid2/FullStack-Blog-Nestjs-Nextjs-Postgres
This commit is contained in:
25
backend/test/app.e2e-spec.ts
Normal file
25
backend/test/app.e2e-spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import request from 'supertest';
|
||||
import { App } from 'supertest/types';
|
||||
import { AppModule } from './../src/app.module';
|
||||
|
||||
describe('AppController (e2e)', () => {
|
||||
let app: INestApplication<App>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleFixture.createNestApplication();
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it('/ (GET)', () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/')
|
||||
.expect(200)
|
||||
.expect('Hello World!');
|
||||
});
|
||||
});
|
||||
141
backend/test/auth.e2e-spec.ts
Normal file
141
backend/test/auth.e2e-spec.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { INestApplication, ValidationPipe } from '@nestjs/common';
|
||||
import * as request from 'supertest';
|
||||
import * as cookieParser from 'cookie-parser';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { AppModule } from '../src/app.module';
|
||||
import { User } from '../src/users/entities/user.entity';
|
||||
|
||||
describe('Auth (e2e)', () => {
|
||||
let app: INestApplication;
|
||||
let userRepo: Repository<User>;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleFixture.createNestApplication();
|
||||
app.use(cookieParser());
|
||||
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
|
||||
await app.init();
|
||||
|
||||
userRepo = moduleFixture.get<Repository<User>>(getRepositoryToken(User));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
const testEmail = `test-${Date.now()}@example.com`;
|
||||
const testPassword = 'Test1234!';
|
||||
|
||||
describe('POST /auth/register', () => {
|
||||
it('should register a new user and return tokens', async () => {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post('/auth/register')
|
||||
.send({ email: testEmail, password: testPassword, name: 'Test User' })
|
||||
.expect(201);
|
||||
|
||||
expect(res.body.success).toBe(true);
|
||||
expect(res.body.accessToken).toBeDefined();
|
||||
expect(res.body.user.email).toBe(testEmail);
|
||||
expect(res.body.user.passwordHash).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should reject duplicate email', async () => {
|
||||
await request(app.getHttpServer())
|
||||
.post('/auth/register')
|
||||
.send({ email: testEmail, password: testPassword })
|
||||
.expect(409);
|
||||
});
|
||||
|
||||
it('should reject short password', async () => {
|
||||
await request(app.getHttpServer())
|
||||
.post('/auth/register')
|
||||
.send({ email: 'new@example.com', password: '123' })
|
||||
.expect(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /auth/login', () => {
|
||||
it('should login with correct credentials', async () => {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post('/auth/login')
|
||||
.send({ email: testEmail, password: testPassword })
|
||||
.expect(201);
|
||||
|
||||
expect(res.body.success).toBe(true);
|
||||
expect(res.body.accessToken).toBeDefined();
|
||||
|
||||
// Cookies should be set
|
||||
const cookies = res.headers['set-cookie'] as string[];
|
||||
expect(cookies).toBeDefined();
|
||||
const hasAccessCookie = cookies.some((c) => c.startsWith('accessToken='));
|
||||
expect(hasAccessCookie).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject wrong password', async () => {
|
||||
await request(app.getHttpServer())
|
||||
.post('/auth/login')
|
||||
.send({ email: testEmail, password: 'wrongpassword' })
|
||||
.expect(401);
|
||||
});
|
||||
|
||||
it('should reject non-existent email', async () => {
|
||||
await request(app.getHttpServer())
|
||||
.post('/auth/login')
|
||||
.send({ email: 'nonexistent@example.com', password: testPassword })
|
||||
.expect(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /auth/refresh', () => {
|
||||
it('should rotate refresh token', async () => {
|
||||
// First login to get refresh token
|
||||
const loginRes = await request(app.getHttpServer())
|
||||
.post('/auth/login')
|
||||
.send({ email: testEmail, password: testPassword });
|
||||
|
||||
const cookies = loginRes.headers['set-cookie'] as string[];
|
||||
const refreshCookie = cookies.find((c) => c.startsWith('refreshToken='));
|
||||
expect(refreshCookie).toBeDefined();
|
||||
|
||||
// Use refresh token
|
||||
const refreshRes = await request(app.getHttpServer())
|
||||
.post('/auth/refresh')
|
||||
.set('Cookie', cookies)
|
||||
.expect(201);
|
||||
|
||||
expect(refreshRes.body.success).toBe(true);
|
||||
expect(refreshRes.body.accessToken).toBeDefined();
|
||||
});
|
||||
|
||||
it('should reject missing refresh token', async () => {
|
||||
await request(app.getHttpServer())
|
||||
.post('/auth/refresh')
|
||||
.expect(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /auth/magic-link', () => {
|
||||
it('should accept magic link request for existing email', async () => {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post('/auth/magic-link')
|
||||
.send({ email: testEmail })
|
||||
.expect(201);
|
||||
|
||||
expect(res.body.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should accept magic link request for non-existing email (no enumeration)', async () => {
|
||||
const res = await request(app.getHttpServer())
|
||||
.post('/auth/magic-link')
|
||||
.send({ email: 'nobody@nowhere.com' })
|
||||
.expect(201);
|
||||
|
||||
expect(res.body.success).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
12
backend/test/jest-e2e.json
Normal file
12
backend/test/jest-e2e.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"moduleFileExtensions": ["js", "json", "ts"],
|
||||
"rootDir": ".",
|
||||
"testEnvironment": "node",
|
||||
"testRegex": ".e2e-spec.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"transformIgnorePatterns": [
|
||||
"/node_modules/(?!(uuid)/)"
|
||||
]
|
||||
}
|
||||
133
backend/test/public-posts.e2e-spec.ts
Normal file
133
backend/test/public-posts.e2e-spec.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { INestApplication, ValidationPipe } from '@nestjs/common';
|
||||
import * as request from 'supertest';
|
||||
import * as cookieParser from 'cookie-parser';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { AppModule } from '../src/app.module';
|
||||
import { BlogPost, PostStatus, ContentFormat } from '../src/blog-posts/entities/blog-post.entity';
|
||||
import { User, UserRole } from '../src/users/entities/user.entity';
|
||||
|
||||
describe('Public Posts (e2e)', () => {
|
||||
let app: INestApplication;
|
||||
let postRepo: Repository<BlogPost>;
|
||||
let userRepo: Repository<User>;
|
||||
let testAuthorId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleFixture.createNestApplication();
|
||||
app.use(cookieParser());
|
||||
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
|
||||
await app.init();
|
||||
|
||||
postRepo = moduleFixture.get<Repository<BlogPost>>(getRepositoryToken(BlogPost));
|
||||
userRepo = moduleFixture.get<Repository<User>>(getRepositoryToken(User));
|
||||
|
||||
// Create test author
|
||||
const author = userRepo.create({
|
||||
id: uuidv4(),
|
||||
email: `author-${Date.now()}@test.com`,
|
||||
role: UserRole.ADMIN,
|
||||
isActive: true,
|
||||
});
|
||||
await userRepo.save(author);
|
||||
testAuthorId = author.id;
|
||||
|
||||
// Create test posts
|
||||
await postRepo.save([
|
||||
postRepo.create({
|
||||
id: uuidv4(),
|
||||
title: 'Published Test Post',
|
||||
slug: `published-test-${Date.now()}`,
|
||||
status: PostStatus.PUBLISHED,
|
||||
excerpt: 'A published post for testing',
|
||||
content: 'Test content',
|
||||
contentFormat: ContentFormat.MARKDOWN,
|
||||
authorId: testAuthorId,
|
||||
tags: ['test', 'published'],
|
||||
categories: ['Test'],
|
||||
}),
|
||||
postRepo.create({
|
||||
id: uuidv4(),
|
||||
title: 'Draft Test Post',
|
||||
slug: `draft-test-${Date.now()}`,
|
||||
status: PostStatus.DRAFT,
|
||||
excerpt: 'A draft post',
|
||||
content: 'Draft content',
|
||||
contentFormat: ContentFormat.MARKDOWN,
|
||||
authorId: testAuthorId,
|
||||
tags: ['test', 'draft'],
|
||||
categories: ['Test'],
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
describe('GET /blog-posts/public', () => {
|
||||
it('should return only published posts', async () => {
|
||||
const res = await request(app.getHttpServer())
|
||||
.get('/blog-posts/public')
|
||||
.expect(200);
|
||||
|
||||
expect(Array.isArray(res.body.posts)).toBe(true);
|
||||
const allPublished = res.body.posts.every((p: any) => p.status === 'published');
|
||||
expect(allPublished).toBe(true);
|
||||
});
|
||||
|
||||
it('should support search by query', async () => {
|
||||
const res = await request(app.getHttpServer())
|
||||
.get('/blog-posts/public?q=Published+Test')
|
||||
.expect(200);
|
||||
|
||||
expect(res.body.posts.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('should support pagination', async () => {
|
||||
const res = await request(app.getHttpServer())
|
||||
.get('/blog-posts/public?page=1&pageSize=2')
|
||||
.expect(200);
|
||||
|
||||
expect(res.body.posts.length).toBeLessThanOrEqual(2);
|
||||
expect(res.body.page).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /blog-posts/public/:slug', () => {
|
||||
it('should return a published post by slug', async () => {
|
||||
// First get a published post slug
|
||||
const listRes = await request(app.getHttpServer()).get('/blog-posts/public');
|
||||
const slug = listRes.body.posts[0]?.slug;
|
||||
expect(slug).toBeDefined();
|
||||
|
||||
const res = await request(app.getHttpServer())
|
||||
.get(`/blog-posts/public/${slug}`)
|
||||
.expect(200);
|
||||
|
||||
expect(res.body.post.slug).toBe(slug);
|
||||
});
|
||||
|
||||
it('should return 404 for draft post', async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get(`/blog-posts/public/draft-test-99999`)
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /blog-posts/public/featured', () => {
|
||||
it('should return featured published posts', async () => {
|
||||
const res = await request(app.getHttpServer())
|
||||
.get('/blog-posts/public/featured')
|
||||
.expect(200);
|
||||
|
||||
expect(Array.isArray(res.body)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
125
backend/test/rbac.e2e-spec.ts
Normal file
125
backend/test/rbac.e2e-spec.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { INestApplication, ValidationPipe } from '@nestjs/common';
|
||||
import * as request from 'supertest';
|
||||
import * as cookieParser from 'cookie-parser';
|
||||
import { AppModule } from '../src/app.module';
|
||||
|
||||
describe('RBAC (e2e)', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
// We'll store cookies per user role
|
||||
let adminCookies: string[];
|
||||
let managerCookies: string[];
|
||||
let memberCookies: string[];
|
||||
|
||||
const ts = Date.now();
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleFixture.createNestApplication();
|
||||
app.use(cookieParser());
|
||||
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
|
||||
await app.init();
|
||||
|
||||
// Register three users, then promote via direct DB or use existing seed
|
||||
// For simplicity, register all as MEMBER — we test permission differences
|
||||
|
||||
const adminEmail = `admin-rbac-${ts}@test.com`;
|
||||
const managerEmail = `manager-rbac-${ts}@test.com`;
|
||||
const memberEmail = `member-rbac-${ts}@test.com`;
|
||||
const pw = 'RbacTest123!';
|
||||
|
||||
// Register all
|
||||
await request(app.getHttpServer())
|
||||
.post('/auth/register')
|
||||
.send({ email: adminEmail, password: pw });
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post('/auth/register')
|
||||
.send({ email: managerEmail, password: pw });
|
||||
|
||||
const memberReg = await request(app.getHttpServer())
|
||||
.post('/auth/register')
|
||||
.send({ email: memberEmail, password: pw });
|
||||
|
||||
// Store member cookies
|
||||
memberCookies = memberReg.headers['set-cookie'] as string[];
|
||||
|
||||
// Login as the newly registered users
|
||||
const adminLogin = await request(app.getHttpServer())
|
||||
.post('/auth/login')
|
||||
.send({ email: adminEmail, password: pw });
|
||||
adminCookies = adminLogin.headers['set-cookie'] as string[];
|
||||
|
||||
const managerLogin = await request(app.getHttpServer())
|
||||
.post('/auth/login')
|
||||
.send({ email: managerEmail, password: pw });
|
||||
managerCookies = managerLogin.headers['set-cookie'] as string[];
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
describe('MEMBER permissions', () => {
|
||||
it('should NOT be able to create a blog post', async () => {
|
||||
await request(app.getHttpServer())
|
||||
.post('/blog-posts')
|
||||
.set('Cookie', memberCookies)
|
||||
.send({ title: 'Member Post', content: 'Content' })
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
it('should NOT be able to list users', async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get('/users')
|
||||
.set('Cookie', memberCookies)
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
it('should be able to view dashboard', async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get('/dashboard')
|
||||
.set('Cookie', memberCookies)
|
||||
.expect(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Unauthenticated access', () => {
|
||||
it('should allow access to public posts', async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get('/blog-posts/public')
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it('should deny access to dashboard', async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get('/dashboard')
|
||||
.expect(401);
|
||||
});
|
||||
|
||||
it('should deny access to users list', async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get('/users')
|
||||
.expect(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Public endpoints', () => {
|
||||
it('GET /health should be publicly accessible', async () => {
|
||||
const res = await request(app.getHttpServer())
|
||||
.get('/health')
|
||||
.expect(200);
|
||||
expect(res.body.status).toBe('ok');
|
||||
});
|
||||
|
||||
it('GET / should be publicly accessible', async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get('/')
|
||||
.expect(200);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user