From b6323c57e103b59640aeb17b08c9cba52b6ec1fe Mon Sep 17 00:00:00 2001 From: Ayush Pratap Singh Date: Sat, 11 Jul 2026 15:58:11 +0530 Subject: [PATCH 1/5] Fix Social Auth Sign-Up Validation Failures --- .../src/__tests__/userAuth.social.test.js | 57 +++++++++++++++++++ .../src/controllers/userAuth.controller.js | 22 ++++++- package-lock.json | 26 --------- 3 files changed, 78 insertions(+), 27 deletions(-) diff --git a/apps/public-api/src/__tests__/userAuth.social.test.js b/apps/public-api/src/__tests__/userAuth.social.test.js index 5b78db78..c7e49533 100644 --- a/apps/public-api/src/__tests__/userAuth.social.test.js +++ b/apps/public-api/src/__tests__/userAuth.social.test.js @@ -690,4 +690,61 @@ describe('public userAuth social auth', () => { expect(res.redirect).toHaveBeenCalledWith(expect.stringContaining('error=')); expect(res.redirect).toHaveBeenCalledWith(expect.stringContaining('deletion')); }); + + test('handleSocialAuthCallback handles ValidationError on create by saving with validateBeforeSave: false', async () => { + redis.get.mockResolvedValueOnce(JSON.stringify({ + projectId: 'project_1', + provider: 'github', + callbackUrl: 'http://localhost:5173/auth/callback', + })); + mockProjectFindByIdChain.lean.mockResolvedValueOnce(makeProject()); + + mockUsersModel.findOne + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null); + + const mockSave = jest.fn().mockResolvedValue(undefined); + class MockModel { + constructor(payload) { + Object.assign(this, payload); + this._id = 'mocked_user_id'; + } + } + MockModel.prototype.save = mockSave; + MockModel.findOne = jest.fn() + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null); + MockModel.create = jest.fn().mockImplementation(() => { + const error = new Error('users validation failed: customField: Path `customField` is required.'); + error.name = 'ValidationError'; + throw error; + }); + + const { getCompiledModel } = require('@urbackend/common'); + getCompiledModel.mockReturnValueOnce(MockModel); + + global.fetch + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ access_token: 'github_access_token' }), + }) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ id: 123, login: 'alice', avatar_url: '' }), + }) + .mockResolvedValueOnce({ + ok: true, + json: async () => ([{ email: 'alice@example.com', verified: true, primary: true }]), + }); + + const req = makeReq({ params: { provider: 'github' }, query: { code: 'code_1', state: 'state_1' } }); + const res = makeRes(); + + await controller.handleSocialAuthCallback(req, res); + + expect(MockModel.create).toHaveBeenCalled(); + expect(mockSave).toHaveBeenCalledWith({ validateBeforeSave: false }); + expect(res.redirect).toHaveBeenCalledWith(expect.stringContaining('/auth/callback?')); + expect(res.redirect).toHaveBeenCalledWith(expect.stringContaining('userId=mocked_user_id')); + }); }); diff --git a/apps/public-api/src/controllers/userAuth.controller.js b/apps/public-api/src/controllers/userAuth.controller.js index da94a698..f9e48648 100644 --- a/apps/public-api/src/controllers/userAuth.controller.js +++ b/apps/public-api/src/controllers/userAuth.controller.js @@ -580,7 +580,27 @@ const findOrCreateSocialUser = async ({ project, usersColConfig, Model, provider newUserPayload.avatarUrl = profile.avatarUrl; } - user = await Model.create(newUserPayload); + try { + user = await Model.create(newUserPayload); + } catch (err) { + if (err.name === 'ValidationError') { + try { + if (typeof Model === 'function') { + const doc = new Model(newUserPayload); + await doc.save({ validateBeforeSave: false }); + user = doc; + } else { + user = await Model.create(newUserPayload); + } + } catch (saveErr) { + const validationError = new Error(`Validation failed during social signup: ${err.message}`); + validationError.statusCode = 400; + throw validationError; + } + } else { + throw err; + } + } return { user, isNewUser: true, linkedByEmail: false }; }; diff --git a/package-lock.json b/package-lock.json index 76c104f7..8df804eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2909,7 +2909,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2943,7 +2942,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2977,7 +2975,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3043,7 +3040,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6876,7 +6872,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6893,7 +6888,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6910,7 +6904,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6927,7 +6920,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6944,7 +6936,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6961,7 +6952,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6978,7 +6968,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6995,7 +6984,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7012,7 +7000,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7029,7 +7016,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7046,7 +7032,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7063,7 +7048,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7080,7 +7064,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7097,7 +7080,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7114,7 +7096,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7131,7 +7112,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7148,7 +7128,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7165,7 +7144,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7182,7 +7160,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7199,7 +7176,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7216,7 +7192,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7233,7 +7208,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ From 61a8f8748779bc5a8752b79f1b7dbab7d064b0d3 Mon Sep 17 00:00:00 2001 From: AYUSH PRATAP SINGH Date: Wed, 15 Jul 2026 14:05:06 +0530 Subject: [PATCH 2/5] Improve error handling for social signup Handle duplicate user error during social signup. --- .../src/controllers/userAuth.controller.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/apps/public-api/src/controllers/userAuth.controller.js b/apps/public-api/src/controllers/userAuth.controller.js index f9e48648..32e6d11a 100644 --- a/apps/public-api/src/controllers/userAuth.controller.js +++ b/apps/public-api/src/controllers/userAuth.controller.js @@ -592,11 +592,17 @@ const findOrCreateSocialUser = async ({ project, usersColConfig, Model, provider } else { user = await Model.create(newUserPayload); } - } catch (saveErr) { - const validationError = new Error(`Validation failed during social signup: ${err.message}`); - validationError.statusCode = 400; - throw validationError; - } + } + catch (saveErr) { + if (saveErr.code === 11000) { + throw new AppError("User already exists.", 409); + } + + throw new AppError( + "Failed to complete social signup.", + 500 + ); +} } else { throw err; } From fa439a6d1b4b881cc278fb8a26d5ba55061739a2 Mon Sep 17 00:00:00 2001 From: Yash Pouranik Date: Wed, 15 Jul 2026 15:42:21 +0530 Subject: [PATCH 3/5] Update apps/public-api/src/controllers/userAuth.controller.js Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- apps/public-api/src/controllers/userAuth.controller.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/public-api/src/controllers/userAuth.controller.js b/apps/public-api/src/controllers/userAuth.controller.js index 32e6d11a..d578823b 100644 --- a/apps/public-api/src/controllers/userAuth.controller.js +++ b/apps/public-api/src/controllers/userAuth.controller.js @@ -604,7 +604,10 @@ const findOrCreateSocialUser = async ({ project, usersColConfig, Model, provider ); } } else { - throw err; + if (err.code === 11000) { + throw new AppError("User already exists.", 409); + } + throw new AppError("Failed to complete social signup.", 500); } } return { user, isNewUser: true, linkedByEmail: false }; From ae65fa475922344f7873e5aa7d4e9d9148bee034 Mon Sep 17 00:00:00 2001 From: AYUSH PRATAP SINGH Date: Wed, 15 Jul 2026 16:07:42 +0530 Subject: [PATCH 4/5] Refactor error handling in userAuth.controller --- .../src/controllers/userAuth.controller.js | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/apps/public-api/src/controllers/userAuth.controller.js b/apps/public-api/src/controllers/userAuth.controller.js index d578823b..b735d4e3 100644 --- a/apps/public-api/src/controllers/userAuth.controller.js +++ b/apps/public-api/src/controllers/userAuth.controller.js @@ -583,31 +583,26 @@ const findOrCreateSocialUser = async ({ project, usersColConfig, Model, provider try { user = await Model.create(newUserPayload); } catch (err) { - if (err.name === 'ValidationError') { + if (err.name === "ValidationError") { try { - if (typeof Model === 'function') { + if (typeof Model === "function") { const doc = new Model(newUserPayload); await doc.save({ validateBeforeSave: false }); user = doc; } else { user = await Model.create(newUserPayload); } - } - catch (saveErr) { - if (saveErr.code === 11000) { - throw new AppError("User already exists.", 409); - } - - throw new AppError( - "Failed to complete social signup.", - 500 - ); -} + } catch (saveErr) { + if (saveErr.code === 11000) { + throw new AppError(409, "User already exists."); + } + throw new AppError(500, "Failed to complete social signup."); + } } else { if (err.code === 11000) { - throw new AppError("User already exists.", 409); + throw new AppError(409, "User already exists."); } - throw new AppError("Failed to complete social signup.", 500); + throw new AppError(500, "Failed to complete social signup."); } } return { user, isNewUser: true, linkedByEmail: false }; @@ -1865,3 +1860,10 @@ module.exports.updateAdminUser = async (req, res) => { res.status(500).json({ error: err.message }); } }; + + + + + + + From 80b413b1bfe8a4b184af6db3dc5dec4dba412415 Mon Sep 17 00:00:00 2001 From: AYUSH PRATAP SINGH Date: Wed, 15 Jul 2026 16:10:08 +0530 Subject: [PATCH 5/5] Add AppError class and improve social auth tests --- .../src/__tests__/userAuth.social.test.js | 199 ++++++++++++++++++ 1 file changed, 199 insertions(+) diff --git a/apps/public-api/src/__tests__/userAuth.social.test.js b/apps/public-api/src/__tests__/userAuth.social.test.js index c7e49533..308c5559 100644 --- a/apps/public-api/src/__tests__/userAuth.social.test.js +++ b/apps/public-api/src/__tests__/userAuth.social.test.js @@ -79,6 +79,15 @@ jest.mock('@urbackend/common', () => { if (encrypted.encrypted === 'google') return 'google_secret'; return null; }), + AppError: class AppError extends Error { + constructor(statusCode, message, error = null) { + super(message); + this.statusCode = statusCode; + this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error'; + this.error = error || (statusCode >= 500 ? "Internal Server Error" : "Error"); + this.isOperational = true; + } + }, }; }); @@ -747,4 +756,194 @@ describe('public userAuth social auth', () => { expect(res.redirect).toHaveBeenCalledWith(expect.stringContaining('/auth/callback?')); expect(res.redirect).toHaveBeenCalledWith(expect.stringContaining('userId=mocked_user_id')); }); + + test('handleSocialAuthCallback handles ValidationError on create where save fails with 11000 duplicate key error', async () => { + redis.get.mockResolvedValueOnce(JSON.stringify({ + projectId: 'project_1', + provider: 'github', + callbackUrl: 'http://localhost:5173/auth/callback', + })); + mockProjectFindByIdChain.lean.mockResolvedValueOnce(makeProject()); + + mockUsersModel.findOne + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null); + + const mockSave = jest.fn().mockRejectedValue({ code: 11000 }); + class MockModel { + constructor(payload) { + Object.assign(this, payload); + } + } + MockModel.prototype.save = mockSave; + MockModel.findOne = jest.fn() + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null); + MockModel.create = jest.fn().mockImplementation(() => { + const error = new Error('users validation failed: customField: Path `customField` is required.'); + error.name = 'ValidationError'; + throw error; + }); + + const { getCompiledModel } = require('@urbackend/common'); + getCompiledModel.mockReturnValueOnce(MockModel); + + global.fetch + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ access_token: 'github_access_token' }), + }) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ id: 123, login: 'alice', avatar_url: '' }), + }) + .mockResolvedValueOnce({ + ok: true, + json: async () => ([{ email: 'alice@example.com', verified: true, primary: true }]), + }); + + const req = makeReq({ params: { provider: 'github' }, query: { code: 'code_1', state: 'state_1' } }); + const res = makeRes(); + + await controller.handleSocialAuthCallback(req, res); + + expect(MockModel.create).toHaveBeenCalled(); + expect(mockSave).toHaveBeenCalledWith({ validateBeforeSave: false }); + expect(res.redirect).toHaveBeenCalledWith(expect.stringContaining('error=User+already+exists.')); + }); + + test('handleSocialAuthCallback handles ValidationError on create where save fails with generic database error', async () => { + redis.get.mockResolvedValueOnce(JSON.stringify({ + projectId: 'project_1', + provider: 'github', + callbackUrl: 'http://localhost:5173/auth/callback', + })); + mockProjectFindByIdChain.lean.mockResolvedValueOnce(makeProject()); + + mockUsersModel.findOne + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null); + + const mockSave = jest.fn().mockRejectedValue(new Error('Connection timed out')); + class MockModel { + constructor(payload) { + Object.assign(this, payload); + } + } + MockModel.prototype.save = mockSave; + MockModel.findOne = jest.fn() + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null); + MockModel.create = jest.fn().mockImplementation(() => { + const error = new Error('users validation failed: customField: Path `customField` is required.'); + error.name = 'ValidationError'; + throw error; + }); + + const { getCompiledModel } = require('@urbackend/common'); + getCompiledModel.mockReturnValueOnce(MockModel); + + global.fetch + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ access_token: 'github_access_token' }), + }) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ id: 123, login: 'alice', avatar_url: '' }), + }) + .mockResolvedValueOnce({ + ok: true, + json: async () => ([{ email: 'alice@example.com', verified: true, primary: true }]), + }); + + const req = makeReq({ params: { provider: 'github' }, query: { code: 'code_1', state: 'state_1' } }); + const res = makeRes(); + + await controller.handleSocialAuthCallback(req, res); + + expect(MockModel.create).toHaveBeenCalled(); + expect(mockSave).toHaveBeenCalledWith({ validateBeforeSave: false }); + expect(res.redirect).toHaveBeenCalledWith(expect.stringContaining('error=Failed+to+complete+social+signup.')); + }); + + test('handleSocialAuthCallback handles direct duplicate key error on Model.create', async () => { + redis.get.mockResolvedValueOnce(JSON.stringify({ + projectId: 'project_1', + provider: 'github', + callbackUrl: 'http://localhost:5173/auth/callback', + })); + mockProjectFindByIdChain.lean.mockResolvedValueOnce(makeProject()); + + mockUsersModel.findOne + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null); + + mockUsersModel.create.mockImplementationOnce(() => { + const error = new Error('E11000 duplicate key error collection'); + error.code = 11000; + throw error; + }); + + global.fetch + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ access_token: 'github_access_token' }), + }) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ id: 123, login: 'alice', avatar_url: '' }), + }) + .mockResolvedValueOnce({ + ok: true, + json: async () => ([{ email: 'alice@example.com', verified: true, primary: true }]), + }); + + const req = makeReq({ params: { provider: 'github' }, query: { code: 'code_1', state: 'state_1' } }); + const res = makeRes(); + + await controller.handleSocialAuthCallback(req, res); + + expect(mockUsersModel.create).toHaveBeenCalled(); + expect(res.redirect).toHaveBeenCalledWith(expect.stringContaining('error=User+already+exists.')); + }); + + test('handleSocialAuthCallback handles direct generic database error on Model.create', async () => { + redis.get.mockResolvedValueOnce(JSON.stringify({ + projectId: 'project_1', + provider: 'github', + callbackUrl: 'http://localhost:5173/auth/callback', + })); + mockProjectFindByIdChain.lean.mockResolvedValueOnce(makeProject()); + + mockUsersModel.findOne + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null); + + mockUsersModel.create.mockImplementationOnce(() => { + throw new Error('Database connection lost'); + }); + + global.fetch + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ access_token: 'github_access_token' }), + }) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ id: 123, login: 'alice', avatar_url: '' }), + }) + .mockResolvedValueOnce({ + ok: true, + json: async () => ([{ email: 'alice@example.com', verified: true, primary: true }]), + }); + + const req = makeReq({ params: { provider: 'github' }, query: { code: 'code_1', state: 'state_1' } }); + const res = makeRes(); + + await controller.handleSocialAuthCallback(req, res); + + expect(mockUsersModel.create).toHaveBeenCalled(); + expect(res.redirect).toHaveBeenCalledWith(expect.stringContaining('error=Failed+to+complete+social+signup.')); + }); });