feat: support JSON import for test cases and add Traditional Chinese locale
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:
LittleYellow
2026-06-16 21:35:05 +08:00
parent e6f3bc799e
commit 55e78875ae
18 changed files with 822 additions and 70 deletions

View 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');
});
});