MM-62426: Remove form-data from @mattermost/client (#29792)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
0c213eae9e
Коммит
95b22f47a0
17
e2e-tests/playwright/package-lock.json
сгенерированный
17
e2e-tests/playwright/package-lock.json
сгенерированный
@@ -19,7 +19,8 @@
|
||||
"dotenv": "16.4.5",
|
||||
"form-data-encoder": "4.0.2",
|
||||
"formdata-node": "6.0.3",
|
||||
"uuid": "11.0.3"
|
||||
"uuid": "11.0.3",
|
||||
"zod": "3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/uuid": "10.0.0",
|
||||
@@ -2537,6 +2538,15 @@
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.24.1",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
|
||||
"integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -4127,6 +4137,11 @@
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
|
||||
"dev": true
|
||||
},
|
||||
"zod": {
|
||||
"version": "3.24.1",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
|
||||
"integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
"dotenv": "16.4.5",
|
||||
"form-data-encoder": "4.0.2",
|
||||
"formdata-node": "6.0.3",
|
||||
"uuid": "11.0.3"
|
||||
"uuid": "11.0.3",
|
||||
"zod": "3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/uuid": "10.0.0",
|
||||
|
||||
65
e2e-tests/playwright/support/file.ts
Обычный файл
65
e2e-tests/playwright/support/file.ts
Обычный файл
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
|
||||
const assetPath = path.resolve(__dirname, 'asset');
|
||||
|
||||
/**
|
||||
* Reads file data and creates a File object.
|
||||
* @param filePath - The path to the file.
|
||||
* @param mimeType - The MIME type of the file.
|
||||
* @returns A File object containing the file data.
|
||||
* @throws If the file does not exist.
|
||||
*/
|
||||
export function getFileData(filePath: string, mimeType: string): File {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error(`File not found at path: ${filePath}`);
|
||||
}
|
||||
|
||||
const fileName = path.basename(filePath);
|
||||
const fileBuffer = fs.readFileSync(filePath);
|
||||
|
||||
return new File([fileBuffer], fileName, {type: mimeType});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads file data from the "asset" directory and creates a File object.
|
||||
* @param filename - The name of the file in the "asset" directory.
|
||||
* @param mimeType - The MIME type of the file.
|
||||
* @returns An object containing a File object and the filename.
|
||||
*/
|
||||
export function getFileDataFromAsset(filename: string, mimeType: string) {
|
||||
const filePath = path.join(assetPath, filename);
|
||||
|
||||
return {file: getFileData(filePath, mimeType), filename: path.basename(filePath)};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads file data and creates a Blob object.
|
||||
* @param filePath - The path to the file.
|
||||
* @param mimeType - The MIME type of the file.
|
||||
* @returns A Blob object containing the file data.
|
||||
* @throws If the file does not exist.
|
||||
*/
|
||||
export function getBlobData(filePath: string, mimeType: string): Blob {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error(`File not found at path: ${filePath}`);
|
||||
}
|
||||
const fileBuffer = fs.readFileSync(filePath);
|
||||
|
||||
return new Blob([fileBuffer], {type: mimeType});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads file data from the "asset" directory and creates a Blob object.
|
||||
* @param filename - The name of the file in the "asset" directory.
|
||||
* @param mimeType - The MIME type of the file.
|
||||
* @returns An object containing a Blob object and the filename.
|
||||
*/
|
||||
export function getBlobDataFromAsset(filename: string, mimeType: string) {
|
||||
const filePath = path.join(assetPath, filename);
|
||||
|
||||
return {blob: getBlobData(filePath, mimeType), filename: path.basename(filePath)};
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import path from 'node:path';
|
||||
import {expect} from '@playwright/test';
|
||||
|
||||
import {PreferenceType} from '@mattermost/types/preferences';
|
||||
import testConfig from '@e2e-test.config';
|
||||
import {getFileDataFromAsset} from '@e2e-support/file';
|
||||
|
||||
import {makeClient} from '.';
|
||||
import {getOnPremServerConfig} from './default_config';
|
||||
@@ -42,9 +42,8 @@ export async function initSetup({
|
||||
const {client: userClient} = await makeClient(user);
|
||||
|
||||
if (withDefaultProfileImage) {
|
||||
// Set user profile image
|
||||
const fullPath = path.join(path.resolve(__dirname), '../', 'asset/mattermost-icon_128x128.png');
|
||||
await userClient.uploadProfileImageX(user.id, fullPath);
|
||||
const {file} = getFileDataFromAsset('mattermost-icon_128x128.png', 'image/png');
|
||||
await userClient.uploadProfileImage(user.id, file);
|
||||
}
|
||||
|
||||
// Update user preference
|
||||
|
||||
25
e2e-tests/playwright/tests/client/schema.ts
Обычный файл
25
e2e-tests/playwright/tests/client/schema.ts
Обычный файл
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {z} from 'zod';
|
||||
|
||||
const FileInfoSchema = z.object({
|
||||
id: z.string(),
|
||||
user_id: z.string(),
|
||||
channel_id: z.string(),
|
||||
create_at: z.number().int(),
|
||||
update_at: z.number().int(),
|
||||
delete_at: z.number().int(),
|
||||
name: z.string(),
|
||||
extension: z.string(),
|
||||
size: z.number().int(),
|
||||
mime_type: z.string(),
|
||||
mini_preview: z.nullable(z.any()),
|
||||
remote_id: z.string(),
|
||||
archived: z.boolean(),
|
||||
});
|
||||
|
||||
export const FileUploadResponseSchema = z.object({
|
||||
file_infos: z.array(FileInfoSchema),
|
||||
client_ids: z.array(z.string()),
|
||||
});
|
||||
140
e2e-tests/playwright/tests/client/upload_file.spec.ts
Обычный файл
140
e2e-tests/playwright/tests/client/upload_file.spec.ts
Обычный файл
@@ -0,0 +1,140 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@e2e-support/test_fixture';
|
||||
import {getBlobDataFromAsset, getFileDataFromAsset} from '@e2e-support/file';
|
||||
import {getRandomId} from '@e2e-support/util';
|
||||
import {Client} from '@e2e-support/server';
|
||||
import {FileUploadResponse} from '@mattermost/types/files';
|
||||
import {ServerChannel} from '@mattermost/types/channels';
|
||||
import {Team} from '@mattermost/types/teams';
|
||||
import {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {FileUploadResponseSchema} from './schema';
|
||||
|
||||
let userClient: Client;
|
||||
let user: UserProfile;
|
||||
let team: Team;
|
||||
let townSquareChannel: ServerChannel;
|
||||
|
||||
const filename = 'mattermost-icon_128x128.png';
|
||||
const mimeType = 'image/png';
|
||||
const {file} = getFileDataFromAsset(filename, mimeType);
|
||||
const {blob} = getBlobDataFromAsset(filename, mimeType);
|
||||
|
||||
test.beforeAll(async ({pw}) => {
|
||||
({userClient, user, team} = await pw.initSetup());
|
||||
townSquareChannel = await userClient.getChannelByName(team.id, 'town-square');
|
||||
});
|
||||
|
||||
test('should succeed with File', async () => {
|
||||
// # Prepare data with File
|
||||
const clientId = getRandomId();
|
||||
const formData = new FormData();
|
||||
formData.set('channel_id', townSquareChannel.id);
|
||||
formData.set('client_ids', clientId);
|
||||
formData.set('files', file, filename);
|
||||
|
||||
// # Do upload then validate the response
|
||||
const data = await userClient.uploadFile(formData);
|
||||
validateFileUploadResponse(data, clientId, user.id, townSquareChannel.id);
|
||||
});
|
||||
|
||||
test('should succeed with Blob', async () => {
|
||||
// # Prepare data with Blob
|
||||
const clientId = getRandomId();
|
||||
const formData = new FormData();
|
||||
formData.set('channel_id', townSquareChannel.id);
|
||||
formData.set('client_ids', clientId);
|
||||
formData.set('files', blob, filename);
|
||||
|
||||
// # Do upload then validate the response
|
||||
const data = await userClient.uploadFile(formData);
|
||||
validateFileUploadResponse(data, clientId, user.id, townSquareChannel.id);
|
||||
});
|
||||
|
||||
test('should succeed even with channel_id only', async () => {
|
||||
// # Set without channel ID
|
||||
const formData = new FormData();
|
||||
formData.set('channel_id', townSquareChannel.id);
|
||||
|
||||
// # Do upload then validate the response
|
||||
const data = await userClient.uploadFile(formData);
|
||||
|
||||
// * Validate that it doe snot throw an error
|
||||
const validate = () => FileUploadResponseSchema.parse(data);
|
||||
expect(validate).not.toThrow();
|
||||
|
||||
// * Validate that file_infos and client_ids are as expected
|
||||
expect(data.client_ids).toMatchObject([]);
|
||||
expect(data.file_infos.length).toBe(0);
|
||||
});
|
||||
|
||||
test('should fail on invalid channel ID', async () => {
|
||||
const clientId = getRandomId();
|
||||
|
||||
// # Set with invalid channel ID
|
||||
let formData = new FormData();
|
||||
formData.set('channel_id', 'invalid.channel.id');
|
||||
formData.set('client_ids', clientId);
|
||||
formData.set('files', file, filename);
|
||||
|
||||
await expect(userClient.uploadFile(formData)).rejects.toThrowError(
|
||||
'Invalid or missing channel_id parameter in request URL.',
|
||||
);
|
||||
|
||||
// # Set without channel ID
|
||||
formData = new FormData();
|
||||
formData.set('client_ids', clientId);
|
||||
formData.set('files', file, filename);
|
||||
|
||||
await expect(userClient.uploadFile(formData)).rejects.toThrowError(
|
||||
'Invalid or missing channel_id in request body.',
|
||||
);
|
||||
});
|
||||
|
||||
test('should fail on missing files', async () => {
|
||||
const clientId = getRandomId();
|
||||
|
||||
// # Set with invalid channel ID
|
||||
const formData = new FormData();
|
||||
formData.set('channel_id', townSquareChannel.id);
|
||||
formData.set('client_ids', clientId);
|
||||
|
||||
await expect(userClient.uploadFile(formData)).rejects.toThrowError(
|
||||
'Unable to upload file(s). Have 1 client_ids for 0 files.',
|
||||
);
|
||||
});
|
||||
|
||||
test('should fail on incorrect order setting up FormData', async () => {
|
||||
const clientId = getRandomId();
|
||||
|
||||
// # Set with files before client_ids
|
||||
const formData = new FormData();
|
||||
formData.set('channel_id', townSquareChannel.id);
|
||||
formData.set('files', file, filename);
|
||||
formData.set('client_ids', clientId);
|
||||
|
||||
await expect(userClient.uploadFile(formData)).rejects.toThrowError(
|
||||
'Invalid or missing client_ids in request body.',
|
||||
);
|
||||
});
|
||||
|
||||
function validateFileUploadResponse(data: FileUploadResponse, clientId: string, userId: string, channelId: string) {
|
||||
// * Validate the schema
|
||||
const validate = () => FileUploadResponseSchema.parse(data);
|
||||
expect(validate).not.toThrow();
|
||||
|
||||
// * Validate that file_infos and client_ids are as expected
|
||||
expect(data.client_ids).toMatchObject([clientId]);
|
||||
expect(data.file_infos.length).toBe(1);
|
||||
|
||||
// * Validate important contents of file_infos
|
||||
const fileInfo = data.file_infos[0];
|
||||
expect(fileInfo.user_id).toBe(userId);
|
||||
expect(fileInfo.channel_id).toBe(channelId);
|
||||
expect(fileInfo.delete_at).toBe(0);
|
||||
expect(fileInfo.extension).toBe('png');
|
||||
expect(fileInfo.mime_type).toBe('image/png');
|
||||
expect(fileInfo.archived).toBe(false);
|
||||
}
|
||||
Ссылка в новой задаче
Block a user