mirror of
https://github.com/labring/FastGPT.git
synced 2025-12-25 20:02:47 +00:00
image compatibility for various content-types (#6119)
* image compatibility for various content-types * perf: image type detect * perf: gethistory * update test * update rerank log * perf: login * fix: query extension use --------- Co-authored-by: archer <545436317@qq.com>
This commit is contained in:
parent
ea7c37745a
commit
5231f4281f
|
|
@ -16,6 +16,7 @@ description: 'FastGPT V4.14.5 更新说明'
|
|||
|
||||
1. MCP 工具创建时,使用自定义鉴权头会报错。
|
||||
2. 获取对话日志列表时,如果用户头像为空,会抛错。
|
||||
3. chatAgent 未开启问题优化时,前端 UI 显示开启。
|
||||
|
||||
|
||||
## 插件
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
"document/content/docs/introduction/development/modelConfig/ppio.mdx": "2025-09-29T11:52:39+08:00",
|
||||
"document/content/docs/introduction/development/modelConfig/siliconCloud.mdx": "2025-08-05T23:20:39+08:00",
|
||||
"document/content/docs/introduction/development/openapi/app.mdx": "2025-09-26T13:18:51+08:00",
|
||||
"document/content/docs/introduction/development/openapi/chat.mdx": "2025-11-14T13:21:17+08:00",
|
||||
"document/content/docs/introduction/development/openapi/chat.mdx": "2025-12-18T13:49:45+08:00",
|
||||
"document/content/docs/introduction/development/openapi/dataset.mdx": "2025-09-29T11:34:11+08:00",
|
||||
"document/content/docs/introduction/development/openapi/intro.mdx": "2025-09-29T11:34:11+08:00",
|
||||
"document/content/docs/introduction/development/openapi/share.mdx": "2025-12-09T23:33:32+08:00",
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { PaginationSchema, PaginationResponseSchema } from '../../../api';
|
|||
export const GetHistoriesBodySchema = PaginationSchema.and(
|
||||
OutLinkChatAuthSchema.and(
|
||||
z.object({
|
||||
appId: ObjectIdSchema.optional().describe('应用ID'),
|
||||
appId: z.string().optional().describe('应用ID'),
|
||||
source: z.enum(ChatSourceEnum).optional().describe('对话来源'),
|
||||
startCreateTime: z.string().optional().describe('创建时间开始'),
|
||||
endCreateTime: z.string().optional().describe('创建时间结束'),
|
||||
|
|
|
|||
|
|
@ -2,13 +2,12 @@ import { type preUploadImgProps } from '@fastgpt/global/common/file/api';
|
|||
import { imageBaseUrl } from '@fastgpt/global/common/file/image/constants';
|
||||
import { MongoImage } from './schema';
|
||||
import { type ClientSession, Types } from '../../../common/mongo';
|
||||
import { guessBase64ImageType } from '../utils';
|
||||
import { guessBase64ImageType } from './utils';
|
||||
import { readFromSecondary } from '../../mongo/utils';
|
||||
import { addHours } from 'date-fns';
|
||||
import { imageFileType } from '@fastgpt/global/common/file/constants';
|
||||
import { retryFn } from '@fastgpt/global/common/system/utils';
|
||||
import { UserError } from '@fastgpt/global/common/error/utils';
|
||||
import { S3Sources } from '../../s3/type';
|
||||
import { getS3AvatarSource } from '../../s3/sources/avatar';
|
||||
import { isS3ObjectKey } from '../../s3/utils';
|
||||
import path from 'path';
|
||||
|
|
|
|||
|
|
@ -1,8 +1,98 @@
|
|||
import axios from 'axios';
|
||||
import { addLog } from '../../system/log';
|
||||
import { serverRequestBaseUrl } from '../../api/serverRequest';
|
||||
import { getFileContentTypeFromHeader, guessBase64ImageType } from '../utils';
|
||||
import { retryFn } from '@fastgpt/global/common/system/utils';
|
||||
import { getContentTypeFromHeader } from '../utils';
|
||||
|
||||
// 图片格式魔数映射表
|
||||
const IMAGE_SIGNATURES: { type: string; magic: number[]; check?: (buffer: Buffer) => boolean }[] = [
|
||||
{ type: 'image/jpeg', magic: [0xff, 0xd8, 0xff] },
|
||||
{ type: 'image/png', magic: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a] },
|
||||
{ type: 'image/gif', magic: [0x47, 0x49, 0x46, 0x38] },
|
||||
{
|
||||
type: 'image/webp',
|
||||
magic: [0x52, 0x49, 0x46, 0x46],
|
||||
check: (buffer) => buffer.length >= 12 && buffer.slice(8, 12).toString('ascii') === 'WEBP'
|
||||
},
|
||||
{ type: 'image/bmp', magic: [0x42, 0x4d] },
|
||||
{ type: 'image/tiff', magic: [0x49, 0x49, 0x2a, 0x00] },
|
||||
{ type: 'image/tiff', magic: [0x4d, 0x4d, 0x00, 0x2a] },
|
||||
{ type: 'image/svg+xml', magic: [0x3c, 0x73, 0x76, 0x67] },
|
||||
{ type: 'image/x-icon', magic: [0x00, 0x00, 0x01, 0x00] }
|
||||
];
|
||||
|
||||
// 有效的图片 MIME 类型
|
||||
const VALID_IMAGE_TYPES = new Set([
|
||||
'image/jpeg',
|
||||
'image/jpg',
|
||||
'image/png',
|
||||
'image/gif',
|
||||
'image/webp',
|
||||
'image/bmp',
|
||||
'image/svg+xml',
|
||||
'image/tiff',
|
||||
'image/x-icon',
|
||||
'image/vnd.microsoft.icon',
|
||||
'image/ico',
|
||||
'image/heic',
|
||||
'image/heif',
|
||||
'image/avif'
|
||||
]);
|
||||
|
||||
// Base64 首字符到图片类型的映射
|
||||
const BASE64_PREFIX_MAP: Record<string, string> = {
|
||||
'/': 'image/jpeg',
|
||||
i: 'image/png',
|
||||
R: 'image/gif',
|
||||
U: 'image/webp',
|
||||
Q: 'image/bmp',
|
||||
P: 'image/svg+xml',
|
||||
T: 'image/tiff',
|
||||
J: 'image/jp2',
|
||||
S: 'image/x-tga',
|
||||
I: 'image/ief',
|
||||
V: 'image/vnd.microsoft.icon',
|
||||
W: 'image/vnd.wap.wbmp',
|
||||
X: 'image/x-xbitmap',
|
||||
Z: 'image/x-xpixmap',
|
||||
Y: 'image/x-xwindowdump'
|
||||
};
|
||||
|
||||
const DEFAULT_IMAGE_TYPE = 'image/jpeg';
|
||||
|
||||
export const isValidImageContentType = (contentType: string): boolean => {
|
||||
if (!contentType) return false;
|
||||
return VALID_IMAGE_TYPES.has(contentType);
|
||||
};
|
||||
|
||||
export const detectImageTypeFromBuffer = (buffer: Buffer): string | undefined => {
|
||||
if (!buffer || buffer.length === 0) return;
|
||||
|
||||
for (const { type, magic, check } of IMAGE_SIGNATURES) {
|
||||
if (buffer.length < magic.length) continue;
|
||||
|
||||
const matches = magic.every((byte, index) => buffer[index] === byte);
|
||||
if (matches && (!check || check(buffer))) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
export const guessBase64ImageType = (str: string): string => {
|
||||
if (!str || typeof str !== 'string') return DEFAULT_IMAGE_TYPE;
|
||||
|
||||
// 尝试从 base64 解码并检测文件头
|
||||
try {
|
||||
const buffer = Buffer.from(str, 'base64');
|
||||
const detectedType = detectImageTypeFromBuffer(buffer);
|
||||
if (detectedType) return detectedType;
|
||||
} catch {}
|
||||
|
||||
// 回退到首字符映射
|
||||
return BASE64_PREFIX_MAP[str.charAt(0)] || DEFAULT_IMAGE_TYPE;
|
||||
};
|
||||
|
||||
export const getImageBase64 = async (url: string) => {
|
||||
addLog.debug(`Load image to base64: ${url}`);
|
||||
|
|
@ -16,10 +106,26 @@ export const getImageBase64 = async (url: string) => {
|
|||
})
|
||||
);
|
||||
|
||||
const base64 = Buffer.from(response.data, 'binary').toString('base64');
|
||||
const imageType =
|
||||
getFileContentTypeFromHeader(response.headers['content-type']) ||
|
||||
guessBase64ImageType(base64);
|
||||
const buffer = Buffer.from(response.data);
|
||||
const base64 = buffer.toString('base64');
|
||||
const headerContentType = getContentTypeFromHeader(response.headers['content-type']);
|
||||
|
||||
// 检测图片类型的优先级策略
|
||||
const imageType = (() => {
|
||||
// 1. 如果 Header 是有效的图片类型,直接使用
|
||||
if (headerContentType && isValidImageContentType(headerContentType)) {
|
||||
return headerContentType;
|
||||
}
|
||||
|
||||
// 2. 使用文件头检测(适用于通用二进制类型或无效类型)
|
||||
const detectedType = detectImageTypeFromBuffer(buffer);
|
||||
if (detectedType) {
|
||||
return detectedType;
|
||||
}
|
||||
|
||||
// 3. 回退到 base64 推断
|
||||
return guessBase64ImageType(base64);
|
||||
})();
|
||||
|
||||
return {
|
||||
completeBase64: `data:${imageType};base64,${base64}`,
|
||||
|
|
|
|||
|
|
@ -17,37 +17,8 @@ export const removeFilesByPaths = (paths: string[]) => {
|
|||
});
|
||||
};
|
||||
|
||||
export const guessBase64ImageType = (str: string) => {
|
||||
const imageTypeMap: Record<string, string> = {
|
||||
'/': 'image/jpeg',
|
||||
i: 'image/png',
|
||||
R: 'image/gif',
|
||||
U: 'image/webp',
|
||||
Q: 'image/bmp',
|
||||
P: 'image/svg+xml',
|
||||
T: 'image/tiff',
|
||||
J: 'image/jp2',
|
||||
S: 'image/x-tga',
|
||||
I: 'image/ief',
|
||||
V: 'image/vnd.microsoft.icon',
|
||||
W: 'image/vnd.wap.wbmp',
|
||||
X: 'image/x-xbitmap',
|
||||
Z: 'image/x-xpixmap',
|
||||
Y: 'image/x-xwindowdump'
|
||||
};
|
||||
|
||||
const defaultType = 'image/jpeg';
|
||||
if (typeof str !== 'string' || str.length === 0) {
|
||||
return defaultType;
|
||||
}
|
||||
|
||||
const firstChar = str.charAt(0);
|
||||
return imageTypeMap[firstChar] || defaultType;
|
||||
};
|
||||
|
||||
export const getFileContentTypeFromHeader = (header: string): string | undefined => {
|
||||
const contentType = header.split(';')[0];
|
||||
return contentType;
|
||||
export const getContentTypeFromHeader = (header: string): string | undefined => {
|
||||
return header?.toLowerCase()?.split(';')?.[0]?.trim();
|
||||
};
|
||||
|
||||
export const clearDirFiles = (dirPath: string) => {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { getAIApi } from '../config';
|
|||
import { countPromptTokens } from '../../../common/string/tiktoken/index';
|
||||
import { EmbeddingTypeEnm } from '@fastgpt/global/core/ai/constants';
|
||||
import { addLog } from '../../../common/system/log';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
import { retryFn } from '@fastgpt/global/common/system/utils';
|
||||
|
||||
type GetVectorProps = {
|
||||
model: EmbeddingModelItemType;
|
||||
|
|
@ -38,55 +40,70 @@ export async function getVectorsByText({ model, input, type, headers }: GetVecto
|
|||
|
||||
for (const chunk of chunks) {
|
||||
// input text to vector
|
||||
const result = await ai.embeddings
|
||||
.create(
|
||||
{
|
||||
...model.defaultConfig,
|
||||
...(type === EmbeddingTypeEnm.db && model.dbConfig),
|
||||
...(type === EmbeddingTypeEnm.query && model.queryConfig),
|
||||
model: model.model,
|
||||
input: chunk
|
||||
},
|
||||
model.requestUrl
|
||||
? {
|
||||
path: model.requestUrl,
|
||||
headers: {
|
||||
...(model.requestAuth ? { Authorization: `Bearer ${model.requestAuth}` } : {}),
|
||||
...headers
|
||||
const result = await retryFn(() =>
|
||||
ai.embeddings
|
||||
.create(
|
||||
{
|
||||
...model.defaultConfig,
|
||||
...(type === EmbeddingTypeEnm.db && model.dbConfig),
|
||||
...(type === EmbeddingTypeEnm.query && model.queryConfig),
|
||||
model: model.model,
|
||||
input: chunk
|
||||
},
|
||||
model.requestUrl
|
||||
? {
|
||||
path: model.requestUrl,
|
||||
headers: {
|
||||
...(model.requestAuth ? { Authorization: `Bearer ${model.requestAuth}` } : {}),
|
||||
...headers
|
||||
}
|
||||
}
|
||||
}
|
||||
: { headers }
|
||||
)
|
||||
.then(async (res) => {
|
||||
if (!res.data) {
|
||||
addLog.error('[Embedding] API is not responding', res);
|
||||
return Promise.reject('Embedding API is not responding');
|
||||
}
|
||||
if (!res?.data?.[0]?.embedding) {
|
||||
// @ts-ignore
|
||||
const msg = res.data?.err?.message || 'Embedding API Error';
|
||||
addLog.error('[Embedding] API Error', {
|
||||
message: msg,
|
||||
data: res
|
||||
});
|
||||
return Promise.reject(msg);
|
||||
}
|
||||
: { headers }
|
||||
)
|
||||
.then(async (res) => {
|
||||
if (!res.data) {
|
||||
addLog.error('[Embedding Error] not responding', {
|
||||
message: '',
|
||||
data: {
|
||||
response: res,
|
||||
model: model.model,
|
||||
inputLength: chunk.length
|
||||
}
|
||||
});
|
||||
return Promise.reject('Embedding API is not responding');
|
||||
}
|
||||
if (!res?.data?.[0]?.embedding) {
|
||||
// @ts-ignore
|
||||
const msg = res.data?.err?.message || '';
|
||||
addLog.error('[Embedding Error]', {
|
||||
message: msg,
|
||||
data: {
|
||||
response: res,
|
||||
model: model.model,
|
||||
inputLength: chunk.length
|
||||
}
|
||||
});
|
||||
return Promise.reject('Embedding API is not responding');
|
||||
}
|
||||
|
||||
const [tokens, vectors] = await Promise.all([
|
||||
(async () => {
|
||||
if (res.usage) return res.usage.total_tokens;
|
||||
const [tokens, vectors] = await Promise.all([
|
||||
(async () => {
|
||||
if (res.usage) return res.usage.total_tokens;
|
||||
|
||||
const tokens = await Promise.all(chunk.map((item) => countPromptTokens(item)));
|
||||
return tokens.reduce((sum, item) => sum + item, 0);
|
||||
})(),
|
||||
Promise.all(res.data.map((item) => formatVectors(item.embedding, model.normalization)))
|
||||
]);
|
||||
const tokens = await Promise.all(chunk.map((item) => countPromptTokens(item)));
|
||||
return tokens.reduce((sum, item) => sum + item, 0);
|
||||
})(),
|
||||
Promise.all(
|
||||
res.data.map((item) => formatVectors(item.embedding, model.normalization))
|
||||
)
|
||||
]);
|
||||
|
||||
return {
|
||||
tokens,
|
||||
vectors
|
||||
};
|
||||
});
|
||||
return {
|
||||
tokens,
|
||||
vectors
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
totalTokens += result.tokens;
|
||||
allVectors.push(...result.vectors);
|
||||
|
|
@ -97,7 +114,13 @@ export async function getVectorsByText({ model, input, type, headers }: GetVecto
|
|||
vectors: allVectors
|
||||
};
|
||||
} catch (error) {
|
||||
addLog.error(`[Embedding] request error`, error);
|
||||
addLog.error(`[Embedding Error]`, {
|
||||
message: getErrText(error),
|
||||
data: {
|
||||
model: model.model,
|
||||
inputLengths: formatInput.map((item) => item.length)
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -628,12 +628,17 @@ const createChatCompletion = async ({
|
|||
('iterator' in response || 'controller' in response);
|
||||
|
||||
const getEmptyResponseTip = () => {
|
||||
addLog.warn(`LLM response empty`, {
|
||||
baseUrl: userKey?.baseUrl,
|
||||
requestBody: body
|
||||
});
|
||||
if (userKey?.baseUrl) {
|
||||
addLog.warn(`User LLM response empty`, {
|
||||
baseUrl: userKey?.baseUrl,
|
||||
requestBody: body
|
||||
});
|
||||
return `您的 OpenAI key 没有响应: ${JSON.stringify(body)}`;
|
||||
} else {
|
||||
addLog.error(`LLM response empty`, {
|
||||
message: '',
|
||||
data: body
|
||||
});
|
||||
}
|
||||
return i18nT('chat:LLM_model_response_empty');
|
||||
};
|
||||
|
|
@ -652,13 +657,18 @@ const createChatCompletion = async ({
|
|||
getEmptyResponseTip
|
||||
};
|
||||
} catch (error) {
|
||||
addLog.error(`LLM response error`, error);
|
||||
addLog.warn(`LLM response error`, {
|
||||
baseUrl: userKey?.baseUrl,
|
||||
requestBody: body
|
||||
});
|
||||
if (userKey?.baseUrl) {
|
||||
addLog.warn(`User ai api error`, {
|
||||
message: getErrText(error),
|
||||
baseUrl: userKey?.baseUrl,
|
||||
data: body
|
||||
});
|
||||
return Promise.reject(`您的 OpenAI key 出错了: ${getErrText(error)}`);
|
||||
} else {
|
||||
addLog.error(`LLM response error`, {
|
||||
message: getErrText(error),
|
||||
data: body
|
||||
});
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export function reRankRecall({
|
|||
headers?: Record<string, string>;
|
||||
}): Promise<ReRankCallResult> {
|
||||
if (!model) {
|
||||
return Promise.reject('[Rerank] No rerank model');
|
||||
return Promise.reject('No rerank model');
|
||||
}
|
||||
if (documents.length === 0) {
|
||||
return Promise.resolve({
|
||||
|
|
@ -67,7 +67,7 @@ export function reRankRecall({
|
|||
addLog.info('ReRank finish:', { time: Date.now() - start });
|
||||
|
||||
if (!data?.results || data?.results?.length === 0) {
|
||||
addLog.error('[Rerank] Empty result', { data });
|
||||
addLog.error('[Rerank Error]', { message: 'Empty result', data });
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -81,7 +81,7 @@ export function reRankRecall({
|
|||
};
|
||||
})
|
||||
.catch((err) => {
|
||||
addLog.error('[Rerank] request error', err);
|
||||
addLog.error('[Rerank Error]', err);
|
||||
|
||||
return Promise.reject(err);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ const SearchParamsTip = ({
|
|||
limit = 5000,
|
||||
responseEmptyText,
|
||||
usingReRank = false,
|
||||
datasetSearchUsingExtensionQuery,
|
||||
usingExtensionQuery,
|
||||
queryExtensionModel
|
||||
}: {
|
||||
searchMode: `${DatasetSearchModeEnum}`;
|
||||
|
|
@ -23,7 +23,7 @@ const SearchParamsTip = ({
|
|||
limit?: number;
|
||||
responseEmptyText?: string;
|
||||
usingReRank?: boolean;
|
||||
datasetSearchUsingExtensionQuery?: boolean;
|
||||
usingExtensionQuery?: boolean;
|
||||
queryExtensionModel?: string;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
|
@ -34,8 +34,8 @@ const SearchParamsTip = ({
|
|||
const hasSimilarityMode = usingReRank || searchMode === DatasetSearchModeEnum.embedding;
|
||||
|
||||
const extensionModelName = useMemo(
|
||||
() => getWebLLMModel(queryExtensionModel)?.name,
|
||||
[queryExtensionModel]
|
||||
() => (usingExtensionQuery ? getWebLLMModel(queryExtensionModel)?.name : ''),
|
||||
[usingExtensionQuery, queryExtensionModel]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
@ -94,7 +94,7 @@ const SearchParamsTip = ({
|
|||
</Td>
|
||||
)}
|
||||
<Td pt={0} pb={2} fontSize={'mini'}>
|
||||
{extensionModelName ? extensionModelName : '❌'}
|
||||
{extensionModelName || '❌'}
|
||||
</Td>
|
||||
{hasEmptyResponseMode && <Th>{responseEmptyText !== '' ? '✅' : '❌'}</Th>}
|
||||
</Tr>
|
||||
|
|
|
|||
|
|
@ -282,7 +282,7 @@ const EditForm = ({
|
|||
similarity={appForm.dataset.similarity}
|
||||
limit={appForm.dataset.limit}
|
||||
usingReRank={appForm.dataset.usingReRank}
|
||||
datasetSearchUsingExtensionQuery={appForm.dataset.datasetSearchUsingExtensionQuery}
|
||||
usingExtensionQuery={appForm.dataset.datasetSearchUsingExtensionQuery}
|
||||
queryExtensionModel={appForm.dataset.datasetSearchExtensionModel}
|
||||
/>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ const SelectDatasetParam = ({ inputs = [], nodeId }: RenderInputProps) => {
|
|||
similarity={data.similarity}
|
||||
limit={data.limit}
|
||||
usingReRank={data.usingReRank}
|
||||
datasetSearchUsingExtensionQuery={data.datasetSearchUsingExtensionQuery}
|
||||
usingExtensionQuery={data.datasetSearchUsingExtensionQuery}
|
||||
queryExtensionModel={data.datasetSearchExtensionModel}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -433,7 +433,7 @@ const TestResults = React.memo(function TestResults({
|
|||
similarity={datasetTestItem.similarity}
|
||||
limit={datasetTestItem.limit}
|
||||
usingReRank={datasetTestItem.usingReRank}
|
||||
datasetSearchUsingExtensionQuery={!!datasetTestItem.queryExtensionModel}
|
||||
usingExtensionQuery={!!datasetTestItem.queryExtensionModel}
|
||||
queryExtensionModel={datasetTestItem.queryExtensionModel}
|
||||
/>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
} from '@fastgpt/global/openapi/core/chat/history/api';
|
||||
import { parsePaginationRequest } from '@fastgpt/service/common/api/pagination';
|
||||
import { addMonths } from 'date-fns';
|
||||
import { ObjectIdSchema } from '@fastgpt/global/common/type/mongo';
|
||||
|
||||
/* get chat histories list */
|
||||
export async function handler(
|
||||
|
|
@ -70,6 +71,13 @@ export async function handler(
|
|||
};
|
||||
}
|
||||
|
||||
if (match.appId && !ObjectIdSchema.safeParse(match.appId).success) {
|
||||
return {
|
||||
list: [],
|
||||
total: 0
|
||||
};
|
||||
}
|
||||
|
||||
const timeMatch: Record<string, any> = {};
|
||||
if (startCreateTime || endCreateTime) {
|
||||
timeMatch.createTime = {
|
||||
|
|
|
|||
|
|
@ -30,21 +30,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||
type: UserAuthTypeEnum.login
|
||||
});
|
||||
|
||||
// 检测用户是否存在
|
||||
const authCert = await MongoUser.findOne(
|
||||
{
|
||||
username
|
||||
},
|
||||
'status'
|
||||
);
|
||||
if (!authCert) {
|
||||
return Promise.reject(UserErrEnum.account_psw_error);
|
||||
}
|
||||
|
||||
if (authCert.status === UserStatusEnum.forbidden) {
|
||||
return Promise.reject('Invalid account!');
|
||||
}
|
||||
|
||||
const user = await MongoUser.findOne({
|
||||
username,
|
||||
password
|
||||
|
|
@ -53,16 +38,18 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||
if (!user) {
|
||||
return Promise.reject(UserErrEnum.account_psw_error);
|
||||
}
|
||||
if (user.status === UserStatusEnum.forbidden) {
|
||||
return Promise.reject('Invalid account!');
|
||||
}
|
||||
|
||||
const userDetail = await getUserDetail({
|
||||
tmbId: user?.lastLoginTmbId,
|
||||
userId: user._id
|
||||
});
|
||||
|
||||
MongoUser.findByIdAndUpdate(user._id, {
|
||||
lastLoginTmbId: userDetail.team.tmbId,
|
||||
language
|
||||
});
|
||||
user.lastLoginTmbId = userDetail.team.tmbId;
|
||||
user.language = language;
|
||||
await user.save();
|
||||
|
||||
const token = await createUserSession({
|
||||
userId: user._id,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ describe('closeCustom api test', () => {
|
|||
let dataId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
testUser = await getUser('test-user-close-custom');
|
||||
testUser = await getUser(`test-user-close-custom-${Math.random()}`);
|
||||
|
||||
// Create test app
|
||||
const app = await MongoApp.create({
|
||||
|
|
@ -94,7 +94,7 @@ describe('closeCustom api test', () => {
|
|||
});
|
||||
|
||||
it('should fail when user does not have permission', async () => {
|
||||
const unauthorizedUser = await getUser('unauthorized-user-close');
|
||||
const unauthorizedUser = await getUser(`unauthorized-user-close-${Math.random()}`);
|
||||
|
||||
const res = await Call<CloseCustomFeedbackBodyType, {}, CloseCustomFeedbackResponseType>(
|
||||
handler,
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ describe('getFeedbackRecordIds api test', () => {
|
|||
let chatId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
testUser = await getUser('test-user-get-feedback-ids');
|
||||
testUser = await getUser(`test-user-get-feedback-ids-${Math.random()}`);
|
||||
|
||||
// Create test app
|
||||
const app = await MongoApp.create({
|
||||
|
|
@ -270,7 +270,7 @@ describe('getFeedbackRecordIds api test', () => {
|
|||
});
|
||||
|
||||
it('should fail when user does not have permission', async () => {
|
||||
const unauthorizedUser = await getUser('unauthorized-user-get-ids');
|
||||
const unauthorizedUser = await getUser(`unauthorized-user-get-ids-${Math.random()}`);
|
||||
|
||||
const res = await Call<GetFeedbackRecordIdsBodyType, {}, GetFeedbackRecordIdsResponseType>(
|
||||
handler,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ describe('updateFeedbackReadStatus api test', () => {
|
|||
let dataId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
testUser = await getUser('test-user-update-read-status');
|
||||
// Use unique username for each test to avoid concurrency issues
|
||||
testUser = await getUser(`test-user-update-read-status-${Math.random()}`);
|
||||
|
||||
// Create test app
|
||||
const app = await MongoApp.create({
|
||||
|
|
@ -127,7 +128,7 @@ describe('updateFeedbackReadStatus api test', () => {
|
|||
});
|
||||
|
||||
it('should fail when user does not have permission', async () => {
|
||||
const unauthorizedUser = await getUser('unauthorized-user-read-status');
|
||||
const unauthorizedUser = await getUser(`unauthorized-user-read-status-${Math.random()}`);
|
||||
|
||||
const res = await Call<
|
||||
UpdateFeedbackReadStatusBodyType,
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ describe('updateUserFeedback api test', () => {
|
|||
let dataId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
testUser = await getUser('test-user-update-feedback');
|
||||
testUser = await getUser(`test-user-update-feedback-${Math.random()}`);
|
||||
|
||||
// Create test app
|
||||
const app = await MongoApp.create({
|
||||
|
|
@ -383,7 +383,7 @@ describe('updateUserFeedback api test', () => {
|
|||
});
|
||||
|
||||
it('should fail when user does not have permission', async () => {
|
||||
const unauthorizedUser = await getUser('unauthorized-user-feedback');
|
||||
const unauthorizedUser = await getUser(`unauthorized-user-feedback-${Math.random()}`);
|
||||
|
||||
const res = await Call<UpdateUserFeedbackBodyType, {}, UpdateUserFeedbackResponseType>(
|
||||
handler,
|
||||
|
|
|
|||
|
|
@ -263,7 +263,7 @@ describe('getHistories api test', () => {
|
|||
});
|
||||
|
||||
it('should return empty list when appId does not exist', async () => {
|
||||
const nonExistentAppId = getNanoid(24);
|
||||
const nonExistentAppId = '507f1f77bcf86cd799439011'; // Valid ObjectId format but non-existent
|
||||
|
||||
const res = await Call<GetHistoriesBodyType, any, GetHistoriesResponseType>(handler, {
|
||||
auth: testUser,
|
||||
|
|
@ -276,9 +276,9 @@ describe('getHistories api test', () => {
|
|||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(500);
|
||||
expect(res.error).toBeDefined();
|
||||
expect(res.error?.name).toBe('ZodError');
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.data.list).toHaveLength(0);
|
||||
expect(res.data.total).toBe(0);
|
||||
});
|
||||
|
||||
it('should fail when appId is missing', async () => {
|
||||
|
|
@ -341,4 +341,40 @@ describe('getHistories api test', () => {
|
|||
// Second should be the most recently updated non-top chat
|
||||
expect(res.data.list[1].chatId).toBe(chatIds[2]);
|
||||
});
|
||||
|
||||
it('should return empty list when appId format is invalid', async () => {
|
||||
const invalidAppId = 'invalid-app-id'; // Not a valid ObjectId format
|
||||
|
||||
const res = await Call<GetHistoriesBodyType, any, GetHistoriesResponseType>(handler, {
|
||||
auth: testUser,
|
||||
body: {
|
||||
appId: invalidAppId
|
||||
},
|
||||
query: {
|
||||
offset: 0,
|
||||
pageSize: 10
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.data.list).toHaveLength(0);
|
||||
expect(res.data.total).toBe(0);
|
||||
});
|
||||
|
||||
it('should accept empty string for appId and return empty list', async () => {
|
||||
const res = await Call<GetHistoriesBodyType, any, GetHistoriesResponseType>(handler, {
|
||||
auth: testUser,
|
||||
body: {
|
||||
appId: ''
|
||||
},
|
||||
query: {
|
||||
offset: 0,
|
||||
pageSize: 10
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.data.list).toHaveLength(0);
|
||||
expect(res.data.total).toBe(0);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -193,9 +193,6 @@ describe('loginByPassword API', () => {
|
|||
});
|
||||
|
||||
it('should accept language parameter on successful login', async () => {
|
||||
// Spy on findByIdAndUpdate to verify it's called with the language
|
||||
const findByIdAndUpdateSpy = vi.spyOn(MongoUser, 'findByIdAndUpdate');
|
||||
|
||||
const res = await Call<PostLoginProps, {}, any>(loginApi.default, {
|
||||
body: {
|
||||
username: 'testuser',
|
||||
|
|
@ -208,15 +205,10 @@ describe('loginByPassword API', () => {
|
|||
expect(res.code).toBe(200);
|
||||
expect(res.error).toBeUndefined();
|
||||
|
||||
// Verify findByIdAndUpdate was called with the language
|
||||
expect(findByIdAndUpdateSpy).toHaveBeenCalledWith(
|
||||
testUser._id,
|
||||
expect.objectContaining({
|
||||
language: 'en'
|
||||
})
|
||||
);
|
||||
|
||||
findByIdAndUpdateSpy.mockRestore();
|
||||
// Verify user was updated with the language
|
||||
const updatedUser = await MongoUser.findById(testUser._id);
|
||||
expect(updatedUser?.language).toBe('en');
|
||||
expect(updatedUser?.lastLoginTmbId).toEqual(testTmb._id);
|
||||
});
|
||||
|
||||
it('should handle root user login correctly', async () => {
|
||||
|
|
@ -261,4 +253,74 @@ describe('loginByPassword API', () => {
|
|||
expect(res.data.token).toBeDefined();
|
||||
expect(typeof res.data.token).toBe('string');
|
||||
});
|
||||
|
||||
it('should use default language when language is not provided', async () => {
|
||||
const res = await Call<PostLoginProps, {}, any>(loginApi.default, {
|
||||
body: {
|
||||
username: 'testuser',
|
||||
password: 'testpassword',
|
||||
code: '123456'
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.error).toBeUndefined();
|
||||
|
||||
// Verify user was updated with the default language 'zh-CN'
|
||||
const updatedUser = await MongoUser.findById(testUser._id);
|
||||
expect(updatedUser?.language).toBe('zh-CN');
|
||||
});
|
||||
|
||||
it('should update lastLoginTmbId on successful login', async () => {
|
||||
const updateOneSpy = vi.spyOn(MongoUser, 'updateOne');
|
||||
|
||||
const res = await Call<PostLoginProps, {}, any>(loginApi.default, {
|
||||
body: {
|
||||
username: 'testuser',
|
||||
password: 'testpassword',
|
||||
code: '123456'
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.error).toBeUndefined();
|
||||
|
||||
// Verify user was updated with lastLoginTmbId
|
||||
const updatedUser = await MongoUser.findById(testUser._id);
|
||||
expect(updatedUser?.lastLoginTmbId).toEqual(testTmb._id);
|
||||
});
|
||||
|
||||
it('should verify user authentication flow', async () => {
|
||||
const res = await Call<PostLoginProps, {}, any>(loginApi.default, {
|
||||
body: {
|
||||
username: 'testuser',
|
||||
password: 'testpassword',
|
||||
code: '123456'
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
|
||||
// Verify the full authentication flow
|
||||
expect(authCode).toHaveBeenCalled();
|
||||
expect(setCookie).toHaveBeenCalled();
|
||||
expect(pushTrack.login).toHaveBeenCalled();
|
||||
expect(addAuditLog).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return user details with correct structure', async () => {
|
||||
const res = await Call<PostLoginProps, {}, any>(loginApi.default, {
|
||||
body: {
|
||||
username: 'testuser',
|
||||
password: 'testpassword',
|
||||
code: '123456'
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.data.user).toBeDefined();
|
||||
expect(res.data.user.team).toBeDefined();
|
||||
expect(res.data.user.team.teamId).toBe(String(testTeam._id));
|
||||
expect(res.data.user.team.tmbId).toBe(String(testTmb._id));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,189 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
isValidImageContentType,
|
||||
detectImageTypeFromBuffer,
|
||||
guessBase64ImageType
|
||||
} from '@fastgpt/service/common/file/image/utils';
|
||||
|
||||
describe('isValidImageContentType', () => {
|
||||
it('should return true for valid image MIME types', () => {
|
||||
expect(isValidImageContentType('image/jpeg')).toBe(true);
|
||||
expect(isValidImageContentType('image/jpg')).toBe(true);
|
||||
expect(isValidImageContentType('image/png')).toBe(true);
|
||||
expect(isValidImageContentType('image/gif')).toBe(true);
|
||||
expect(isValidImageContentType('image/webp')).toBe(true);
|
||||
expect(isValidImageContentType('image/bmp')).toBe(true);
|
||||
expect(isValidImageContentType('image/svg+xml')).toBe(true);
|
||||
expect(isValidImageContentType('image/tiff')).toBe(true);
|
||||
expect(isValidImageContentType('image/x-icon')).toBe(true);
|
||||
expect(isValidImageContentType('image/vnd.microsoft.icon')).toBe(true);
|
||||
expect(isValidImageContentType('image/ico')).toBe(true);
|
||||
expect(isValidImageContentType('image/heic')).toBe(true);
|
||||
expect(isValidImageContentType('image/heif')).toBe(true);
|
||||
expect(isValidImageContentType('image/avif')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for invalid image MIME types', () => {
|
||||
expect(isValidImageContentType('text/plain')).toBe(false);
|
||||
expect(isValidImageContentType('application/json')).toBe(false);
|
||||
expect(isValidImageContentType('video/mp4')).toBe(false);
|
||||
expect(isValidImageContentType('audio/mpeg')).toBe(false);
|
||||
expect(isValidImageContentType('application/pdf')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for empty or undefined input', () => {
|
||||
expect(isValidImageContentType('')).toBe(false);
|
||||
expect(isValidImageContentType(undefined as any)).toBe(false);
|
||||
expect(isValidImageContentType(null as any)).toBe(false);
|
||||
});
|
||||
|
||||
it('should be case-sensitive (requires lowercase input)', () => {
|
||||
// Note: This function expects lowercase input
|
||||
// The getContentTypeFromHeader function should normalize it first
|
||||
expect(isValidImageContentType('IMAGE/JPEG')).toBe(false);
|
||||
expect(isValidImageContentType('Image/Png')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('detectImageTypeFromBuffer', () => {
|
||||
it('should detect JPEG images', () => {
|
||||
const jpegBuffer = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46]);
|
||||
expect(detectImageTypeFromBuffer(jpegBuffer)).toBe('image/jpeg');
|
||||
});
|
||||
|
||||
it('should detect PNG images', () => {
|
||||
const pngBuffer = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00]);
|
||||
expect(detectImageTypeFromBuffer(pngBuffer)).toBe('image/png');
|
||||
});
|
||||
|
||||
it('should detect GIF images', () => {
|
||||
const gifBuffer = Buffer.from([0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x00, 0x00]);
|
||||
expect(detectImageTypeFromBuffer(gifBuffer)).toBe('image/gif');
|
||||
});
|
||||
|
||||
it('should detect WebP images', () => {
|
||||
// RIFF....WEBP
|
||||
const webpBuffer = Buffer.from([
|
||||
0x52, 0x49, 0x46, 0x46, 0x00, 0x00, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50
|
||||
]);
|
||||
expect(detectImageTypeFromBuffer(webpBuffer)).toBe('image/webp');
|
||||
});
|
||||
|
||||
it('should detect BMP images', () => {
|
||||
const bmpBuffer = Buffer.from([0x42, 0x4d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
|
||||
expect(detectImageTypeFromBuffer(bmpBuffer)).toBe('image/bmp');
|
||||
});
|
||||
|
||||
it('should detect TIFF images (little-endian)', () => {
|
||||
const tiffBuffer = Buffer.from([0x49, 0x49, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00]);
|
||||
expect(detectImageTypeFromBuffer(tiffBuffer)).toBe('image/tiff');
|
||||
});
|
||||
|
||||
it('should detect TIFF images (big-endian)', () => {
|
||||
const tiffBuffer = Buffer.from([0x4d, 0x4d, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00]);
|
||||
expect(detectImageTypeFromBuffer(tiffBuffer)).toBe('image/tiff');
|
||||
});
|
||||
|
||||
it('should detect SVG images', () => {
|
||||
const svgBuffer = Buffer.from([0x3c, 0x73, 0x76, 0x67, 0x20, 0x78, 0x6d, 0x6c]);
|
||||
expect(detectImageTypeFromBuffer(svgBuffer)).toBe('image/svg+xml');
|
||||
});
|
||||
|
||||
it('should detect ICO images', () => {
|
||||
const icoBuffer = Buffer.from([0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00]);
|
||||
expect(detectImageTypeFromBuffer(icoBuffer)).toBe('image/x-icon');
|
||||
});
|
||||
|
||||
it('should return undefined for invalid or too short buffers', () => {
|
||||
expect(detectImageTypeFromBuffer(Buffer.from([]))).toBe(undefined);
|
||||
expect(detectImageTypeFromBuffer(Buffer.from([0x00]))).toBe(undefined);
|
||||
expect(detectImageTypeFromBuffer(Buffer.from([0x00, 0x00]))).toBe(undefined);
|
||||
expect(detectImageTypeFromBuffer(null as any)).toBe(undefined);
|
||||
expect(detectImageTypeFromBuffer(undefined as any)).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should return undefined for unknown image formats', () => {
|
||||
const unknownBuffer = Buffer.from([0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11]);
|
||||
expect(detectImageTypeFromBuffer(unknownBuffer)).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should not detect WebP without proper WEBP marker', () => {
|
||||
// RIFF but no WEBP marker
|
||||
const fakeWebpBuffer = Buffer.from([
|
||||
0x52, 0x49, 0x46, 0x46, 0x00, 0x00, 0x00, 0x00, 0x41, 0x42, 0x43, 0x44
|
||||
]);
|
||||
expect(detectImageTypeFromBuffer(fakeWebpBuffer)).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('guessBase64ImageType', () => {
|
||||
it('should detect JPEG from base64 string', () => {
|
||||
// /9j/ is the base64 encoded start of a JPEG file (0xFF 0xD8 0xFF)
|
||||
const jpegBase64 = '/9j/4AAQSkZJRgABAQAAAQABAAD/';
|
||||
expect(guessBase64ImageType(jpegBase64)).toBe('image/jpeg');
|
||||
});
|
||||
|
||||
it('should detect PNG from base64 string', () => {
|
||||
// iVBORw== is the base64 encoded start of a PNG file
|
||||
const pngBase64 =
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
|
||||
expect(guessBase64ImageType(pngBase64)).toBe('image/png');
|
||||
});
|
||||
|
||||
it('should detect GIF from base64 string', () => {
|
||||
// R0lGOD is the base64 encoded start of a GIF file
|
||||
const gifBase64 = 'R0lGODlhAQABAAAAACw=';
|
||||
expect(guessBase64ImageType(gifBase64)).toBe('image/gif');
|
||||
});
|
||||
|
||||
it('should detect WebP from base64 string', () => {
|
||||
// UklGR is RIFF in base64
|
||||
const webpBase64 = 'UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAAwA0JaQAA3AA/vuUAAA=';
|
||||
expect(guessBase64ImageType(webpBase64)).toBe('image/webp');
|
||||
});
|
||||
|
||||
it('should detect BMP from base64 string', () => {
|
||||
// Qk0= is the base64 encoded start of a BMP file (0x42 0x4D)
|
||||
const bmpBase64 = 'Qk02AgAAAAAAADYAAAAoAAAA';
|
||||
expect(guessBase64ImageType(bmpBase64)).toBe('image/bmp');
|
||||
});
|
||||
|
||||
it('should fallback to first character mapping for unknown formats', () => {
|
||||
// Test various first characters from BASE64_PREFIX_MAP
|
||||
expect(guessBase64ImageType('/')).toBe('image/jpeg');
|
||||
expect(guessBase64ImageType('i')).toBe('image/png');
|
||||
expect(guessBase64ImageType('R')).toBe('image/gif');
|
||||
expect(guessBase64ImageType('U')).toBe('image/webp');
|
||||
expect(guessBase64ImageType('Q')).toBe('image/bmp');
|
||||
expect(guessBase64ImageType('P')).toBe('image/svg+xml');
|
||||
expect(guessBase64ImageType('T')).toBe('image/tiff');
|
||||
expect(guessBase64ImageType('V')).toBe('image/vnd.microsoft.icon');
|
||||
});
|
||||
|
||||
it('should return default type for unknown first characters', () => {
|
||||
expect(guessBase64ImageType('xyz')).toBe('image/jpeg');
|
||||
expect(guessBase64ImageType('123')).toBe('image/jpeg');
|
||||
expect(guessBase64ImageType('ABC')).toBe('image/jpeg');
|
||||
});
|
||||
|
||||
it('should handle empty or invalid input', () => {
|
||||
expect(guessBase64ImageType('')).toBe('image/jpeg');
|
||||
expect(guessBase64ImageType(null as any)).toBe('image/jpeg');
|
||||
expect(guessBase64ImageType(undefined as any)).toBe('image/jpeg');
|
||||
expect(guessBase64ImageType(123 as any)).toBe('image/jpeg');
|
||||
});
|
||||
|
||||
it('should handle invalid base64 strings', () => {
|
||||
// Should fallback to first character mapping
|
||||
expect(guessBase64ImageType('!!!invalid!!!')).toBe('image/jpeg');
|
||||
expect(guessBase64ImageType('not-base64')).toBe('image/jpeg');
|
||||
});
|
||||
|
||||
it('should prefer buffer detection over first character mapping', () => {
|
||||
// Create a valid JPEG base64 that starts with 'i' (which would map to PNG)
|
||||
// But the actual content is JPEG
|
||||
const jpegBytes = Buffer.from([0xff, 0xd8, 0xff, 0xe0]);
|
||||
const base64 = jpegBytes.toString('base64'); // This is "/9j/4A=="
|
||||
expect(guessBase64ImageType(base64)).toBe('image/jpeg');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { getContentTypeFromHeader } from '@fastgpt/service/common/file/utils';
|
||||
|
||||
describe('getContentTypeFromHeader', () => {
|
||||
it('should extract and normalize content type from header', () => {
|
||||
expect(getContentTypeFromHeader('image/jpeg')).toBe('image/jpeg');
|
||||
expect(getContentTypeFromHeader('image/png; charset=utf-8')).toBe('image/png');
|
||||
expect(getContentTypeFromHeader('text/html; charset=UTF-8')).toBe('text/html');
|
||||
expect(getContentTypeFromHeader('application/json;charset=utf-8')).toBe('application/json');
|
||||
});
|
||||
|
||||
it('should handle uppercase content types and convert to lowercase', () => {
|
||||
expect(getContentTypeFromHeader('Image/JPEG')).toBe('image/jpeg');
|
||||
expect(getContentTypeFromHeader('IMAGE/PNG')).toBe('image/png');
|
||||
expect(getContentTypeFromHeader('Application/JSON')).toBe('application/json');
|
||||
expect(getContentTypeFromHeader('TEXT/HTML')).toBe('text/html');
|
||||
});
|
||||
|
||||
it('should handle mixed case content types', () => {
|
||||
expect(getContentTypeFromHeader('Image/Jpeg')).toBe('image/jpeg');
|
||||
expect(getContentTypeFromHeader('IMAGE/png; charset=UTF-8')).toBe('image/png');
|
||||
expect(getContentTypeFromHeader('Application/Json')).toBe('application/json');
|
||||
});
|
||||
|
||||
it('should trim whitespace', () => {
|
||||
expect(getContentTypeFromHeader(' image/jpeg ')).toBe('image/jpeg');
|
||||
expect(getContentTypeFromHeader(' image/png ; charset=utf-8 ')).toBe('image/png');
|
||||
expect(getContentTypeFromHeader('text/html ;charset=UTF-8')).toBe('text/html');
|
||||
});
|
||||
|
||||
it('should handle empty or undefined input', () => {
|
||||
// Empty string after processing results in empty string, not undefined
|
||||
expect(getContentTypeFromHeader('')).toBe('');
|
||||
expect(getContentTypeFromHeader(undefined as any)).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should handle content types with multiple parameters', () => {
|
||||
expect(getContentTypeFromHeader('image/jpeg; charset=utf-8; boundary=something')).toBe(
|
||||
'image/jpeg'
|
||||
);
|
||||
expect(getContentTypeFromHeader('multipart/form-data; boundary=----WebKit')).toBe(
|
||||
'multipart/form-data'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle content types without parameters', () => {
|
||||
expect(getContentTypeFromHeader('image/webp')).toBe('image/webp');
|
||||
expect(getContentTypeFromHeader('image/gif')).toBe('image/gif');
|
||||
expect(getContentTypeFromHeader('image/svg+xml')).toBe('image/svg+xml');
|
||||
});
|
||||
|
||||
it('should handle special image formats', () => {
|
||||
expect(getContentTypeFromHeader('image/x-icon')).toBe('image/x-icon');
|
||||
expect(getContentTypeFromHeader('image/vnd.microsoft.icon')).toBe('image/vnd.microsoft.icon');
|
||||
expect(getContentTypeFromHeader('image/heic')).toBe('image/heic');
|
||||
expect(getContentTypeFromHeader('image/avif')).toBe('image/avif');
|
||||
});
|
||||
|
||||
it('should handle edge cases with semicolons', () => {
|
||||
expect(getContentTypeFromHeader('image/jpeg;')).toBe('image/jpeg');
|
||||
expect(getContentTypeFromHeader('image/png;;')).toBe('image/png');
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue