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:
Sergei 2026-01-29 11:47:23 -08:00
parent 7feca4d54b
commit 8456e85cfe
2 changed files with 421 additions and 20 deletions

View 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');
});
});
});

View File

@ -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
};
});