Skip to content

Commit b15ce31

Browse files
committed
fix: add tests for import export plugin
1 parent c00e2db commit b15ce31

4 files changed

Lines changed: 237 additions & 3 deletions

File tree

tests/application/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,4 +157,4 @@ if (isMainModule) {
157157
});
158158
}
159159

160-
export { app, appReady, closeApplication };
160+
export { app, appReady, closeApplication, Filters };

tests/application/resources/adminuser.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export default {
66
...usersResource.plugins?.filter((p) => ![
77
'AdminForthAgentPlugin',
88
'TwoFactorsAuthPlugin',
9+
'DashboardPlugin',
910
].includes(p.className)) || [],
1011
],
1112
}
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import { admin, agent, closeApplication, Filters } from './testApp';
2+
3+
const RESOURCE_ID = 'cars_sl';
4+
5+
let pluginBase: string;
6+
7+
// column-oriented CSV data (same shape PapaParse produces and the plugin expects)
8+
function carData(rows: Array<Record<string, string>>) {
9+
const data: Record<string, string[]> = {};
10+
for (const row of rows) {
11+
for (const [key, value] of Object.entries(row)) {
12+
(data[key] ??= []).push(value);
13+
}
14+
}
15+
return data;
16+
}
17+
18+
function makeRow(id: string, overrides: Record<string, string> = {}) {
19+
return {
20+
id,
21+
model: `Model ${id}`,
22+
price: '1000',
23+
engine_type: 'gasoline',
24+
engine_power: '100',
25+
listed: 'true',
26+
mileage: '1000',
27+
...overrides,
28+
};
29+
}
30+
31+
beforeAll(async () => {
32+
const plugin = (admin as any).activatedPlugins.find(
33+
(p: any) => p.className === 'ImportExport' && p.resourceConfig?.resourceId === RESOURCE_ID,
34+
);
35+
expect(plugin).toBeDefined();
36+
pluginBase = `/adminapi/v1/plugin/${plugin.pluginInstanceId}`;
37+
38+
// plugin endpoints now require authentication + create/edit/list/show access;
39+
// the agent keeps the auth cookie for all subsequent requests
40+
const login = await agent
41+
.post('/adminapi/v1/login')
42+
.send({ username: 'adminforth', password: 'adminforth' });
43+
expect(login.status).toEqual(200);
44+
});
45+
46+
afterAll(async () => {
47+
await closeApplication();
48+
});
49+
50+
describe('Import-Export plugin', () => {
51+
describe('POST /import-csv', () => {
52+
it('creates new records from column-oriented data', async () => {
53+
const res = await agent
54+
.post(`${pluginBase}/import-csv`)
55+
.send({ data: carData([makeRow('ie-1'), makeRow('ie-2')]) });
56+
57+
expect(res.status).toEqual(200);
58+
expect(res.body.ok).toBe(true);
59+
expect(res.body.importedCount).toEqual(2);
60+
expect(res.body.updatedCount).toEqual(0);
61+
expect(res.body.errors).toEqual([]);
62+
63+
const created = await admin.resource(RESOURCE_ID).list([Filters.EQ('id', 'ie-1')]);
64+
expect(created.length).toEqual(1);
65+
expect(created[0].model).toEqual('Model ie-1');
66+
});
67+
68+
it('coerces value types according to column definitions', async () => {
69+
await agent
70+
.post(`${pluginBase}/import-csv`)
71+
.send({ data: carData([makeRow('ie-types', { engine_power: '250', listed: 'false', mileage: '42' })]) });
72+
73+
const [record] = await admin.resource(RESOURCE_ID).list([Filters.EQ('id', 'ie-types')]);
74+
expect(record.engine_power).toEqual(250); // INTEGER coerced from string
75+
expect(record.mileage).toEqual(42); // FLOAT coerced from string
76+
expect(record.listed).toEqual(false); // BOOLEAN coerced from "false"
77+
});
78+
79+
it('updates existing records matched by primary key', async () => {
80+
const res = await agent
81+
.post(`${pluginBase}/import-csv`)
82+
.send({
83+
data: carData([
84+
makeRow('ie-1', { model: 'Model ie-1 UPDATED' }), // exists -> update
85+
makeRow('ie-3'), // new -> create
86+
]),
87+
});
88+
89+
expect(res.status).toEqual(200);
90+
expect(res.body.ok).toBe(true);
91+
expect(res.body.importedCount).toEqual(1);
92+
expect(res.body.updatedCount).toEqual(1);
93+
94+
const [updated] = await admin.resource(RESOURCE_ID).list([Filters.EQ('id', 'ie-1')]);
95+
expect(updated.model).toEqual('Model ie-1 UPDATED');
96+
});
97+
98+
it('returns validation errors for unknown columns and imports nothing', async () => {
99+
const res = await agent
100+
.post(`${pluginBase}/import-csv`)
101+
.send({ data: carData([makeRow('ie-bad', { not_a_column: 'x' })]) });
102+
103+
expect(res.status).toEqual(200);
104+
expect(res.body.ok).toBe(false);
105+
expect(res.body.errors.length).toBeGreaterThan(0);
106+
expect(res.body.errors[0]).toContain("not_a_column");
107+
108+
const found = await admin.resource(RESOURCE_ID).list([Filters.EQ('id', 'ie-bad')]);
109+
expect(found.length).toEqual(0);
110+
});
111+
});
112+
113+
describe('POST /import-csv-new-only', () => {
114+
it('imports only new records and skips existing ones', async () => {
115+
const res = await agent
116+
.post(`${pluginBase}/import-csv-new-only`)
117+
.send({
118+
data: carData([
119+
makeRow('ie-1', { model: 'Model ie-1 MUST NOT CHANGE' }), // exists -> skip
120+
makeRow('ie-new-only'), // new -> create
121+
]),
122+
});
123+
124+
expect(res.status).toEqual(200);
125+
expect(res.body.ok).toBe(true);
126+
expect(res.body.importedCount).toEqual(1);
127+
128+
const [existing] = await admin.resource(RESOURCE_ID).list([Filters.EQ('id', 'ie-1')]);
129+
expect(existing.model).toEqual('Model ie-1 UPDATED'); // unchanged by new-only import
130+
131+
const created = await admin.resource(RESOURCE_ID).list([Filters.EQ('id', 'ie-new-only')]);
132+
expect(created.length).toEqual(1);
133+
});
134+
});
135+
136+
describe('POST /check-records', () => {
137+
it('reports existing vs new record counts by primary key', async () => {
138+
const res = await agent
139+
.post(`${pluginBase}/check-records`)
140+
.send({
141+
data: carData([
142+
makeRow('ie-1'), // exists
143+
makeRow('ie-2'), // exists
144+
makeRow('ie-does-not-exist'), // new
145+
]),
146+
});
147+
148+
expect(res.status).toEqual(200);
149+
expect(res.body.ok).toBe(true);
150+
expect(res.body.total).toEqual(3);
151+
expect(res.body.existingCount).toEqual(2);
152+
expect(res.body.newCount).toEqual(1);
153+
});
154+
});
155+
156+
describe('POST /export-csv', () => {
157+
it('exports all records with fields and rows', async () => {
158+
const res = await agent.post(`${pluginBase}/export-csv`).send({ filters: [], sort: [] });
159+
160+
expect(res.status).toEqual(200);
161+
expect(res.body.ok).toBe(true);
162+
expect(Array.isArray(res.body.data.fields)).toBe(true);
163+
expect(res.body.data.fields).toContain('model');
164+
expect(res.body.data.fields).toContain('price');
165+
expect(Array.isArray(res.body.data.data)).toBe(true);
166+
expect(res.body.exportedCount).toEqual(res.body.data.data.length);
167+
expect(res.body.exportedCount).toBeGreaterThan(0);
168+
});
169+
170+
it('respects filters when exporting', async () => {
171+
const res = await agent
172+
.post(`${pluginBase}/export-csv`)
173+
.send({ filters: [Filters.EQ('id', 'ie-1')], sort: [] });
174+
175+
expect(res.status).toEqual(200);
176+
expect(res.body.ok).toBe(true);
177+
expect(res.body.exportedCount).toEqual(1);
178+
179+
const modelIdx = res.body.data.fields.indexOf('model');
180+
expect(res.body.data.data[0][modelIdx]).toEqual('Model ie-1 UPDATED');
181+
});
182+
});
183+
184+
describe('JSON type fields', () => {
185+
const colorObj = { hex: '#ffffff', name: 'white' };
186+
187+
it('exports JSON column object values as JSON strings', async () => {
188+
await admin.resource(RESOURCE_ID).create({
189+
id: 'ie-json',
190+
model: 'JSON Car',
191+
price: '1000',
192+
engine_type: 'gasoline',
193+
engine_power: 100,
194+
listed: true,
195+
mileage: 1000,
196+
color: colorObj,
197+
});
198+
199+
const res = await agent
200+
.post(`${pluginBase}/export-csv`)
201+
.send({ filters: [Filters.EQ('id', 'ie-json')], sort: [] });
202+
203+
expect(res.status).toEqual(200);
204+
expect(res.body.ok).toBe(true);
205+
expect(res.body.exportedCount).toEqual(1);
206+
207+
const colorIdx = res.body.data.fields.indexOf('color');
208+
const exported = res.body.data.data[0][colorIdx];
209+
expect(typeof exported).toEqual('string'); // object serialized for CSV
210+
expect(JSON.parse(exported)).toEqual(colorObj);
211+
});
212+
213+
it('imports JSON column values supplied as JSON strings', async () => {
214+
const importedColor = { hex: '#000000', name: 'black' };
215+
const res = await agent
216+
.post(`${pluginBase}/import-csv`)
217+
.send({
218+
data: carData([
219+
makeRow('ie-json-import', { color: JSON.stringify(importedColor) }),
220+
]),
221+
});
222+
223+
expect(res.status).toEqual(200);
224+
expect(res.body.ok).toBe(true);
225+
expect(res.body.importedCount).toEqual(1);
226+
expect(res.body.errors).toEqual([]);
227+
228+
const [record] = await admin.resource(RESOURCE_ID).list([Filters.EQ('id', 'ie-json-import')]);
229+
const stored = typeof record.color === 'string' ? JSON.parse(record.color) : record.color;
230+
expect(stored).toEqual(importedColor);
231+
});
232+
});
233+
});

tests/jest_tests/testApp.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ await execFileAsync('pnpm', ['migrate:local'], {
2929
env: testEnv,
3030
});
3131

32-
const { app, appReady, closeApplication } = await import('../application/index');
32+
const { admin, app, appReady, closeApplication, Filters } = await import('../application/index');
3333
await appReady;
3434

3535
const agent = request.agent(app);
3636

37-
export { agent, app, closeApplication };
37+
export { admin, agent, app, closeApplication, Filters };

0 commit comments

Comments
 (0)