mirror of
https://github.com/labring/FastGPT.git
synced 2026-04-27 02:08:10 +08:00
fix: login secret (#6635)
* fix: login secret * lock * env template * fix: ts * fix: ts * fix: ts
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import * as checkPswExpiredApi from '@/pages/api/support/user/account/checkPswExpired';
|
||||
import { MongoUser } from '@fastgpt/service/support/user/schema';
|
||||
import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema';
|
||||
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
|
||||
import { UserStatusEnum } from '@fastgpt/global/support/user/constant';
|
||||
import { initTeamFreePlan } from '@fastgpt/service/support/wallet/sub/utils';
|
||||
import { Call } from '@test/utils/request';
|
||||
|
||||
describe('checkPswExpired API', () => {
|
||||
let testUser: any;
|
||||
let testTeam: any;
|
||||
let testTmb: any;
|
||||
const originalEnv = process.env.PASSWORD_EXPIRED_MONTH;
|
||||
|
||||
beforeEach(async () => {
|
||||
testUser = await MongoUser.create({
|
||||
username: 'testuser',
|
||||
password: 'testpassword',
|
||||
status: UserStatusEnum.active,
|
||||
passwordUpdateTime: new Date()
|
||||
});
|
||||
testTeam = await MongoTeam.create({
|
||||
name: 'Test Team',
|
||||
ownerId: testUser._id
|
||||
});
|
||||
await initTeamFreePlan({ teamId: String(testTeam._id) });
|
||||
testTmb = await MongoTeamMember.create({
|
||||
teamId: testTeam._id,
|
||||
userId: testUser._id,
|
||||
status: 'active',
|
||||
role: 'owner'
|
||||
});
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env.PASSWORD_EXPIRED_MONTH = originalEnv;
|
||||
});
|
||||
|
||||
it('should return false when PASSWORD_EXPIRED_MONTH is not set', async () => {
|
||||
delete process.env.PASSWORD_EXPIRED_MONTH;
|
||||
|
||||
const res = await Call(checkPswExpiredApi.default, {
|
||||
auth: {
|
||||
userId: String(testUser._id),
|
||||
teamId: String(testTeam._id),
|
||||
tmbId: String(testTmb._id),
|
||||
isRoot: false,
|
||||
sessionId: 'session123'
|
||||
} as any
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.data).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when PASSWORD_EXPIRED_MONTH=0', async () => {
|
||||
process.env.PASSWORD_EXPIRED_MONTH = '0';
|
||||
|
||||
const res = await Call(checkPswExpiredApi.default, {
|
||||
auth: {
|
||||
userId: String(testUser._id),
|
||||
teamId: String(testTeam._id),
|
||||
tmbId: String(testTmb._id),
|
||||
isRoot: false,
|
||||
sessionId: 'session123'
|
||||
} as any
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.data).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when password was updated recently', async () => {
|
||||
process.env.PASSWORD_EXPIRED_MONTH = '3';
|
||||
|
||||
// Update password time to now
|
||||
await MongoUser.findByIdAndUpdate(testUser._id, {
|
||||
passwordUpdateTime: new Date()
|
||||
});
|
||||
|
||||
const res = await Call(checkPswExpiredApi.default, {
|
||||
auth: {
|
||||
userId: String(testUser._id),
|
||||
teamId: String(testTeam._id),
|
||||
tmbId: String(testTmb._id),
|
||||
isRoot: false,
|
||||
sessionId: 'session123'
|
||||
} as any
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.data).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when password has expired (update time older than expiry period)', async () => {
|
||||
process.env.PASSWORD_EXPIRED_MONTH = '1';
|
||||
|
||||
// Set password update time to 2 months ago
|
||||
const twoMonthsAgo = new Date();
|
||||
twoMonthsAgo.setMonth(twoMonthsAgo.getMonth() - 2);
|
||||
await MongoUser.findByIdAndUpdate(testUser._id, {
|
||||
passwordUpdateTime: twoMonthsAgo
|
||||
});
|
||||
|
||||
const res = await Call(checkPswExpiredApi.default, {
|
||||
auth: {
|
||||
userId: String(testUser._id),
|
||||
teamId: String(testTeam._id),
|
||||
tmbId: String(testTmb._id),
|
||||
isRoot: false,
|
||||
sessionId: 'session123'
|
||||
} as any
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.data).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when passwordUpdateTime is not set and env is configured', async () => {
|
||||
process.env.PASSWORD_EXPIRED_MONTH = '3';
|
||||
|
||||
// Remove passwordUpdateTime
|
||||
await MongoUser.findByIdAndUpdate(testUser._id, {
|
||||
$unset: { passwordUpdateTime: '' }
|
||||
});
|
||||
|
||||
const res = await Call(checkPswExpiredApi.default, {
|
||||
auth: {
|
||||
userId: String(testUser._id),
|
||||
teamId: String(testTeam._id),
|
||||
tmbId: String(testTmb._id),
|
||||
isRoot: false,
|
||||
sessionId: 'session123'
|
||||
} as any
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.data).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when user is not found', async () => {
|
||||
const nonExistentId = '000000000000000000000001';
|
||||
|
||||
const res = await Call(checkPswExpiredApi.default, {
|
||||
auth: {
|
||||
userId: nonExistentId,
|
||||
teamId: String(testTeam._id),
|
||||
tmbId: String(testTmb._id),
|
||||
isRoot: false,
|
||||
sessionId: 'session123'
|
||||
} as any
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.data).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject request without authentication', async () => {
|
||||
const res = await Call(checkPswExpiredApi.default, {});
|
||||
|
||||
expect(res.code).toBe(500);
|
||||
});
|
||||
});
|
||||
@@ -5,14 +5,12 @@ import { UserStatusEnum } from '@fastgpt/global/support/user/constant';
|
||||
import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema';
|
||||
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
|
||||
import { authCode } from '@fastgpt/service/support/user/auth/controller';
|
||||
import { createUserSession } from '@fastgpt/service/support/user/session';
|
||||
import { setCookie } from '@fastgpt/service/support/permission/auth/common';
|
||||
import { pushTrack } from '@fastgpt/service/common/middle/tracks/utils';
|
||||
import { addAuditLog } from '@fastgpt/service/support/user/audit/util';
|
||||
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
|
||||
import { UserErrEnum } from '@fastgpt/global/common/error/code/user';
|
||||
import { Call } from '@test/utils/request';
|
||||
import type { PostLoginProps } from '@fastgpt/global/support/user/api';
|
||||
import type { LoginByPasswordBodyType } from '@fastgpt/global/openapi/support/user/account/login/api';
|
||||
import { initTeamFreePlan } from '@fastgpt/service/support/wallet/sub/utils';
|
||||
|
||||
describe('loginByPassword API', () => {
|
||||
@@ -21,7 +19,6 @@ describe('loginByPassword API', () => {
|
||||
let testTmb: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create test user and team
|
||||
testUser = await MongoUser.create({
|
||||
username: 'testuser',
|
||||
password: 'testpassword',
|
||||
@@ -48,12 +45,11 @@ describe('loginByPassword API', () => {
|
||||
lastLoginTmbId: testTmb._id
|
||||
});
|
||||
|
||||
// Reset mocks before each test
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should login successfully with valid credentials', async () => {
|
||||
const res = await Call<PostLoginProps, {}, any>(loginApi.default, {
|
||||
const res = await Call<LoginByPasswordBodyType, {}, any>(loginApi.default, {
|
||||
body: {
|
||||
username: 'testuser',
|
||||
password: 'testpassword',
|
||||
@@ -64,83 +60,66 @@ describe('loginByPassword API', () => {
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.error).toBeUndefined();
|
||||
expect(res.data).toBeDefined();
|
||||
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));
|
||||
expect(res.data.token).toBeDefined();
|
||||
expect(typeof res.data.token).toBe('string');
|
||||
expect(res.data.token.length).toBeGreaterThan(0);
|
||||
|
||||
// Verify authCode was called
|
||||
expect(authCode).toHaveBeenCalledWith({
|
||||
key: 'testuser',
|
||||
code: '123456',
|
||||
type: expect.any(String)
|
||||
});
|
||||
|
||||
// Verify setCookie was called
|
||||
expect(setCookie).toHaveBeenCalled();
|
||||
|
||||
// Verify tracking was called
|
||||
expect(pushTrack.login).toHaveBeenCalledWith({
|
||||
type: 'password',
|
||||
uid: testUser._id,
|
||||
teamId: String(testTeam._id),
|
||||
tmbId: String(testTmb._id)
|
||||
});
|
||||
|
||||
// Verify audit log was called
|
||||
expect(addAuditLog).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reject login when username is missing', async () => {
|
||||
const res = await Call<PostLoginProps, {}, any>(loginApi.default, {
|
||||
it('should reject login when username is empty', async () => {
|
||||
const res = await Call<LoginByPasswordBodyType, {}, any>(loginApi.default, {
|
||||
body: {
|
||||
username: '',
|
||||
password: 'testpassword',
|
||||
code: '123456'
|
||||
code: '123456',
|
||||
language: 'zh-CN'
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(500);
|
||||
expect(res.error).toBe(CommonErrEnum.invalidParams);
|
||||
});
|
||||
|
||||
it('should reject login when password is missing', async () => {
|
||||
const res = await Call<PostLoginProps, {}, any>(loginApi.default, {
|
||||
it('should reject login when password is empty', async () => {
|
||||
const res = await Call<LoginByPasswordBodyType, {}, any>(loginApi.default, {
|
||||
body: {
|
||||
username: 'testuser',
|
||||
password: '',
|
||||
code: '123456'
|
||||
code: '123456',
|
||||
language: 'zh-CN'
|
||||
}
|
||||
});
|
||||
|
||||
// Empty password passes zod z.string() but won't match any user record
|
||||
expect(res.code).toBe(500);
|
||||
expect(res.error).toBe(CommonErrEnum.invalidParams);
|
||||
});
|
||||
|
||||
it('should reject login when code is missing', async () => {
|
||||
const res = await Call<PostLoginProps, {}, any>(loginApi.default, {
|
||||
body: {
|
||||
username: 'testuser',
|
||||
password: 'testpassword',
|
||||
code: ''
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(500);
|
||||
expect(res.error).toBe(CommonErrEnum.invalidParams);
|
||||
expect(res.error).toBe(UserErrEnum.account_psw_error);
|
||||
});
|
||||
|
||||
it('should reject login when auth code verification fails', async () => {
|
||||
// Mock authCode to reject
|
||||
vi.mocked(authCode).mockRejectedValueOnce(new Error('Invalid code'));
|
||||
|
||||
const res = await Call<PostLoginProps, {}, any>(loginApi.default, {
|
||||
const res = await Call<LoginByPasswordBodyType, {}, any>(loginApi.default, {
|
||||
body: {
|
||||
username: 'testuser',
|
||||
password: 'testpassword',
|
||||
code: 'wrongcode'
|
||||
code: 'wrongcode',
|
||||
language: 'zh-CN'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -149,11 +128,12 @@ describe('loginByPassword API', () => {
|
||||
});
|
||||
|
||||
it('should reject login when user does not exist', async () => {
|
||||
const res = await Call<PostLoginProps, {}, any>(loginApi.default, {
|
||||
const res = await Call<LoginByPasswordBodyType, {}, any>(loginApi.default, {
|
||||
body: {
|
||||
username: 'nonexistentuser',
|
||||
password: 'testpassword',
|
||||
code: '123456'
|
||||
code: '123456',
|
||||
language: 'zh-CN'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -162,16 +142,16 @@ describe('loginByPassword API', () => {
|
||||
});
|
||||
|
||||
it('should reject login when user is forbidden', async () => {
|
||||
// Update user status to forbidden
|
||||
await MongoUser.findByIdAndUpdate(testUser._id, {
|
||||
status: UserStatusEnum.forbidden
|
||||
});
|
||||
|
||||
const res = await Call<PostLoginProps, {}, any>(loginApi.default, {
|
||||
const res = await Call<LoginByPasswordBodyType, {}, any>(loginApi.default, {
|
||||
body: {
|
||||
username: 'testuser',
|
||||
password: 'testpassword',
|
||||
code: '123456'
|
||||
code: '123456',
|
||||
language: 'zh-CN'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -180,11 +160,12 @@ describe('loginByPassword API', () => {
|
||||
});
|
||||
|
||||
it('should reject login when password is incorrect', async () => {
|
||||
const res = await Call<PostLoginProps, {}, any>(loginApi.default, {
|
||||
const res = await Call<LoginByPasswordBodyType, {}, any>(loginApi.default, {
|
||||
body: {
|
||||
username: 'testuser',
|
||||
password: 'wrongpassword',
|
||||
code: '123456'
|
||||
code: '123456',
|
||||
language: 'zh-CN'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -192,8 +173,8 @@ describe('loginByPassword API', () => {
|
||||
expect(res.error).toBe(UserErrEnum.account_psw_error);
|
||||
});
|
||||
|
||||
it('should accept language parameter on successful login', async () => {
|
||||
const res = await Call<PostLoginProps, {}, any>(loginApi.default, {
|
||||
it('should update language on successful login', async () => {
|
||||
const res = await Call<LoginByPasswordBodyType, {}, any>(loginApi.default, {
|
||||
body: {
|
||||
username: 'testuser',
|
||||
password: 'testpassword',
|
||||
@@ -203,16 +184,13 @@ describe('loginByPassword API', () => {
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.error).toBeUndefined();
|
||||
|
||||
// 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 () => {
|
||||
// Create root user
|
||||
const rootUser = await MongoUser.create({
|
||||
username: 'root',
|
||||
password: 'rootpassword',
|
||||
@@ -239,88 +217,125 @@ describe('loginByPassword API', () => {
|
||||
lastLoginTmbId: rootTmb._id
|
||||
});
|
||||
|
||||
const res = await Call<PostLoginProps, {}, any>(loginApi.default, {
|
||||
const res = await Call<LoginByPasswordBodyType, {}, any>(loginApi.default, {
|
||||
body: {
|
||||
username: 'root',
|
||||
password: 'rootpassword',
|
||||
code: '123456'
|
||||
code: '123456',
|
||||
language: 'zh-CN'
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.error).toBeUndefined();
|
||||
expect(res.data).toBeDefined();
|
||||
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'
|
||||
}
|
||||
// ===== Security: NoSQL injection prevention (GHSA-jxvr-h2vx-p73r) =====
|
||||
|
||||
describe('NoSQL injection prevention', () => {
|
||||
it('should reject password as object with MongoDB operator ($ne)', async () => {
|
||||
// GHSA-jxvr-h2vx-p73r Step 2: password: {"$ne": ""} bypasses password check
|
||||
const res = await Call<any, {}, any>(loginApi.default, {
|
||||
body: {
|
||||
username: 'testuser',
|
||||
password: { $ne: '' },
|
||||
code: '123456',
|
||||
language: 'zh-CN'
|
||||
}
|
||||
});
|
||||
|
||||
// Zod z.string() must reject object-type password
|
||||
expect(res.code).toBe(500);
|
||||
expect(res.data?.token).toBeUndefined();
|
||||
expect(res.data?.user).toBeUndefined();
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.error).toBeUndefined();
|
||||
it('should reject password with $regex operator', async () => {
|
||||
const res = await Call<any, {}, any>(loginApi.default, {
|
||||
body: {
|
||||
username: 'testuser',
|
||||
password: { $regex: '.*' },
|
||||
code: '123456',
|
||||
language: 'zh-CN'
|
||||
}
|
||||
});
|
||||
|
||||
// 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(500);
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.error).toBeUndefined();
|
||||
it('should reject password with $where injection', async () => {
|
||||
const res = await Call<any, {}, any>(loginApi.default, {
|
||||
body: {
|
||||
username: 'testuser',
|
||||
password: { $where: 'return true' },
|
||||
code: '123456',
|
||||
language: 'zh-CN'
|
||||
}
|
||||
});
|
||||
|
||||
// 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(500);
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
it('should reject username as object with MongoDB operator', async () => {
|
||||
const res = await Call<any, {}, any>(loginApi.default, {
|
||||
body: {
|
||||
username: { $ne: '' },
|
||||
password: 'testpassword',
|
||||
code: '123456',
|
||||
language: 'zh-CN'
|
||||
}
|
||||
});
|
||||
|
||||
// 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(500);
|
||||
});
|
||||
|
||||
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));
|
||||
it('should reject code as object with MongoDB operator', async () => {
|
||||
const res = await Call<any, {}, any>(loginApi.default, {
|
||||
body: {
|
||||
username: 'testuser',
|
||||
password: 'testpassword',
|
||||
code: { $regex: '.*' },
|
||||
language: 'zh-CN'
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(500);
|
||||
});
|
||||
|
||||
it('should reject all fields as injection objects simultaneously', async () => {
|
||||
const res = await Call<any, {}, any>(loginApi.default, {
|
||||
body: {
|
||||
username: { $ne: '' },
|
||||
password: { $ne: '' },
|
||||
code: { $ne: '' },
|
||||
language: 'zh-CN'
|
||||
}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(500);
|
||||
});
|
||||
|
||||
it('should reject password as non-string types (array, number)', async () => {
|
||||
const arrayRes = await Call<any, {}, any>(loginApi.default, {
|
||||
body: {
|
||||
username: 'testuser',
|
||||
password: ['testpassword'],
|
||||
code: '123456',
|
||||
language: 'zh-CN'
|
||||
}
|
||||
});
|
||||
expect(arrayRes.code).toBe(500);
|
||||
|
||||
const numberRes = await Call<any, {}, any>(loginApi.default, {
|
||||
body: {
|
||||
username: 'testuser',
|
||||
password: 12345,
|
||||
code: '123456',
|
||||
language: 'zh-CN'
|
||||
}
|
||||
});
|
||||
expect(numberRes.code).toBe(500);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import * as loginoutApi from '@/pages/api/support/user/account/loginout';
|
||||
import { MongoUser } from '@fastgpt/service/support/user/schema';
|
||||
import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema';
|
||||
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
|
||||
import { UserStatusEnum } from '@fastgpt/global/support/user/constant';
|
||||
import { initTeamFreePlan } from '@fastgpt/service/support/wallet/sub/utils';
|
||||
import { Call } from '@test/utils/request';
|
||||
|
||||
describe('loginout API', () => {
|
||||
let testUser: any;
|
||||
let testTeam: any;
|
||||
let testTmb: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
testUser = await MongoUser.create({
|
||||
username: 'testuser',
|
||||
password: 'testpassword',
|
||||
status: UserStatusEnum.active
|
||||
});
|
||||
testTeam = await MongoTeam.create({
|
||||
name: 'Test Team',
|
||||
ownerId: testUser._id
|
||||
});
|
||||
await initTeamFreePlan({ teamId: String(testTeam._id) });
|
||||
testTmb = await MongoTeamMember.create({
|
||||
teamId: testTeam._id,
|
||||
userId: testUser._id,
|
||||
status: 'active',
|
||||
role: 'owner'
|
||||
});
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should logout successfully with valid auth', async () => {
|
||||
const res = await Call(loginoutApi.default, {
|
||||
auth: {
|
||||
userId: String(testUser._id),
|
||||
teamId: String(testTeam._id),
|
||||
tmbId: String(testTmb._id),
|
||||
isRoot: false,
|
||||
sessionId: 'session123'
|
||||
} as any
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
});
|
||||
|
||||
it('should succeed even when unauthenticated (auth errors are caught)', async () => {
|
||||
// loginout catches auth errors and always calls clearCookie
|
||||
const res = await Call(loginoutApi.default, {});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,100 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import * as preLoginApi from '@/pages/api/support/user/account/preLogin';
|
||||
import { MongoUserAuth } from '@fastgpt/service/support/user/auth/schema';
|
||||
import { UserAuthTypeEnum } from '@fastgpt/global/support/user/auth/constants';
|
||||
import { Call } from '@test/utils/request';
|
||||
|
||||
describe('preLogin API', () => {
|
||||
beforeEach(async () => {
|
||||
await MongoUserAuth.deleteMany({});
|
||||
});
|
||||
|
||||
it('should return a 6-char verification code for valid username', async () => {
|
||||
const res = await Call(preLoginApi.default, {
|
||||
query: { username: 'testuser' }
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.data).toBeDefined();
|
||||
expect(typeof res.data.code).toBe('string');
|
||||
expect(res.data.code.length).toBe(6);
|
||||
});
|
||||
|
||||
it('should store the verification code in database', async () => {
|
||||
const res = await Call(preLoginApi.default, {
|
||||
query: { username: 'testuser' }
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
const record = await MongoUserAuth.findOne({
|
||||
key: 'testuser',
|
||||
type: UserAuthTypeEnum.login
|
||||
});
|
||||
expect(record).toBeDefined();
|
||||
expect(record?.code).toBe(res.data.code);
|
||||
});
|
||||
|
||||
it('should generate different codes for different usernames', async () => {
|
||||
const res1 = await Call(preLoginApi.default, {
|
||||
query: { username: 'user1' }
|
||||
});
|
||||
const res2 = await Call(preLoginApi.default, {
|
||||
query: { username: 'user2' }
|
||||
});
|
||||
|
||||
expect(res1.code).toBe(200);
|
||||
expect(res2.code).toBe(200);
|
||||
|
||||
const record1 = await MongoUserAuth.findOne({ key: 'user1', type: UserAuthTypeEnum.login });
|
||||
const record2 = await MongoUserAuth.findOne({ key: 'user2', type: UserAuthTypeEnum.login });
|
||||
expect(record1?.key).toBe('user1');
|
||||
expect(record2?.key).toBe('user2');
|
||||
});
|
||||
|
||||
it('should overwrite previous code for the same username', async () => {
|
||||
await Call(preLoginApi.default, { query: { username: 'testuser' } });
|
||||
const res2 = await Call(preLoginApi.default, { query: { username: 'testuser' } });
|
||||
|
||||
const records = await MongoUserAuth.find({
|
||||
key: 'testuser',
|
||||
type: UserAuthTypeEnum.login
|
||||
});
|
||||
// upsert: only one record per key+type
|
||||
expect(records.length).toBe(1);
|
||||
expect(records[0].code).toBe(res2.data.code);
|
||||
});
|
||||
|
||||
it('should set code expiredTime about 30 seconds from now', async () => {
|
||||
const before = new Date();
|
||||
const res = await Call(preLoginApi.default, {
|
||||
query: { username: 'testuser' }
|
||||
});
|
||||
const after = new Date();
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
const record = await MongoUserAuth.findOne({ key: 'testuser', type: UserAuthTypeEnum.login });
|
||||
expect(record?.expiredTime).toBeDefined();
|
||||
const expiredTime = new Date(record!.expiredTime!).getTime();
|
||||
// Should expire ~30 seconds from now (allow ±2s for test execution)
|
||||
expect(expiredTime).toBeGreaterThanOrEqual(before.getTime() + 28000);
|
||||
expect(expiredTime).toBeLessThanOrEqual(after.getTime() + 32000);
|
||||
});
|
||||
|
||||
it('should reject when username is missing', async () => {
|
||||
const res = await Call(preLoginApi.default, {
|
||||
query: {}
|
||||
});
|
||||
|
||||
expect(res.code).toBe(500);
|
||||
});
|
||||
|
||||
it('should handle root username', async () => {
|
||||
const res = await Call(preLoginApi.default, {
|
||||
query: { username: 'root' }
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.data.code).toBeDefined();
|
||||
expect(res.data.code.length).toBe(6);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,185 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import * as resetExpiredPswApi from '@/pages/api/support/user/account/resetExpiredPsw';
|
||||
import { MongoUser } from '@fastgpt/service/support/user/schema';
|
||||
import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema';
|
||||
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
|
||||
import { UserStatusEnum } from '@fastgpt/global/support/user/constant';
|
||||
import { initTeamFreePlan } from '@fastgpt/service/support/wallet/sub/utils';
|
||||
import type { ResetExpiredPswBodyType } from '@fastgpt/global/openapi/support/user/account/password/api';
|
||||
import { Call } from '@test/utils/request';
|
||||
|
||||
describe('resetExpiredPsw API', () => {
|
||||
let testUser: any;
|
||||
let testTeam: any;
|
||||
let testTmb: any;
|
||||
const originalEnv = process.env.PASSWORD_EXPIRED_MONTH;
|
||||
|
||||
beforeEach(async () => {
|
||||
testUser = await MongoUser.create({
|
||||
username: 'testuser',
|
||||
password: 'oldpassword',
|
||||
status: UserStatusEnum.active
|
||||
});
|
||||
testTeam = await MongoTeam.create({
|
||||
name: 'Test Team',
|
||||
ownerId: testUser._id
|
||||
});
|
||||
await initTeamFreePlan({ teamId: String(testTeam._id) });
|
||||
testTmb = await MongoTeamMember.create({
|
||||
teamId: testTeam._id,
|
||||
userId: testUser._id,
|
||||
status: 'active',
|
||||
role: 'owner'
|
||||
});
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env.PASSWORD_EXPIRED_MONTH = originalEnv;
|
||||
});
|
||||
|
||||
it('should successfully reset password when expired', async () => {
|
||||
process.env.PASSWORD_EXPIRED_MONTH = '1';
|
||||
|
||||
// Set password update time to 2 months ago (expired)
|
||||
const twoMonthsAgo = new Date();
|
||||
twoMonthsAgo.setMonth(twoMonthsAgo.getMonth() - 2);
|
||||
await MongoUser.findByIdAndUpdate(testUser._id, {
|
||||
passwordUpdateTime: twoMonthsAgo
|
||||
});
|
||||
|
||||
const res = await Call<ResetExpiredPswBodyType, {}, any>(resetExpiredPswApi.default, {
|
||||
body: { newPsw: 'newhashedpassword' },
|
||||
auth: {
|
||||
userId: String(testUser._id),
|
||||
teamId: String(testTeam._id),
|
||||
tmbId: String(testTmb._id),
|
||||
isRoot: false,
|
||||
sessionId: 'session123'
|
||||
} as any
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
|
||||
// Verify password was updated
|
||||
const updatedUser = await MongoUser.findById(testUser._id).select(
|
||||
'+password +passwordUpdateTime'
|
||||
);
|
||||
expect(updatedUser?.password).toBeDefined();
|
||||
expect(updatedUser?.passwordUpdateTime).toBeDefined();
|
||||
const newUpdateTime = new Date(updatedUser!.passwordUpdateTime!).getTime();
|
||||
expect(newUpdateTime).toBeGreaterThan(twoMonthsAgo.getTime());
|
||||
});
|
||||
|
||||
it('should reject when password is not expired (PASSWORD_EXPIRED_MONTH not set)', async () => {
|
||||
delete process.env.PASSWORD_EXPIRED_MONTH;
|
||||
|
||||
await MongoUser.findByIdAndUpdate(testUser._id, {
|
||||
passwordUpdateTime: new Date()
|
||||
});
|
||||
|
||||
const res = await Call<ResetExpiredPswBodyType, {}, any>(resetExpiredPswApi.default, {
|
||||
body: { newPsw: 'newhashedpassword' },
|
||||
auth: {
|
||||
userId: String(testUser._id),
|
||||
teamId: String(testTeam._id),
|
||||
tmbId: String(testTmb._id),
|
||||
isRoot: false,
|
||||
sessionId: 'session123'
|
||||
} as any
|
||||
});
|
||||
|
||||
expect(res.code).toBe(500);
|
||||
expect(res.error).toBeDefined();
|
||||
});
|
||||
|
||||
it('should reject when password is not expired (still within expiry period)', async () => {
|
||||
process.env.PASSWORD_EXPIRED_MONTH = '3';
|
||||
|
||||
// Update password just now — not expired
|
||||
await MongoUser.findByIdAndUpdate(testUser._id, {
|
||||
passwordUpdateTime: new Date()
|
||||
});
|
||||
|
||||
const res = await Call<ResetExpiredPswBodyType, {}, any>(resetExpiredPswApi.default, {
|
||||
body: { newPsw: 'newhashedpassword' },
|
||||
auth: {
|
||||
userId: String(testUser._id),
|
||||
teamId: String(testTeam._id),
|
||||
tmbId: String(testTmb._id),
|
||||
isRoot: false,
|
||||
sessionId: 'session123'
|
||||
} as any
|
||||
});
|
||||
|
||||
expect(res.code).toBe(500);
|
||||
});
|
||||
|
||||
it('should reject when newPsw is missing', async () => {
|
||||
process.env.PASSWORD_EXPIRED_MONTH = '1';
|
||||
|
||||
const res = await Call<any, {}, any>(resetExpiredPswApi.default, {
|
||||
body: {},
|
||||
auth: {
|
||||
userId: String(testUser._id),
|
||||
teamId: String(testTeam._id),
|
||||
tmbId: String(testTmb._id),
|
||||
isRoot: false,
|
||||
sessionId: 'session123'
|
||||
} as any
|
||||
});
|
||||
|
||||
expect(res.code).toBe(500);
|
||||
});
|
||||
|
||||
it('should reject when user is not found', async () => {
|
||||
process.env.PASSWORD_EXPIRED_MONTH = '1';
|
||||
|
||||
const nonExistentId = '000000000000000000000001';
|
||||
|
||||
const res = await Call<ResetExpiredPswBodyType, {}, any>(resetExpiredPswApi.default, {
|
||||
body: { newPsw: 'newhashedpassword' },
|
||||
auth: {
|
||||
userId: nonExistentId,
|
||||
teamId: String(testTeam._id),
|
||||
tmbId: String(testTmb._id),
|
||||
isRoot: false,
|
||||
sessionId: 'session123'
|
||||
} as any
|
||||
});
|
||||
|
||||
expect(res.code).toBe(500);
|
||||
expect(res.error).toBe('The password has not expired');
|
||||
});
|
||||
|
||||
it('should reject request without authentication', async () => {
|
||||
const res = await Call<ResetExpiredPswBodyType, {}, any>(resetExpiredPswApi.default, {
|
||||
body: { newPsw: 'newhashedpassword' }
|
||||
});
|
||||
|
||||
expect(res.code).toBe(500);
|
||||
});
|
||||
|
||||
it('should reject newPsw as non-string (injection guard)', async () => {
|
||||
process.env.PASSWORD_EXPIRED_MONTH = '1';
|
||||
|
||||
const twoMonthsAgo = new Date();
|
||||
twoMonthsAgo.setMonth(twoMonthsAgo.getMonth() - 2);
|
||||
await MongoUser.findByIdAndUpdate(testUser._id, {
|
||||
passwordUpdateTime: twoMonthsAgo
|
||||
});
|
||||
|
||||
const res = await Call<any, {}, any>(resetExpiredPswApi.default, {
|
||||
body: { newPsw: { $ne: '' } },
|
||||
auth: {
|
||||
userId: String(testUser._id),
|
||||
teamId: String(testTeam._id),
|
||||
tmbId: String(testTmb._id),
|
||||
isRoot: false,
|
||||
sessionId: 'session123'
|
||||
} as any
|
||||
});
|
||||
|
||||
expect(res.code).toBe(500);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,125 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import * as tokenLoginApi from '@/pages/api/support/user/account/tokenLogin';
|
||||
import { MongoUser } from '@fastgpt/service/support/user/schema';
|
||||
import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema';
|
||||
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
|
||||
import { UserStatusEnum } from '@fastgpt/global/support/user/constant';
|
||||
import { pushTrack } from '@fastgpt/service/common/middle/tracks/utils';
|
||||
import { initTeamFreePlan } from '@fastgpt/service/support/wallet/sub/utils';
|
||||
import { Call } from '@test/utils/request';
|
||||
|
||||
describe('tokenLogin API', () => {
|
||||
let testUser: any;
|
||||
let testTeam: any;
|
||||
let testTmb: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
testUser = await MongoUser.create({
|
||||
username: 'testuser',
|
||||
password: 'testpassword',
|
||||
status: UserStatusEnum.active
|
||||
});
|
||||
testTeam = await MongoTeam.create({
|
||||
name: 'Test Team',
|
||||
ownerId: testUser._id
|
||||
});
|
||||
await initTeamFreePlan({ teamId: String(testTeam._id) });
|
||||
testTmb = await MongoTeamMember.create({
|
||||
teamId: testTeam._id,
|
||||
userId: testUser._id,
|
||||
status: 'active',
|
||||
role: 'owner'
|
||||
});
|
||||
await MongoUser.findByIdAndUpdate(testUser._id, {
|
||||
lastLoginTmbId: testTmb._id
|
||||
});
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return user detail on valid token', async () => {
|
||||
const res = await Call(tokenLoginApi.default, {
|
||||
auth: {
|
||||
userId: String(testUser._id),
|
||||
teamId: String(testTeam._id),
|
||||
tmbId: String(testTmb._id),
|
||||
isRoot: false,
|
||||
sessionId: 'session123'
|
||||
} as any
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.data).toBeDefined();
|
||||
expect(res.data.team).toBeDefined();
|
||||
expect(res.data.team.teamId).toBe(String(testTeam._id));
|
||||
expect(res.data.team.tmbId).toBe(String(testTmb._id));
|
||||
});
|
||||
|
||||
it('should call pushTrack.dailyUserActive', async () => {
|
||||
await Call(tokenLoginApi.default, {
|
||||
auth: {
|
||||
userId: String(testUser._id),
|
||||
teamId: String(testTeam._id),
|
||||
tmbId: String(testTmb._id),
|
||||
isRoot: false,
|
||||
sessionId: 'session123'
|
||||
} as any
|
||||
});
|
||||
|
||||
expect(pushTrack.dailyUserActive).toHaveBeenCalledWith({
|
||||
uid: String(testUser._id),
|
||||
teamId: String(testTeam._id),
|
||||
tmbId: String(testTmb._id)
|
||||
});
|
||||
});
|
||||
|
||||
it('should mask openaiAccount key but keep baseUrl', async () => {
|
||||
await MongoTeamMember.findByIdAndUpdate(testTmb._id, {
|
||||
openaiAccount: { key: 'sk-secret-key', baseUrl: 'https://api.openai.com' }
|
||||
});
|
||||
|
||||
const res = await Call(tokenLoginApi.default, {
|
||||
auth: {
|
||||
userId: String(testUser._id),
|
||||
teamId: String(testTeam._id),
|
||||
tmbId: String(testTmb._id),
|
||||
isRoot: false,
|
||||
sessionId: 'session123'
|
||||
} as any
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
if (res.data.team.openaiAccount) {
|
||||
expect(res.data.team.openaiAccount.key).toBe('');
|
||||
expect(res.data.team.openaiAccount.baseUrl).toBe('https://api.openai.com');
|
||||
}
|
||||
});
|
||||
|
||||
it('should mask all values in externalWorkflowVariables', async () => {
|
||||
await MongoTeamMember.findByIdAndUpdate(testTmb._id, {
|
||||
externalWorkflowVariables: { SECRET: 'top-secret', API_KEY: 'sk-123' }
|
||||
});
|
||||
|
||||
const res = await Call(tokenLoginApi.default, {
|
||||
auth: {
|
||||
userId: String(testUser._id),
|
||||
teamId: String(testTeam._id),
|
||||
tmbId: String(testTmb._id),
|
||||
isRoot: false,
|
||||
sessionId: 'session123'
|
||||
} as any
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
if (res.data.team.externalWorkflowVariables) {
|
||||
Object.values(res.data.team.externalWorkflowVariables).forEach((val) => {
|
||||
expect(val).toBe('');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject request without authentication', async () => {
|
||||
const res = await Call(tokenLoginApi.default, {});
|
||||
|
||||
expect(res.code).toBe(500);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,122 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import * as updateApi from '@/pages/api/support/user/account/update';
|
||||
import { MongoUser } from '@fastgpt/service/support/user/schema';
|
||||
import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema';
|
||||
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
|
||||
import { UserStatusEnum } from '@fastgpt/global/support/user/constant';
|
||||
import { initTeamFreePlan } from '@fastgpt/service/support/wallet/sub/utils';
|
||||
import { Call } from '@test/utils/request';
|
||||
|
||||
describe('update (user account) API', () => {
|
||||
let testUser: any;
|
||||
let testTeam: any;
|
||||
let testTmb: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
testUser = await MongoUser.create({
|
||||
username: 'testuser',
|
||||
password: 'testpassword',
|
||||
status: UserStatusEnum.active,
|
||||
language: 'zh-CN',
|
||||
timezone: 'Asia/Shanghai'
|
||||
});
|
||||
testTeam = await MongoTeam.create({
|
||||
name: 'Test Team',
|
||||
ownerId: testUser._id
|
||||
});
|
||||
await initTeamFreePlan({ teamId: String(testTeam._id) });
|
||||
testTmb = await MongoTeamMember.create({
|
||||
teamId: testTeam._id,
|
||||
userId: testUser._id,
|
||||
status: 'active',
|
||||
role: 'owner'
|
||||
});
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const makeAuth = (user: any, team: any, tmb: any) => ({
|
||||
userId: String(user._id),
|
||||
teamId: String(team._id),
|
||||
tmbId: String(tmb._id),
|
||||
isRoot: false,
|
||||
sessionId: 'session123'
|
||||
});
|
||||
|
||||
it('should update language successfully', async () => {
|
||||
const res = await Call(updateApi.default, {
|
||||
body: { language: 'en' },
|
||||
auth: makeAuth(testUser, testTeam, testTmb) as any
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
const updatedUser = await MongoUser.findById(testUser._id);
|
||||
expect(updatedUser?.language).toBe('en');
|
||||
});
|
||||
|
||||
it('should update timezone successfully', async () => {
|
||||
const res = await Call(updateApi.default, {
|
||||
body: { timezone: 'America/New_York' },
|
||||
auth: makeAuth(testUser, testTeam, testTmb) as any
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
const updatedUser = await MongoUser.findById(testUser._id);
|
||||
expect(updatedUser?.timezone).toBe('America/New_York');
|
||||
});
|
||||
|
||||
it('should update both language and timezone simultaneously', async () => {
|
||||
const res = await Call(updateApi.default, {
|
||||
body: { language: 'zh-Hant', timezone: 'Asia/Tokyo' },
|
||||
auth: makeAuth(testUser, testTeam, testTmb) as any
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
const updatedUser = await MongoUser.findById(testUser._id);
|
||||
expect(updatedUser?.language).toBe('zh-Hant');
|
||||
expect(updatedUser?.timezone).toBe('Asia/Tokyo');
|
||||
});
|
||||
|
||||
it('should update avatar on team member', async () => {
|
||||
const newAvatar = '/avatar/test-avatar.png';
|
||||
|
||||
const res = await Call(updateApi.default, {
|
||||
body: { avatar: newAvatar },
|
||||
auth: makeAuth(testUser, testTeam, testTmb) as any
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
const updatedTmb = await MongoTeamMember.findById(testTmb._id);
|
||||
expect(updatedTmb?.avatar).toBe(newAvatar);
|
||||
});
|
||||
|
||||
it('should return empty object on success', async () => {
|
||||
const res = await Call(updateApi.default, {
|
||||
body: { language: 'en' },
|
||||
auth: makeAuth(testUser, testTeam, testTmb) as any
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
expect(res.data).toEqual({});
|
||||
});
|
||||
|
||||
it('should handle empty body without error', async () => {
|
||||
const res = await Call(updateApi.default, {
|
||||
body: {},
|
||||
auth: makeAuth(testUser, testTeam, testTmb) as any
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
// Nothing should change
|
||||
const updatedUser = await MongoUser.findById(testUser._id);
|
||||
expect(updatedUser?.language).toBe('zh-CN');
|
||||
expect(updatedUser?.timezone).toBe('Asia/Shanghai');
|
||||
});
|
||||
|
||||
it('should reject request without authentication', async () => {
|
||||
const res = await Call(updateApi.default, {
|
||||
body: { language: 'en' }
|
||||
});
|
||||
|
||||
expect(res.code).toBe(500);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import * as updatePasswordApi from '@/pages/api/support/user/account/updatePasswordByOld';
|
||||
import { MongoUser } from '@fastgpt/service/support/user/schema';
|
||||
import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema';
|
||||
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
|
||||
import { UserStatusEnum } from '@fastgpt/global/support/user/constant';
|
||||
import { initTeamFreePlan } from '@fastgpt/service/support/wallet/sub/utils';
|
||||
import type { UpdatePasswordByOldBodyType } from '@fastgpt/global/openapi/support/user/account/password/api';
|
||||
import { Call } from '@test/utils/request';
|
||||
|
||||
describe('updatePasswordByOld API', () => {
|
||||
let testUser: any;
|
||||
let testTeam: any;
|
||||
let testTmb: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
testUser = await MongoUser.create({
|
||||
username: 'testuser',
|
||||
password: 'oldhashpassword',
|
||||
status: UserStatusEnum.active
|
||||
});
|
||||
testTeam = await MongoTeam.create({
|
||||
name: 'Test Team',
|
||||
ownerId: testUser._id
|
||||
});
|
||||
await initTeamFreePlan({ teamId: String(testTeam._id) });
|
||||
testTmb = await MongoTeamMember.create({
|
||||
teamId: testTeam._id,
|
||||
userId: testUser._id,
|
||||
status: 'active',
|
||||
role: 'owner'
|
||||
});
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const makeAuth = (user: any, team: any, tmb: any) => ({
|
||||
userId: String(user._id),
|
||||
teamId: String(team._id),
|
||||
tmbId: String(tmb._id),
|
||||
isRoot: false,
|
||||
sessionId: 'session123'
|
||||
});
|
||||
|
||||
it('should update password successfully with correct old password', async () => {
|
||||
const res = await Call<UpdatePasswordByOldBodyType, {}, any>(updatePasswordApi.default, {
|
||||
body: { oldPsw: 'oldhashpassword', newPsw: 'newhashpassword' },
|
||||
auth: makeAuth(testUser, testTeam, testTmb) as any
|
||||
});
|
||||
|
||||
expect(res.code).toBe(200);
|
||||
|
||||
const updatedUser = await MongoUser.findById(testUser._id).select('+password');
|
||||
expect(updatedUser?.password).toBeDefined();
|
||||
expect(updatedUser?.passwordUpdateTime).toBeDefined();
|
||||
});
|
||||
|
||||
it('should reject when old password is incorrect', async () => {
|
||||
const res = await Call<UpdatePasswordByOldBodyType, {}, any>(updatePasswordApi.default, {
|
||||
body: { oldPsw: 'wrongpassword', newPsw: 'newhashpassword' },
|
||||
auth: makeAuth(testUser, testTeam, testTmb) as any
|
||||
});
|
||||
|
||||
expect(res.code).toBe(500);
|
||||
|
||||
// Password should not change
|
||||
const user = await MongoUser.findById(testUser._id).select('+passwordUpdateTime');
|
||||
expect(user?.passwordUpdateTime).toBeUndefined(); // we didn't set it initially
|
||||
});
|
||||
|
||||
it('should reject when old and new passwords are the same', async () => {
|
||||
const res = await Call<UpdatePasswordByOldBodyType, {}, any>(updatePasswordApi.default, {
|
||||
body: { oldPsw: 'oldhashpassword', newPsw: 'oldhashpassword' },
|
||||
auth: makeAuth(testUser, testTeam, testTmb) as any
|
||||
});
|
||||
|
||||
expect(res.code).toBe(500);
|
||||
});
|
||||
|
||||
it('should reject when oldPsw is missing', async () => {
|
||||
const res = await Call<any, {}, any>(updatePasswordApi.default, {
|
||||
body: { newPsw: 'newhashpassword' },
|
||||
auth: makeAuth(testUser, testTeam, testTmb) as any
|
||||
});
|
||||
|
||||
expect(res.code).toBe(500);
|
||||
});
|
||||
|
||||
it('should reject when newPsw is missing', async () => {
|
||||
const res = await Call<any, {}, any>(updatePasswordApi.default, {
|
||||
body: { oldPsw: 'oldhashpassword' },
|
||||
auth: makeAuth(testUser, testTeam, testTmb) as any
|
||||
});
|
||||
|
||||
expect(res.code).toBe(500);
|
||||
});
|
||||
|
||||
it('should reject request without authentication', async () => {
|
||||
const res = await Call<UpdatePasswordByOldBodyType, {}, any>(updatePasswordApi.default, {
|
||||
body: { oldPsw: 'oldhashpassword', newPsw: 'newhashpassword' }
|
||||
});
|
||||
|
||||
expect(res.code).toBe(500);
|
||||
});
|
||||
|
||||
// ===== Security: NoSQL injection prevention (GHSA-jxvr-h2vx-p73r Step 3) =====
|
||||
|
||||
it('should reject oldPsw as MongoDB operator object ($ne injection)', async () => {
|
||||
// GHSA-jxvr-h2vx-p73r Step 3: oldPsw: {"$ne": ""} bypasses old password check
|
||||
const res = await Call<any, {}, any>(updatePasswordApi.default, {
|
||||
body: { oldPsw: { $ne: '' }, newPsw: 'newhashpassword' },
|
||||
auth: makeAuth(testUser, testTeam, testTmb) as any
|
||||
});
|
||||
|
||||
// Zod z.string() must reject object-type oldPsw
|
||||
expect(res.code).toBe(500);
|
||||
|
||||
// Password must NOT be changed
|
||||
const user = await MongoUser.findById(testUser._id).select('+passwordUpdateTime');
|
||||
expect(user?.passwordUpdateTime).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should reject oldPsw with $regex injection', async () => {
|
||||
const res = await Call<any, {}, any>(updatePasswordApi.default, {
|
||||
body: { oldPsw: { $regex: '.*' }, newPsw: 'newhashpassword' },
|
||||
auth: makeAuth(testUser, testTeam, testTmb) as any
|
||||
});
|
||||
|
||||
expect(res.code).toBe(500);
|
||||
});
|
||||
|
||||
it('should reject newPsw as non-string type', async () => {
|
||||
const res = await Call<any, {}, any>(updatePasswordApi.default, {
|
||||
body: { oldPsw: 'oldhashpassword', newPsw: { $ne: '' } },
|
||||
auth: makeAuth(testUser, testTeam, testTmb) as any
|
||||
});
|
||||
|
||||
expect(res.code).toBe(500);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user