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