Testing Guide

Complete guide to testing SCORM API integrations.

Table of Contents

Overview

Testing ensures:

  • API integration works correctly
  • Error handling is robust
  • Performance meets requirements
  • Security is maintained

Testing Strategies

Test Pyramid

        /\
       /  \  E2E Tests (Few)
      /____\
     /      \  Integration Tests (Some)
    /________\
   /          \  Unit Tests (Many)
  /____________\

Test Types

  1. Unit Tests: Test individual functions/components
  2. Integration Tests: Test API interactions
  3. E2E Tests: Test complete workflows
  4. Performance Tests: Test under load
  5. Security Tests: Test security measures

Unit Testing

Testing API Client

// __tests__/scorm-client.test.ts
import { ScormAPIClient } from '../lib/scorm-client';

describe('ScormAPIClient', () => {
  let client: ScormAPIClient;
  
  beforeEach(() => {
    client = new ScormAPIClient(
      'test-api-key',
      'https://test-api.example.com',
      'test-tenant-id'
    );
  });
  
  test('uploadPackage creates FormData correctly', async () => {
    const file = new File(['test'], 'test.zip');
    const formData = client.createFormData(file, 'user-123');
    
    expect(formData.get('file')).toBe(file);
    expect(formData.get('tenant_id')).toBe('test-tenant-id');
    expect(formData.get('uploaded_by')).toBe('user-123');
  });
  
  test('handles version conflicts', async () => {
    // Mock fetch to return 409
    global.fetch = jest.fn()
      .mockResolvedValueOnce({
        status: 409,
        json: async () => ({ error: 'Version conflict' })
      })
      .mockResolvedValueOnce({
        status: 200,
        json: async () => ({ id: 'session-123', version: 2 })
      });
    
    const result = await client.updateSessionWithRetry(
      'session-123',
      { completion_status: 'completed' }
    );
    
    expect(result.version).toBe(2);
    expect(global.fetch).toHaveBeenCalledTimes(2);
  });
});

Testing Error Handling

describe('Error Handling', () => {
  test('handles network errors', async () => {
    global.fetch = jest.fn().mockRejectedValue(new Error('Network error'));
    
    await expect(
      client.uploadPackage(new File(['test'], 'test.zip'), 'user-123')
    ).rejects.toThrow('Network error');
  });
  
  test('handles rate limiting', async () => {
    global.fetch = jest.fn().mockResolvedValue({
      status: 429,
      headers: new Headers({ 'Retry-After': '60' })
    });
    
    // Should retry after delay
    const start = Date.now();
    await client.makeRequest('/api/v1/packages');
    const duration = Date.now() - start;
    
    expect(duration).toBeGreaterThan(50000); // Should wait ~60s
  });
});

Integration Testing

Testing Package Upload

Integration tests should follow the presigned flow against a real or staging tenant (https://app.allureconnect.com): POST /api/v1/packages/upload-urlPUTPOST /api/v1/packages/process. Do not assert on a multipart POST /api/v1/packages — that path is not the supported upload surface.

// __tests__/integration/package-upload.test.ts
describe('Package Upload Integration', () => {
  const base = process.env.TEST_API_BASE ?? 'https://app.allureconnect.com';
  const apiKey = process.env.TEST_API_KEY!;
  const tenantId = process.env.TEST_TENANT_ID!;
  const auth = { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' };

  test('mints upload URL and rejects invalid filename', async () => {
    const mint = await fetch(`${base}/api/v1/packages/upload-url`, {
      method: 'POST',
      headers: auth,
      body: JSON.stringify({
        tenant_id: tenantId,
        uploaded_by: 'test-user',
        filename: 'not-a-zip.txt',
        file_size: 12,
      }),
    });
    expect(mint.status).toBeGreaterThanOrEqual(400);
  });

  // Happy-path tests need a real ZIP bytes fixture and storage PUT permissions.
});

Testing Session Management

describe('Session Management Integration', () => {
  test('creates and tracks session', async () => {
    // 1. Create session
    const launchResponse = await fetch(
      `https://app.allureconnect.com/api/v1/packages/${packageId}/launch`,
      {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${apiKey}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          user_id: 'test-user',
          session_id: 'test-session'
        })
      }
    );
    
    const launch = await launchResponse.json();
    expect(launch.session_id).toBe('test-session');
    
    // 2. Get session
    const sessionResponse = await fetch(
      `https://app.allureconnect.com/api/v1/sessions/test-session`,
      {
        headers: { Authorization: `Bearer ${apiKey}` }
      }
    );
    
    const session = await sessionResponse.json();
    expect(session.id).toBe('test-session');
    expect(session.completion_status).toBe('not_attempted');
    
    // 3. Update session
    const updateResponse = await fetch(
      `https://app.allureconnect.com/api/v1/sessions/test-session`,
      {
        method: 'PUT',
        headers: {
          Authorization: `Bearer ${apiKey}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          version: session.version,
          completion_status: 'completed'
        })
      }
    );
    
    expect(updateResponse.status).toBe(200);
  });
});

End-to-End Testing

Complete Workflow Test

describe('Complete SCORM Workflow', () => {
  test('upload, launch, track, complete', async () => {
    const client = new ScormAPIClient(apiKey, baseUrl, tenantId);
    
    // 1. Upload package
    const package = await client.uploadPackage(testPackageFile, 'test-user');
    expect(package.id).toBeDefined();
    
    // 2. Wait for processing
    await waitForProcessing(package.id, 30000); // 30s timeout
    
    // 3. Launch session
    const launch = await client.launchSession(package.id, 'test-user');
    expect(launch.launch_url).toBeDefined();
    
    // 4. Track progress
    let session = await client.getSession(launch.session_id);
    expect(session.completion_status).toBe('not_attempted');
    
    // 5. Simulate progress
    await client.updateSession(launch.session_id, {
      version: session.version,
      completion_status: 'completed',
      score: { scaled: 0.85 }
    });
    
    // 6. Verify completion
    session = await client.getSession(launch.session_id);
    expect(session.completion_status).toBe('completed');
    expect(session.score.scaled).toBe(0.85);
  });
});

Testing with Real SCORM Package

describe('Real SCORM Package Test', () => {
  test('processes real SCORM 1.2 package', async () => {
    const packageFile = await fs.readFile('test-packages/scorm12-test.zip');
    
    const uploadResponse = await uploadPackage(packageFile);
    expect(uploadResponse.status).toBe(200);
    
    const package = await uploadResponse.json();
    
    // Wait for processing
    await pollUntilProcessed(package.id, {
      timeout: 60000,
      interval: 2000
    });
    
    // Verify package details
    const packageDetails = await getPackage(package.id);
    expect(packageDetails.version).toBe('1.2');
    expect(packageDetails.launch_url).toBeDefined();
  });
});

Test Data Management

Test Fixtures

// __tests__/fixtures/packages.ts
export const testPackages = {
  scorm12: {
    file: 'test-packages/scorm12-test.zip',
    expectedVersion: '1.2',
    expectedTitle: 'Test SCORM 1.2 Package'
  },
  scorm2004: {
    file: 'test-packages/scorm2004-test.zip',
    expectedVersion: '2004',
    expectedTitle: 'Test SCORM 2004 Package'
  }
};

Test Utilities

// __tests__/utils/test-helpers.ts
export async function createTestPackage() {
  const response = await uploadPackage(testPackages.scorm12.file);
  const pkg = await response.json();
  
  // Wait for processing
  await waitForProcessing(pkg.id);
  
  return pkg;
}

export async function createTestSession(packageId: string, userId: string) {
  const response = await launchSession(packageId, userId);
  return response.json();
}

export async function cleanupTestData(packageId: string) {
  // Delete test package
  await deletePackage(packageId);
}

Test Isolation

describe('Package Management', () => {
  let testPackageId: string;
  
  beforeEach(async () => {
    // Create test package for each test
    const pkg = await createTestPackage();
    testPackageId = pkg.id;
  });
  
  afterEach(async () => {
    // Cleanup after each test
    await cleanupTestData(testPackageId);
  });
  
  test('gets package details', async () => {
    const pkg = await getPackage(testPackageId);
    expect(pkg.id).toBe(testPackageId);
  });
});

Best Practices

1. Use Test Environment

// Use separate test environment
const baseUrl = process.env.TEST_API_URL || 'https://test-api.allurelms.com';
const apiKey = process.env.TEST_API_KEY;

2. Mock External Dependencies

// Mock fetch for unit tests
global.fetch = jest.fn().mockResolvedValue({
  status: 200,
  json: async () => ({ id: 'test-id' })
});

3. Test Error Scenarios

test('handles all error codes', async () => {
  const errorCodes = [400, 401, 403, 404, 409, 429, 500];
  
  for (const code of errorCodes) {
    global.fetch = jest.fn().mockResolvedValue({
      status: code,
      json: async () => ({ error: 'Test error', code: `ERROR_${code}` })
    });
    
    await expect(client.makeRequest('/test')).rejects.toThrow();
  }
});

4. Test Performance

test('meets performance requirements', async () => {
  const start = Date.now();
  await client.uploadPackage(testFile, 'user-123');
  const duration = Date.now() - start;
  
  expect(duration).toBeLessThan(5000); // Should complete in <5s
});

5. Test Security

test('prevents cross-tenant access', async () => {
  const otherTenantPackage = 'other-tenant-package-id';
  
  await expect(
    client.getPackage(otherTenantPackage)
  ).rejects.toThrow('Package not found');
});

Testing Checklist

  • Unit tests for API client
  • Integration tests for all endpoints
  • E2E tests for complete workflows
  • Error handling tests
  • Performance tests
  • Security tests
  • Test data cleanup
  • Test isolation
  • Mock external dependencies
  • Test with real SCORM packages

Related Documentation


Last Updated: 2025-01-15