mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-10-21 17:44:11 +00:00
* 📮 fix: Consume an Invite Only Once the Account Exists `checkInviteUser` deleted the invite token and then called `next()`, but everything that can still reject a registration runs after it: the schema, the allowed-domain check, and the email-already-in-use check. A mistyped password confirmation therefore destroyed the invite — the invitee corrected it, resubmitted, and got "Invalid invite token" with no way back short of an admin re-inviting them. The deletion moves to `registrationController`, after `registerUser` reports success. That report needed a new signal. `registerUser` returns the same 200 and the same generic message whether it created an account or found the email already in use — deliberately, so the response cannot be used to enumerate accounts — so the status alone cannot say whether an account exists. It now also returns `userCreated` on the creation path, which the controller reads and never forwards; the response body is unchanged. A failed deletion is logged rather than surfaced. By that point the account exists, and leaving a usable invite behind is recoverable in a way that telling the user their registration failed is not. Fixes #15541 * ♻️ test: Fold the Invite Tests Into the Existing AuthController Spec From Copilot's review. The new spec reached for `jest.requireActual` on `@librechat/data-schemas`, `~/server/services/AuthService` and `~/models`, which pulls real implementations into a unit test — `~/models` builds the data-schemas methods against mongoose — and diverges from `AuthController.spec.js`, which stubs each module outright. Rather than restate that harness with narrower stubs, the tests move into the spec that already has it. `deleteTokens` joins its `~/models` mock, which also keeps that shared mock in step with the controller's imports: a stale mock there hands the controller an undefined function, and the omission only surfaces when some later test happens to exercise the path.
70 lines
2.2 KiB
JavaScript
70 lines
2.2 KiB
JavaScript
const mockGetInvite = jest.fn();
|
|
const mockDeleteTokens = jest.fn();
|
|
|
|
jest.mock('@librechat/api', () => ({
|
|
getInvite: (...args) => mockGetInvite(...args),
|
|
}));
|
|
|
|
jest.mock('~/models', () => ({
|
|
createToken: jest.fn(),
|
|
findToken: jest.fn(),
|
|
deleteTokens: (...args) => mockDeleteTokens(...args),
|
|
}));
|
|
|
|
const checkInviteUser = require('./checkInviteUser');
|
|
|
|
const buildRes = () => {
|
|
const res = {};
|
|
res.status = jest.fn(() => res);
|
|
res.json = jest.fn(() => res);
|
|
return res;
|
|
};
|
|
|
|
describe('checkInviteUser', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
it('does not consume the invite, so a later failure cannot destroy it', async () => {
|
|
/** The reported bug: the invite was deleted here, before the schema, the
|
|
* allowed-domain check or the email-in-use check had run. A mistyped password
|
|
* confirmation left the invitee with no invite and no account. */
|
|
mockGetInvite.mockResolvedValue({ token: 'hashed-invite', email: 'invitee@example.com' });
|
|
const req = { body: { token: 'raw-token', email: 'invitee@example.com' } };
|
|
const next = jest.fn();
|
|
|
|
await checkInviteUser(req, buildRes(), next);
|
|
|
|
expect(mockDeleteTokens).not.toHaveBeenCalled();
|
|
expect(next).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('hands the validated invite to the rest of the chain', async () => {
|
|
const invite = { token: 'hashed-invite', email: 'invitee@example.com' };
|
|
mockGetInvite.mockResolvedValue(invite);
|
|
const req = { body: { token: 'raw-token', email: 'invitee@example.com' } };
|
|
|
|
await checkInviteUser(req, buildRes(), jest.fn());
|
|
|
|
expect(req.invite).toBe(invite);
|
|
});
|
|
|
|
it('rejects an invalid invite without touching the token store', async () => {
|
|
mockGetInvite.mockResolvedValue(null);
|
|
const res = buildRes();
|
|
|
|
await checkInviteUser({ body: { token: 'raw-token', email: 'a@b.c' } }, res, jest.fn());
|
|
|
|
expect(res.status).toHaveBeenCalledWith(400);
|
|
expect(mockDeleteTokens).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('passes straight through when no invite token is supplied', async () => {
|
|
const next = jest.fn();
|
|
|
|
await checkInviteUser({ body: {} }, buildRes(), next);
|
|
|
|
expect(next).toHaveBeenCalledTimes(1);
|
|
expect(mockGetInvite).not.toHaveBeenCalled();
|
|
});
|
|
});
|