← back to Wine Finder Next

__tests__/integration/api-bottles.test.ts

260 lines

// Integration Tests for Bottles API
import { NextRequest } from 'next/server';
import { GET, POST } from '@/app/api/membership/bottles/route';

// Mock the dependencies
jest.mock('@/lib/membershipDatabase');
jest.mock('@/lib/rateLimit');
jest.mock('@/lib/auth');
jest.mock('@/lib/securityMonitoring');

import { bottleDb } from '@/lib/membershipDatabase';
import { checkRateLimit, getClientIdentifier } from '@/lib/rateLimit';
import { logAudit } from '@/lib/auth';
import { analyzeRequest } from '@/lib/securityMonitoring';

describe('Bottles API Integration Tests', () => {
  beforeEach(() => {
    jest.clearAllMocks();

    // Setup default mocks
    (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/bottles', () => {
    it('should return all bottles successfully', async () => {
      const mockBottles = [
        {
          id: 'bottle_1',
          name: 'Test Wine 1',
          vintage: 2010,
          producer: 'Test Producer',
          region: 'Test Region',
          size: '750ml',
          totalMembershipUnits: 1000,
          availableMembershipUnits: 500,
          estimatedValue: 50000,
          provenance: 'Test',
          status: 'active',
          createdAt: new Date(),
          updatedAt: new Date(),
        },
      ];

      (bottleDb.getAll as jest.Mock).mockReturnValue(mockBottles);

      const request = new NextRequest('http://localhost:3000/api/membership/bottles');
      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].name).toBe('Test Wine 1');
    });

    it('should handle rate limiting', async () => {
      (checkRateLimit as jest.Mock).mockReturnValue({
        success: false,
        limit: 100,
        remaining: 0,
        reset: Date.now() + 60000,
      });

      const request = new NextRequest('http://localhost:3000/api/membership/bottles');
      const response = await GET(request);
      const data = await response.json();

      expect(response.status).toBe(429);
      expect(data.success).toBe(false);
      expect(data.error).toContain('Rate limit exceeded');
    });

    it('should include rate limit headers', async () => {
      (bottleDb.getAll as jest.Mock).mockReturnValue([]);

      const request = new NextRequest('http://localhost:3000/api/membership/bottles');
      const response = await GET(request);

      expect(response.headers.get('X-RateLimit-Limit')).toBe('100');
      expect(response.headers.get('X-RateLimit-Remaining')).toBe('99');
      expect(response.headers.has('X-RateLimit-Reset')).toBe(true);
    });

    it('should log audit trail', async () => {
      (bottleDb.getAll as jest.Mock).mockReturnValue([]);

      const request = new NextRequest('http://localhost:3000/api/membership/bottles');
      await GET(request);

      expect(logAudit).toHaveBeenCalledWith({
        action: 'BOTTLES_LIST',
        resource: 'bottles',
        ipAddress: '127.0.0.1',
        userAgent: expect.any(String),
        success: true,
        metadata: { count: 0 },
      });
    });
  });

  describe('POST /api/membership/bottles', () => {
    it('should create a new bottle successfully', async () => {
      const newBottle = {
        id: 'bottle_new',
        name: 'New Test Wine',
        vintage: 2015,
        producer: 'New Producer',
        region: 'New Region',
        size: '750ml',
        totalMembershipUnits: 1000,
        availableMembershipUnits: 1000,
        estimatedValue: 75000,
        provenance: 'New provenance',
        status: 'active',
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      (bottleDb.create as jest.Mock).mockReturnValue(newBottle);

      const requestBody = {
        name: 'New Test Wine',
        vintage: 2015,
        producer: 'New Producer',
        region: 'New Region',
        size: '750ml',
        totalMembershipUnits: 1000,
        availableMembershipUnits: 1000,
        estimatedValue: 75000,
        provenance: 'New provenance',
        status: 'active',
      };

      const request = new NextRequest('http://localhost:3000/api/membership/bottles', {
        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.name).toBe('New Test Wine');
      expect(data.message).toBe('Bottle created successfully');
    });

    it('should detect security threats', async () => {
      (analyzeRequest as jest.Mock).mockReturnValue({
        safe: false,
        threats: ['XSS_DETECTED'],
      });

      const maliciousBody = {
        name: '<script>alert("XSS")</script>',
        vintage: 2015,
        producer: 'Test',
        region: 'Test',
        size: '750ml',
        totalMembershipUnits: 1000,
        availableMembershipUnits: 1000,
        estimatedValue: 75000,
        provenance: 'Test',
        status: 'active',
      };

      const request = new NextRequest('http://localhost:3000/api/membership/bottles', {
        method: 'POST',
        body: JSON.stringify(maliciousBody),
      });

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

    it('should sanitize input strings', async () => {
      const newBottle = {
        id: 'bottle_sanitized',
        name: 'Test Wine',
        vintage: 2015,
        producer: 'Test',
        region: 'Test',
        size: '750ml',
        totalMembershipUnits: 1000,
        availableMembershipUnits: 1000,
        estimatedValue: 75000,
        provenance: 'Test',
        status: 'active',
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      (bottleDb.create as jest.Mock).mockImplementation((data) => ({
        ...newBottle,
        name: data.name,
      }));

      const requestBody = {
        name: '<script>Test Wine</script>',
        vintage: 2015,
        producer: 'Test',
        region: 'Test',
        size: '750ml',
        totalMembershipUnits: 1000,
        availableMembershipUnits: 1000,
        estimatedValue: 75000,
        provenance: 'Test',
        status: 'active',
      };

      const request = new NextRequest('http://localhost:3000/api/membership/bottles', {
        method: 'POST',
        body: JSON.stringify(requestBody),
      });

      await POST(request);

      // Verify sanitization was applied
      expect(bottleDb.create).toHaveBeenCalledWith(
        expect.objectContaining({
          name: expect.not.stringContaining('<script>'),
        })
      );
    });

    it('should handle rate limiting on POST', async () => {
      (checkRateLimit as jest.Mock).mockReturnValue({
        success: false,
        limit: 100,
        remaining: 0,
        reset: Date.now() + 60000,
      });

      const request = new NextRequest('http://localhost:3000/api/membership/bottles', {
        method: 'POST',
        body: JSON.stringify({ name: 'Test' }),
      });

      const response = await POST(request);
      const data = await response.json();

      expect(response.status).toBe(429);
      expect(data.success).toBe(false);
    });
  });
});