diff --git a/server/models/User.js b/server/models/User.js index 65b3363..cf9c393 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -203,6 +203,11 @@ class User { */ static async findByIdAndUpdate(id, updateData) { try { + // Hash password if provided + if (updateData.password) { + const salt = await bcrypt.genSalt(10); + updateData.password = await bcrypt.hash(updateData.password, salt); + } // Convert to snake_case for Supabase const snakeCaseData = {}; @@ -236,6 +241,10 @@ class User { */ async save() { try { + if (this.password) { + const salt = await bcrypt.genSalt(10); + this.password = await bcrypt.hash(this.password, salt); + } // Convert user object to snake_case for Supabase const userData = { username: this.username, diff --git a/tests/server/models/user-password.test.js b/tests/server/models/user-password.test.js new file mode 100644 index 0000000..039a8be --- /dev/null +++ b/tests/server/models/user-password.test.js @@ -0,0 +1,43 @@ +const bcrypt = require('bcrypt'); + +jest.mock('../../../server/utils/database', () => { + const chain = { + from: jest.fn(() => chain), + update: jest.fn(() => chain), + eq: jest.fn(() => chain), + select: jest.fn(() => chain), + single: jest.fn(() => Promise.resolve({ data: { id: 'user1', password: 'hashed' }, error: null })) + }; + return { supabase: chain, supabaseAdmin: chain }; +}); + +const User = require('../../../server/models/User'); + +describe('User password hashing', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('hashes password on findByIdAndUpdate', async () => { + jest.spyOn(bcrypt, 'genSalt').mockResolvedValue('salt'); + jest.spyOn(bcrypt, 'hash').mockResolvedValue('hashed_pw'); + await User.findByIdAndUpdate('user1', { password: 'plain' }); + expect(bcrypt.hash).toHaveBeenCalledWith('plain', 'salt'); + const { supabaseAdmin } = require('../../../server/utils/database'); + expect(supabaseAdmin.update).toHaveBeenCalledWith({ password: 'hashed_pw' }); + }); + + it('hashes password on save', async () => { + jest.spyOn(bcrypt, 'genSalt').mockResolvedValue('salt2'); + jest.spyOn(bcrypt, 'hash').mockResolvedValue('hashed_pw2'); + const user = new User(); + user.id = 'user2'; + user.username = ''; + user.email = ''; + user.password = 'plain2'; + await user.save(); + expect(bcrypt.hash).toHaveBeenCalledWith('plain2', 'salt2'); + const { supabaseAdmin } = require('../../../server/utils/database'); + expect(supabaseAdmin.update).toHaveBeenCalledWith(expect.objectContaining({ password: 'hashed_pw2' })); + }); +}); diff --git a/tests/server/routes/index.test.js b/tests/server/routes/index.test.js index f8c613b..a994d37 100644 --- a/tests/server/routes/index.test.js +++ b/tests/server/routes/index.test.js @@ -4,6 +4,10 @@ const request = require('supertest'); const express = require('express'); +process.env.SUPABASE_URL = process.env.SUPABASE_URL || 'http://localhost'; +process.env.SUPABASE_KEY = process.env.SUPABASE_KEY || 'key'; +process.env.SUPABASE_SERVICE_KEY = process.env.SUPABASE_SERVICE_KEY || 'service'; + // Mock dependencies jest.mock('../../../server/models/User', () => ({ findRecent: jest.fn().mockResolvedValue([ @@ -21,7 +25,7 @@ jest.mock('../../../server/models/Item', () => ({ { id: 3, title: 'Featured Item 1' }, { id: 4, title: 'Featured Item 2' } ]) -})); +}), { virtual: true }); // Mock express-handlebars jest.mock('express-handlebars', () => ({ diff --git a/tests/wir-transactions.test.js b/tests/wir-transactions.test.js index db0f443..8246c24 100644 --- a/tests/wir-transactions.test.js +++ b/tests/wir-transactions.test.js @@ -3,6 +3,10 @@ * Tests for the WIR currency system and transactions */ +process.env.SUPABASE_URL = process.env.SUPABASE_URL || 'http://localhost'; +process.env.SUPABASE_KEY = process.env.SUPABASE_KEY || 'key'; +process.env.SUPABASE_SERVICE_KEY = process.env.SUPABASE_SERVICE_KEY || 'service'; + const { supabase } = require('../server/utils/database'); const WIRTransaction = require('../server/models/WIRTransaction'); const createWIRTransactionsTable = require('../scripts/migrations/create-wir-transactions-table');