const request = require('supertest'); const mongoose = require('mongoose'); const fs = require('fs'); const path = require('path'); const Scene = require('../models/Scene'); const Asset = require('../models/Asset'); const Hotspot = require('../models/Hotspot'); const User = require('../models/User'); // Mock fs để không xóa file thật trong quá trình test và kiểm tra số lần gọi hàm jest.mock('fs', () => ({ ...jest.requireActual('fs'), promises: { unlink: jest.fn().mockResolvedValue() }, existsSync: jest.fn().mockReturnValue(true) })); // Import app - Giả định server.js của bạn export express app // Nếu file khởi tạo app của bạn có tên khác, hãy điều chỉnh đường dẫn bên dưới const app = require('../server'); describe('Integration Test: Cascade Scene Deletion (BFS)', () => { let adminToken; let adminUser; let parentAsset, childAsset; let parentScene, childScene; beforeAll(async () => { // Kết nối tới Database Test (Sử dụng biến môi trường hoặc mặc định) if (mongoose.connection.readyState === 0) { await mongoose.connect(process.env.MONGO_URI || 'mongodb://localhost:27017/3dtours_test'); } // Thiết lập Admin User để thực hiện các request có quyền bảo mật await User.deleteMany({}); adminUser = await User.create({ fullName: 'Admin Test', username: 'admintest', email: 'admin@test.com', password: 'password123', role: 'admin', agreedToRules: true }); // Đăng nhập để lấy JWT Token const res = await request(app) .post('/api/auth/login') .send({ username: 'admintest', password: 'password123' }); adminToken = res.body.token; }); afterAll(async () => { await User.deleteMany({}); await Scene.deleteMany({}); await Asset.deleteMany({}); await Hotspot.deleteMany({}); await mongoose.connection.close(); }); beforeEach(async () => { jest.clearAllMocks(); await Scene.deleteMany({}); await Asset.deleteMany({}); await Hotspot.deleteMany({}); // 1. Tạo dữ liệu Scene Cha và Asset tương ứng parentAsset = await Asset.create({ filePath: path.join(__dirname, '../uploads/parent_room.jpg'), fileSize: 1024 * 1024, uploadedBy: adminUser._id }); parentScene = await Scene.create({ name: 'Phòng Khách (Cha)', assetId: parentAsset._id, createdBy: adminUser._id, status: 'completed' }); // 2. Tạo dữ liệu Scene Con và Asset tương ứng childAsset = await Asset.create({ filePath: path.join(__dirname, '../uploads/child_balcony.jpg'), fileSize: 800 * 1024, uploadedBy: adminUser._id }); childScene = await Scene.create({ name: 'Ban Công (Con)', assetId: childAsset._id, createdBy: adminUser._id, status: 'completed' }); // 3. Tạo liên kết: Cha -> trỏ tới -> Con thông qua Hotspot await Hotspot.create({ parent_scene_id: parentScene._id, target_scene_id: childScene._id, title: 'Đi ra Ban Công' }); }); test('Khi xóa scene CHA, phải xóa dây chuyền sang scene CON và gỡ bỏ toàn bộ file vật lý', async () => { const res = await request(app) .delete(`/api/scenes/${parentScene._id}`) .set('Authorization', `Bearer ${adminToken}`); expect(res.status).toBe(200); // Kiểm tra Database: Không còn bất kỳ scene nào const scenesInDB = await Scene.find({}); expect(scenesInDB.length).toBe(0); // Kiểm tra Assets: Các bản ghi asset cũng phải bị xóa sạch const assetsInDB = await Asset.find({}); expect(assetsInDB.length).toBe(0); // Kiểm tra Filesystem: Phải gọi lệnh xóa (unlink) cho cả 2 tệp tin (cha và con) expect(fs.promises.unlink).toHaveBeenCalledTimes(2); }); test('Khi xóa scene CON, scene CHA vẫn phải tồn tại (Không được xóa ngược)', async () => { const res = await request(app) .delete(`/api/scenes/${childScene._id}`) .set('Authorization', `Bearer ${adminToken}`); expect(res.status).toBe(200); // Scene Cha và Asset của nó phải còn nguyên trong Database const parentInDB = await Scene.findById(parentScene._id); expect(parentInDB).not.toBeNull(); const parentAssetInDB = await Asset.findById(parentAsset._id); expect(parentAssetInDB).not.toBeNull(); // Chỉ có 1 tệp tin bị xóa (tệp của scene con) expect(fs.promises.unlink).toHaveBeenCalledTimes(1); expect(fs.promises.unlink).toHaveBeenCalledWith(expect.stringContaining('child_balcony.jpg')); }); });