Remove incorrect beneficiary schema from auth endpoints
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 <noreply@anthropic.com>
This commit is contained in:
parent
7feca4d54b
commit
8456e85cfe
395
backend/src/routes/__tests__/auth-beneficiaries.test.js
Normal file
395
backend/src/routes/__tests__/auth-beneficiaries.test.js
Normal file
@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -226,15 +226,12 @@ router.post('/verify-otp', verifyOtpLimiter, async (req, res) => {
|
|||||||
custom_name,
|
custom_name,
|
||||||
beneficiaries:beneficiary_id (
|
beneficiaries:beneficiary_id (
|
||||||
id,
|
id,
|
||||||
email,
|
name,
|
||||||
first_name,
|
|
||||||
last_name,
|
|
||||||
phone,
|
phone,
|
||||||
address_street,
|
address,
|
||||||
address_city,
|
avatar_url,
|
||||||
address_zip,
|
equipment_status,
|
||||||
address_state,
|
created_at
|
||||||
address_country
|
|
||||||
)
|
)
|
||||||
`)
|
`)
|
||||||
.eq('accessor_id', user.id);
|
.eq('accessor_id', user.id);
|
||||||
@ -242,7 +239,7 @@ router.post('/verify-otp', verifyOtpLimiter, async (req, res) => {
|
|||||||
// Форматируем beneficiaries с displayName
|
// Форматируем beneficiaries с displayName
|
||||||
const beneficiaries = (accessRecords || []).map(record => {
|
const beneficiaries = (accessRecords || []).map(record => {
|
||||||
const customName = record.custom_name || null;
|
const customName = record.custom_name || null;
|
||||||
const originalName = record.beneficiaries?.first_name || null;
|
const originalName = record.beneficiaries?.name || null;
|
||||||
const displayName = customName || originalName;
|
const displayName = customName || originalName;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -252,7 +249,13 @@ router.post('/verify-otp', verifyOtpLimiter, async (req, res) => {
|
|||||||
customName: customName,
|
customName: customName,
|
||||||
displayName: displayName,
|
displayName: displayName,
|
||||||
originalName: originalName,
|
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,
|
custom_name,
|
||||||
beneficiaries:beneficiary_id (
|
beneficiaries:beneficiary_id (
|
||||||
id,
|
id,
|
||||||
email,
|
name,
|
||||||
first_name,
|
|
||||||
last_name,
|
|
||||||
phone,
|
phone,
|
||||||
address_street,
|
address,
|
||||||
address_city,
|
avatar_url,
|
||||||
address_zip,
|
equipment_status,
|
||||||
address_state,
|
created_at
|
||||||
address_country
|
|
||||||
)
|
)
|
||||||
`)
|
`)
|
||||||
.eq('accessor_id', user.id);
|
.eq('accessor_id', user.id);
|
||||||
@ -344,7 +344,7 @@ router.get('/me', async (req, res) => {
|
|||||||
// Форматируем beneficiaries с displayName
|
// Форматируем beneficiaries с displayName
|
||||||
const beneficiaries = (accessRecords || []).map(record => {
|
const beneficiaries = (accessRecords || []).map(record => {
|
||||||
const customName = record.custom_name || null;
|
const customName = record.custom_name || null;
|
||||||
const originalName = record.beneficiaries?.first_name || null;
|
const originalName = record.beneficiaries?.name || null;
|
||||||
const displayName = customName || originalName;
|
const displayName = customName || originalName;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -354,7 +354,13 @@ router.get('/me', async (req, res) => {
|
|||||||
customName: customName,
|
customName: customName,
|
||||||
displayName: displayName,
|
displayName: displayName,
|
||||||
originalName: originalName,
|
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
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user