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,
|
||||
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
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user