← back to Wine Finder Next
__tests__/integration/api-votes.test.ts
283 lines
// Integration Tests for Governance Votes API
import { NextRequest } from 'next/server';
import { GET, POST } from '@/app/api/membership/votes/route';
jest.mock('@/lib/membershipDatabase');
jest.mock('@/lib/rateLimit');
jest.mock('@/lib/auth');
jest.mock('@/lib/securityMonitoring');
import { governanceVoteDb } from '@/lib/membershipDatabase';
import { checkRateLimit, getClientIdentifier } from '@/lib/rateLimit';
import { logAudit } from '@/lib/auth';
import { analyzeRequest } from '@/lib/securityMonitoring';
describe('Governance Votes API Integration Tests', () => {
beforeEach(() => {
jest.clearAllMocks();
(checkRateLimit as jest.Mock).mockReturnValue({
success: true,
limit: 100,
remaining: 99,
reset: Date.now() + 60000,
});
(getClientIdentifier as jest.Mock).mockReturnValue('127.0.0.1');
(logAudit as jest.Mock).mockReturnValue(undefined);
(analyzeRequest as jest.Mock).mockReturnValue({ safe: true, threats: [] });
});
describe('GET /api/membership/votes', () => {
it('should return all votes', async () => {
const mockVotes = [
{
id: 'vote_1',
bottleId: 'bottle_1',
proposal: 'Test Proposal',
proposalType: 'unlock',
description: 'Test description',
startDate: new Date(),
endDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
status: 'active',
votesFor: 10,
votesAgainst: 2,
votesAbstain: 1,
quorumRequired: 50,
approvalThreshold: 66,
votes: [],
createdBy: 'admin',
},
];
(governanceVoteDb.getAll as jest.Mock).mockReturnValue(mockVotes);
const request = new NextRequest('http://localhost:3000/api/membership/votes');
const response = await GET(request);
const data = await response.json();
expect(response.status).toBe(200);
expect(data.success).toBe(true);
expect(data.data).toHaveLength(1);
expect(data.data[0].proposal).toBe('Test Proposal');
});
it('should filter by active votes', async () => {
const mockActiveVotes = [
{
id: 'vote_active',
bottleId: 'bottle_1',
proposal: 'Active Proposal',
proposalType: 'unlock',
description: 'Active',
startDate: new Date(),
endDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
status: 'active',
votesFor: 5,
votesAgainst: 1,
votesAbstain: 0,
quorumRequired: 50,
approvalThreshold: 66,
votes: [],
createdBy: 'admin',
},
];
(governanceVoteDb.getActive as jest.Mock).mockReturnValue(mockActiveVotes);
const request = new NextRequest('http://localhost:3000/api/membership/votes?active=true');
const response = await GET(request);
const data = await response.json();
expect(governanceVoteDb.getActive).toHaveBeenCalled();
expect(data.success).toBe(true);
});
it('should filter by bottle ID', async () => {
const bottleId = 'bottle_123';
const mockBottleVotes = [
{
id: 'vote_bottle',
bottleId,
proposal: 'Bottle Proposal',
proposalType: 'unlock',
description: 'Test',
startDate: new Date(),
endDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
status: 'active',
votesFor: 0,
votesAgainst: 0,
votesAbstain: 0,
quorumRequired: 50,
approvalThreshold: 66,
votes: [],
createdBy: 'admin',
},
];
(governanceVoteDb.getByBottleId as jest.Mock).mockReturnValue(mockBottleVotes);
const request = new NextRequest(`http://localhost:3000/api/membership/votes?bottleId=${bottleId}`);
const response = await GET(request);
const data = await response.json();
expect(governanceVoteDb.getByBottleId).toHaveBeenCalledWith(bottleId);
expect(data.success).toBe(true);
});
it('should validate bottle ID format', async () => {
const invalidBottleId = 'invalid<script>';
const request = new NextRequest(`http://localhost:3000/api/membership/votes?bottleId=${invalidBottleId}`);
const response = await GET(request);
const data = await response.json();
expect(response.status).toBe(400);
expect(data.success).toBe(false);
expect(data.error).toContain('Invalid bottle ID format');
});
});
describe('POST /api/membership/votes', () => {
it('should create a new governance vote', async () => {
const newVote = {
id: 'vote_new',
bottleId: 'bottle_123',
proposal: 'New Proposal',
proposalType: 'unlock',
description: 'Test proposal description',
startDate: new Date(),
endDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
status: 'active',
votesFor: 0,
votesAgainst: 0,
votesAbstain: 0,
quorumRequired: 50,
approvalThreshold: 66,
votes: [],
createdBy: 'admin',
};
(governanceVoteDb.create as jest.Mock).mockReturnValue(newVote);
const requestBody = {
bottleId: 'bottle_123',
proposal: 'New Proposal',
proposalType: 'unlock',
description: 'Test proposal description',
startDate: new Date().toISOString(),
endDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
status: 'active',
quorumRequired: 50,
approvalThreshold: 66,
createdBy: 'admin',
};
const request = new NextRequest('http://localhost:3000/api/membership/votes', {
method: 'POST',
body: JSON.stringify(requestBody),
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(200);
expect(data.success).toBe(true);
expect(data.data.proposal).toBe('New Proposal');
expect(data.message).toBe('Vote created successfully');
});
it('should sanitize title and description', async () => {
const newVote = {
id: 'vote_sanitized',
bottleId: 'bottle_1',
proposal: 'Clean Title',
proposalType: 'unlock',
description: 'Clean Description',
startDate: new Date(),
endDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
status: 'active',
votesFor: 0,
votesAgainst: 0,
votesAbstain: 0,
quorumRequired: 50,
approvalThreshold: 66,
votes: [],
createdBy: 'admin',
};
(governanceVoteDb.create as jest.Mock).mockReturnValue(newVote);
const requestBody = {
bottleId: 'bottle_1',
title: '<script>alert("XSS")</script>Clean Title',
description: '<img src=x onerror=alert(1)>Clean Description',
proposalType: 'unlock',
startDate: new Date().toISOString(),
endDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
status: 'active',
quorumRequired: 50,
approvalThreshold: 66,
createdBy: 'admin',
};
const request = new NextRequest('http://localhost:3000/api/membership/votes', {
method: 'POST',
body: JSON.stringify(requestBody),
});
await POST(request);
// Verify sanitization was called
expect(governanceVoteDb.create).toHaveBeenCalled();
});
it('should validate bottle ID on POST', async () => {
const requestBody = {
bottleId: 'invalid_bottle_id',
proposal: 'Test',
proposalType: 'unlock',
description: 'Test',
};
const request = new NextRequest('http://localhost:3000/api/membership/votes', {
method: 'POST',
body: JSON.stringify(requestBody),
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(400);
expect(data.success).toBe(false);
expect(data.error).toContain('Invalid bottle ID format');
});
it('should detect security threats', async () => {
(analyzeRequest as jest.Mock).mockReturnValue({
safe: false,
threats: ['SQL_INJECTION'],
});
const requestBody = {
bottleId: "bottle_1'; DROP TABLE votes; --",
proposal: 'Test',
proposalType: 'unlock',
description: 'Test',
};
const request = new NextRequest('http://localhost:3000/api/membership/votes', {
method: 'POST',
body: JSON.stringify(requestBody),
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(403);
expect(data.success).toBe(false);
expect(data.error).toContain('Security threat detected');
});
});
});