From 8456e85cfefd2044cc54be680e7ea848ca10fc2f Mon Sep 17 00:00:00 2001 From: Sergei Date: Thu, 29 Jan 2026 11:47:23 -0800 Subject: [PATCH] Remove incorrect beneficiary schema from auth endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed GET /auth/me and POST /auth/verify-otp endpoints to use the correct beneficiaries table schema. Previously, these endpoints were querying for fields like email, first_name, last_name, address_street which don't exist in the actual beneficiaries table, causing empty/incorrect data to be returned. Changes: - Updated Supabase queries to fetch correct fields: name, phone, address, avatar_url, equipment_status, created_at - Fixed response mapping to use 'name' instead of 'first_name'/'last_name' - Added proper equipmentStatus and hasDevices calculations - Removed spread operator that was adding incorrect fields to response Added comprehensive tests to verify correct schema usage and ensure beneficiary data is returned with the proper structure. πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../__tests__/auth-beneficiaries.test.js | 395 ++++++++++++++++++ backend/src/routes/auth.js | 46 +- 2 files changed, 421 insertions(+), 20 deletions(-) create mode 100644 backend/src/routes/__tests__/auth-beneficiaries.test.js diff --git a/backend/src/routes/__tests__/auth-beneficiaries.test.js b/backend/src/routes/__tests__/auth-beneficiaries.test.js new file mode 100644 index 0000000..f5cce7d --- /dev/null +++ b/backend/src/routes/__tests__/auth-beneficiaries.test.js @@ -0,0 +1,395 @@ +const request = require('supertest'); +const express = require('express'); +const jwt = require('jsonwebtoken'); +const { supabase } = require('../../config/supabase'); + +// Set JWT_SECRET for tests +process.env.JWT_SECRET = 'test-secret'; + +// Mock supabase +jest.mock('../../config/supabase', () => ({ + supabase: { + from: jest.fn(), + auth: { + getUser: jest.fn() + } + } +})); + +// Mock email service +jest.mock('../../services/email', () => ({ + sendOTPEmail: jest.fn().mockResolvedValue(true) +})); + +// Mock storage +jest.mock('../../services/storage', () => ({ + uploadBase64Image: jest.fn(), + isConfigured: jest.fn().mockReturnValue(false) +})); + +const authRouter = require('../auth'); + +const app = express(); +app.use(express.json()); +app.use('/api/auth', authRouter); + +// Helper to generate valid JWT token +function generateToken(userId = 1, email = 'test@example.com') { + return jwt.sign({ userId, email }, process.env.JWT_SECRET); +} + +describe('Auth Endpoints - Beneficiaries Data', () => { + let validToken; + + beforeEach(() => { + jest.clearAllMocks(); + validToken = generateToken(); + }); + + describe('GET /api/auth/me', () => { + it('should return beneficiaries with correct schema (name, not first_name/last_name)', async () => { + const mockUser = { + id: 1, + email: 'test@example.com', + first_name: 'John', + last_name: 'Doe', + phone: '+1234567890', + role: 'user', + avatar_url: null + }; + + const mockAccessRecords = [{ + beneficiary_id: 42, + role: 'custodian', + granted_at: '2025-01-01T00:00:00Z', + custom_name: null, + beneficiaries: { + id: 42, + name: 'Jane Smith', + phone: '+9876543210', + address: '123 Main St', + avatar_url: 'https://example.com/avatar.jpg', + equipment_status: 'active', + created_at: '2025-01-01T00:00:00Z' + } + }]; + + supabase.from.mockImplementation((table) => { + if (table === 'users') { + return { + select: jest.fn().mockReturnThis(), + eq: jest.fn().mockReturnThis(), + single: jest.fn().mockResolvedValue({ data: mockUser, error: null }) + }; + } + if (table === 'user_access') { + return { + select: jest.fn().mockReturnThis(), + eq: jest.fn().mockResolvedValue({ data: mockAccessRecords, error: null }) + }; + } + return { + select: jest.fn().mockReturnThis(), + eq: jest.fn().mockResolvedValue({ data: null, error: null }) + }; + }); + + const response = await request(app) + .get('/api/auth/me') + .set('Authorization', `Bearer ${validToken}`); + + expect(response.status).toBe(200); + expect(response.body.user).toMatchObject({ + id: 1, + email: 'test@example.com', + firstName: 'John', + lastName: 'Doe' + }); + + expect(response.body.beneficiaries).toHaveLength(1); + expect(response.body.beneficiaries[0]).toMatchObject({ + id: 42, + role: 'custodian', + name: 'Jane Smith', + displayName: 'Jane Smith', + originalName: 'Jane Smith', + customName: null, + phone: '+9876543210', + address: '123 Main St', + avatarUrl: 'https://example.com/avatar.jpg', + equipmentStatus: 'active', + hasDevices: true + }); + + // Verify it does NOT have old schema fields + expect(response.body.beneficiaries[0]).not.toHaveProperty('first_name'); + expect(response.body.beneficiaries[0]).not.toHaveProperty('last_name'); + expect(response.body.beneficiaries[0]).not.toHaveProperty('email'); + expect(response.body.beneficiaries[0]).not.toHaveProperty('address_street'); + }); + + it('should use customName as displayName when present', async () => { + const mockUser = { + id: 1, + email: 'test@example.com', + first_name: 'John', + last_name: 'Doe', + phone: null, + role: 'user', + avatar_url: null + }; + + const mockAccessRecords = [{ + beneficiary_id: 42, + role: 'guardian', + granted_at: '2025-01-01T00:00:00Z', + custom_name: 'Mom', + beneficiaries: { + id: 42, + name: 'Jane Smith', + phone: null, + address: null, + avatar_url: null, + equipment_status: 'demo', + created_at: '2025-01-01T00:00:00Z' + } + }]; + + supabase.from.mockImplementation((table) => { + if (table === 'users') { + return { + select: jest.fn().mockReturnThis(), + eq: jest.fn().mockReturnThis(), + single: jest.fn().mockResolvedValue({ data: mockUser, error: null }) + }; + } + if (table === 'user_access') { + return { + select: jest.fn().mockReturnThis(), + eq: jest.fn().mockResolvedValue({ data: mockAccessRecords, error: null }) + }; + } + return { + select: jest.fn().mockReturnThis(), + eq: jest.fn().mockResolvedValue({ data: null, error: null }) + }; + }); + + const response = await request(app) + .get('/api/auth/me') + .set('Authorization', `Bearer ${validToken}`); + + expect(response.status).toBe(200); + expect(response.body.beneficiaries[0]).toMatchObject({ + customName: 'Mom', + originalName: 'Jane Smith', + displayName: 'Mom', + equipmentStatus: 'demo', + hasDevices: true + }); + }); + + it('should correctly calculate hasDevices based on equipment_status', async () => { + const mockUser = { + id: 1, + email: 'test@example.com', + first_name: 'John', + last_name: 'Doe', + phone: null, + role: 'user', + avatar_url: null + }; + + const testCases = [ + { status: 'none', expectedHasDevices: false }, + { status: 'ordered', expectedHasDevices: false }, + { status: 'shipped', expectedHasDevices: false }, + { status: 'delivered', expectedHasDevices: false }, + { status: 'active', expectedHasDevices: true }, + { status: 'demo', expectedHasDevices: true } + ]; + + for (const testCase of testCases) { + const mockAccessRecords = [{ + beneficiary_id: 42, + role: 'custodian', + granted_at: '2025-01-01T00:00:00Z', + custom_name: null, + beneficiaries: { + id: 42, + name: 'Test Beneficiary', + phone: null, + address: null, + avatar_url: null, + equipment_status: testCase.status, + created_at: '2025-01-01T00:00:00Z' + } + }]; + + supabase.from.mockImplementation((table) => { + if (table === 'users') { + return { + select: jest.fn().mockReturnThis(), + eq: jest.fn().mockReturnThis(), + single: jest.fn().mockResolvedValue({ data: mockUser, error: null }) + }; + } + if (table === 'user_access') { + return { + select: jest.fn().mockReturnThis(), + eq: jest.fn().mockResolvedValue({ data: mockAccessRecords, error: null }) + }; + } + return { + select: jest.fn().mockReturnThis(), + eq: jest.fn().mockResolvedValue({ data: null, error: null }) + }; + }); + + const response = await request(app) + .get('/api/auth/me') + .set('Authorization', `Bearer ${validToken}`); + + expect(response.status).toBe(200); + expect(response.body.beneficiaries[0]).toMatchObject({ + equipmentStatus: testCase.status, + hasDevices: testCase.expectedHasDevices + }); + } + }); + + it('should return empty beneficiaries array when user has no access records', async () => { + const mockUser = { + id: 1, + email: 'test@example.com', + first_name: 'John', + last_name: 'Doe', + phone: null, + role: 'user', + avatar_url: null + }; + + supabase.from.mockImplementation((table) => { + if (table === 'users') { + return { + select: jest.fn().mockReturnThis(), + eq: jest.fn().mockReturnThis(), + single: jest.fn().mockResolvedValue({ data: mockUser, error: null }) + }; + } + if (table === 'user_access') { + return { + select: jest.fn().mockReturnThis(), + eq: jest.fn().mockResolvedValue({ data: [], error: null }) + }; + } + return { + select: jest.fn().mockReturnThis(), + eq: jest.fn().mockResolvedValue({ data: null, error: null }) + }; + }); + + const response = await request(app) + .get('/api/auth/me') + .set('Authorization', `Bearer ${validToken}`); + + expect(response.status).toBe(200); + expect(response.body.beneficiaries).toEqual([]); + }); + }); + + describe('POST /api/auth/verify-otp', () => { + it('should return beneficiaries with correct schema after OTP verification', async () => { + const mockOtpRecord = { + id: 1, + email: 'test@example.com', + code: '123456', + expires_at: new Date(Date.now() + 10 * 60 * 1000).toISOString(), + used_at: null + }; + + const mockUser = { + id: 1, + email: 'test@example.com', + first_name: 'John', + last_name: 'Doe', + phone: '+1234567890', + role: 'user', + avatar_url: null + }; + + const mockAccessRecords = [{ + beneficiary_id: 42, + role: 'custodian', + granted_at: '2025-01-01T00:00:00Z', + custom_name: null, + beneficiaries: { + id: 42, + name: 'Jane Smith', + phone: '+9876543210', + address: '123 Main St', + avatar_url: null, + equipment_status: 'active', + created_at: '2025-01-01T00:00:00Z' + } + }]; + + supabase.from.mockImplementation((table) => { + if (table === 'otp_codes') { + return { + select: jest.fn().mockReturnThis(), + eq: jest.fn().mockReturnThis(), + is: jest.fn().mockReturnThis(), + gt: jest.fn().mockReturnThis(), + single: jest.fn().mockResolvedValue({ data: mockOtpRecord, error: null }), + update: jest.fn().mockReturnThis() + }; + } + if (table === 'users') { + return { + select: jest.fn().mockReturnThis(), + eq: jest.fn().mockReturnThis(), + single: jest.fn().mockResolvedValue({ data: mockUser, error: null }), + update: jest.fn().mockReturnThis() + }; + } + if (table === 'user_access') { + return { + select: jest.fn().mockReturnThis(), + eq: jest.fn().mockResolvedValue({ data: mockAccessRecords, error: null }) + }; + } + return { + select: jest.fn().mockReturnThis(), + eq: jest.fn().mockResolvedValue({ data: null, error: null }) + }; + }); + + const response = await request(app) + .post('/api/auth/verify-otp') + .send({ email: 'test@example.com', code: '123456' }); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(true); + expect(response.body.token).toBeDefined(); + + expect(response.body.beneficiaries).toHaveLength(1); + expect(response.body.beneficiaries[0]).toMatchObject({ + id: 42, + role: 'custodian', + name: 'Jane Smith', + displayName: 'Jane Smith', + originalName: 'Jane Smith', + phone: '+9876543210', + address: '123 Main St', + equipmentStatus: 'active', + hasDevices: true + }); + + // Verify it does NOT have old schema fields + expect(response.body.beneficiaries[0]).not.toHaveProperty('first_name'); + expect(response.body.beneficiaries[0]).not.toHaveProperty('last_name'); + expect(response.body.beneficiaries[0]).not.toHaveProperty('email'); + }); + }); +}); diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js index 6ef59f8..474f748 100644 --- a/backend/src/routes/auth.js +++ b/backend/src/routes/auth.js @@ -226,15 +226,12 @@ router.post('/verify-otp', verifyOtpLimiter, async (req, res) => { custom_name, beneficiaries:beneficiary_id ( id, - email, - first_name, - last_name, + name, phone, - address_street, - address_city, - address_zip, - address_state, - address_country + address, + avatar_url, + equipment_status, + created_at ) `) .eq('accessor_id', user.id); @@ -242,7 +239,7 @@ router.post('/verify-otp', verifyOtpLimiter, async (req, res) => { // Π€ΠΎΡ€ΠΌΠ°Ρ‚ΠΈΡ€ΡƒΠ΅ΠΌ beneficiaries с displayName const beneficiaries = (accessRecords || []).map(record => { const customName = record.custom_name || null; - const originalName = record.beneficiaries?.first_name || null; + const originalName = record.beneficiaries?.name || null; const displayName = customName || originalName; return { @@ -252,7 +249,13 @@ router.post('/verify-otp', verifyOtpLimiter, async (req, res) => { customName: customName, displayName: displayName, originalName: originalName, - ...record.beneficiaries + name: record.beneficiaries?.name || null, + phone: record.beneficiaries?.phone || null, + address: record.beneficiaries?.address || null, + avatarUrl: record.beneficiaries?.avatar_url || null, + equipmentStatus: record.beneficiaries?.equipment_status || 'none', + hasDevices: record.beneficiaries?.equipment_status === 'active' || record.beneficiaries?.equipment_status === 'demo', + createdAt: record.beneficiaries?.created_at || null }; }); @@ -328,15 +331,12 @@ router.get('/me', async (req, res) => { custom_name, beneficiaries:beneficiary_id ( id, - email, - first_name, - last_name, + name, phone, - address_street, - address_city, - address_zip, - address_state, - address_country + address, + avatar_url, + equipment_status, + created_at ) `) .eq('accessor_id', user.id); @@ -344,7 +344,7 @@ router.get('/me', async (req, res) => { // Π€ΠΎΡ€ΠΌΠ°Ρ‚ΠΈΡ€ΡƒΠ΅ΠΌ beneficiaries с displayName const beneficiaries = (accessRecords || []).map(record => { const customName = record.custom_name || null; - const originalName = record.beneficiaries?.first_name || null; + const originalName = record.beneficiaries?.name || null; const displayName = customName || originalName; return { @@ -354,7 +354,13 @@ router.get('/me', async (req, res) => { customName: customName, displayName: displayName, originalName: originalName, - ...record.beneficiaries + name: record.beneficiaries?.name || null, + phone: record.beneficiaries?.phone || null, + address: record.beneficiaries?.address || null, + avatarUrl: record.beneficiaries?.avatar_url || null, + equipmentStatus: record.beneficiaries?.equipment_status || 'none', + hasDevices: record.beneficiaries?.equipment_status === 'active' || record.beneficiaries?.equipment_status === 'demo', + createdAt: record.beneficiaries?.created_at || null }; });