feat: support JSON import for test cases and add Traditional Chinese locale
Some checks failed
UnitTCMS CI / unittcms-test (push) Has been cancelled
Some checks failed
UnitTCMS CI / unittcms-test (push) Has been cancelled
JSON import lets users round-trip cases exported via the JSON download endpoint back into a folder, alongside the existing Excel import path.
This commit is contained in:
163
backend/routes/cases/import.test.js
Normal file
163
backend/routes/cases/import.test.js
Normal file
@@ -0,0 +1,163 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import request from 'supertest';
|
||||
import express from 'express';
|
||||
import { Sequelize } from 'sequelize';
|
||||
import casesImportRoute from './import.js';
|
||||
|
||||
// mock of authentication / permission middleware
|
||||
vi.mock('../../middleware/auth.js', () => ({
|
||||
default: () => ({
|
||||
verifySignedIn: vi.fn((req, res, next) => {
|
||||
req.userId = 1;
|
||||
next();
|
||||
}),
|
||||
}),
|
||||
}));
|
||||
vi.mock('../../middleware/verifyEditable.js', () => ({
|
||||
default: () => ({
|
||||
verifyProjectDeveloperFromFolderId: vi.fn((req, res, next) => {
|
||||
next();
|
||||
}),
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockCase = {
|
||||
bulkCreate: vi.fn(),
|
||||
belongsToMany: vi.fn(),
|
||||
};
|
||||
vi.mock('../../models/cases.js', () => ({
|
||||
default: () => mockCase,
|
||||
}));
|
||||
|
||||
const mockStep = {
|
||||
create: vi.fn(),
|
||||
belongsToMany: vi.fn(),
|
||||
};
|
||||
vi.mock('../../models/steps.js', () => ({
|
||||
default: () => mockStep,
|
||||
}));
|
||||
|
||||
const mockCaseStep = {
|
||||
create: vi.fn(),
|
||||
};
|
||||
vi.mock('../../models/caseSteps.js', () => ({
|
||||
default: () => mockCaseStep,
|
||||
}));
|
||||
|
||||
describe('POST /import with JSON file', () => {
|
||||
let app;
|
||||
const sequelize = new Sequelize({ dialect: 'sqlite', logging: false });
|
||||
|
||||
beforeEach(() => {
|
||||
app = express();
|
||||
app.use(express.json());
|
||||
app.use('/', casesImportRoute(sequelize));
|
||||
vi.clearAllMocks();
|
||||
vi.spyOn(sequelize, 'transaction').mockResolvedValue({
|
||||
commit: vi.fn(),
|
||||
rollback: vi.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
const attachJson = (data) =>
|
||||
request(app)
|
||||
.post('/import?folderId=1')
|
||||
.attach('file', Buffer.from(JSON.stringify(data)), 'cases.json');
|
||||
|
||||
it('creates cases from a JSON array, mapping labels to enum indexes', async () => {
|
||||
mockCase.bulkCreate.mockResolvedValue([{ id: 101 }, { id: 102 }]);
|
||||
mockStep.create.mockResolvedValue({ id: 201 });
|
||||
mockCaseStep.create.mockResolvedValue({});
|
||||
|
||||
const response = await attachJson([
|
||||
{
|
||||
title: 'Text case',
|
||||
priority: 'high',
|
||||
type: 'functional',
|
||||
template: 'text',
|
||||
description: 'desc',
|
||||
Steps: [],
|
||||
},
|
||||
{
|
||||
title: 'Step case',
|
||||
priority: 'medium',
|
||||
type: 'acceptance',
|
||||
template: 'step',
|
||||
automationStatus: 'cannot-be-automated',
|
||||
Steps: [
|
||||
{ step: 'do A', expectedStepResult: 'A done' },
|
||||
{ step: 'do B', expectedStepResult: 'B done' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
const created = mockCase.bulkCreate.mock.calls[0][0];
|
||||
expect(created).toHaveLength(2);
|
||||
expect(created[0]).toMatchObject({
|
||||
folderId: '1',
|
||||
title: 'Text case',
|
||||
state: 0,
|
||||
priority: 1, // high
|
||||
type: 4, // functional
|
||||
template: 0, // text
|
||||
automationStatus: 1, // default automation-not-required
|
||||
});
|
||||
expect(created[1]).toMatchObject({
|
||||
title: 'Step case',
|
||||
priority: 2, // medium
|
||||
type: 5, // acceptance
|
||||
template: 1, // step
|
||||
automationStatus: 2, // cannot-be-automated
|
||||
});
|
||||
|
||||
// Only the second case has steps -> 2 steps created
|
||||
expect(mockStep.create).toHaveBeenCalledTimes(2);
|
||||
expect(mockCaseStep.create).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('accepts the object-map shape produced by the JSON export', async () => {
|
||||
mockCase.bulkCreate.mockResolvedValue([{ id: 101 }]);
|
||||
|
||||
const response = await attachJson({
|
||||
1: { title: 'Mapped case', priority: 'low', type: 'other', template: 'text', Steps: [] },
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(mockCase.bulkCreate.mock.calls[0][0]).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('returns 400 when a required field is missing', async () => {
|
||||
const response = await attachJson([{ priority: 'high', type: 'functional', template: 'text' }]);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toContain('missing required field: title');
|
||||
expect(mockCase.bulkCreate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns 400 for an invalid priority label', async () => {
|
||||
const response = await attachJson([
|
||||
{ title: 'Bad priority', priority: 'urgent', type: 'functional', template: 'text' },
|
||||
]);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toContain('invalid priority');
|
||||
});
|
||||
|
||||
it('returns 400 for malformed JSON', async () => {
|
||||
const response = await request(app)
|
||||
.post('/import?folderId=1')
|
||||
.attach('file', Buffer.from('{ not json'), 'cases.json');
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toBe('Invalid JSON file');
|
||||
});
|
||||
|
||||
it('rejects unsupported file extensions', async () => {
|
||||
const response = await request(app).post('/import?folderId=1').attach('file', Buffer.from('hello'), 'cases.txt');
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.error).toContain('Only Excel');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user