Testing

This guide covers how to test applications that use the Asterisms JS SDK, including unit testing, integration testing, and end-to-end testing.

Testing Overview

Testing SDK-integrated applications involves several considerations:

  • Mocking SDK dependencies for unit tests
  • Testing authentication flows without real credentials
  • Simulating network conditions and error scenarios
  • Testing real-time events and state changes
  • Integration testing with actual SDK services
  • End-to-end testing of complete user workflows

Unit Testing

Setting Up Test Environment

// tests/setup.ts
import { vi } from 'vitest';

// Mock the SDK
vi.mock('@asterisms/sdk', () => ({
  createAsterismsSDK: vi.fn(() => ({
    auth: {
      login: vi.fn(),
      logout: vi.fn(),
      isAuthenticated: vi.fn(),
      getCurrentUser: vi.fn(),
      onAuthStateChanged: vi.fn(),
      registerProvider: vi.fn()
    },
    storage: {
      upload: vi.fn(),
      uploadWithProgress: vi.fn(),
      list: vi.fn(),
      delete: vi.fn()
    },
    drive: {
      createFolder: vi.fn(),
      listFolder: vi.fn(),
      share: vi.fn(),
      move: vi.fn()
    },
    notification: {
      getNotifications: vi.fn(),
      onNotificationReceived: vi.fn(),
      markAsRead: vi.fn()
    }
  }))
}));

Testing Authentication

// tests/auth.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { createAsterismsSDK } from '@asterisms/sdk';
import { AuthService } from '../src/services/AuthService';

// Mock the SDK
const mockSDK = {
  auth: {
    login: vi.fn(),
    logout: vi.fn(),
    isAuthenticated: vi.fn(),
    getCurrentUser: vi.fn(),
    onAuthStateChanged: vi.fn()
  }
};

vi.mocked(createAsterismsSDK).mockReturnValue(mockSDK as any);

describe('AuthService', () => {
  let authService: AuthService;

  beforeEach(() => {
    vi.clearAllMocks();
    authService = new AuthService();
  });

  it('should login successfully', async () => {
    const credentials = { username: 'test@example.com', password: 'password' };
    const expectedUser = { id: '1', email: 'test@example.com' };

    mockSDK.auth.login.mockResolvedValue({ user: expectedUser });

    const result = await authService.login(credentials);

    expect(mockSDK.auth.login).toHaveBeenCalledWith({
      provider: 'srp6',
      credentials
    });
    expect(result.user).toEqual(expectedUser);
  });

  it('should handle login errors', async () => {
    const credentials = { username: 'test@example.com', password: 'wrong' };
    const error = new Error('Invalid credentials');

    mockSDK.auth.login.mockRejectedValue(error);

    await expect(authService.login(credentials)).rejects.toThrow('Invalid credentials');
  });

  it('should check authentication status', () => {
    mockSDK.auth.isAuthenticated.mockReturnValue(true);

    const isAuthenticated = authService.isAuthenticated();

    expect(mockSDK.auth.isAuthenticated).toHaveBeenCalled();
    expect(isAuthenticated).toBe(true);
  });

  it('should handle auth state changes', () => {
    const callback = vi.fn();
    const unsubscribe = vi.fn();

    mockSDK.auth.onAuthStateChanged.mockReturnValue(unsubscribe);

    const result = authService.onAuthStateChanged(callback);

    expect(mockSDK.auth.onAuthStateChanged).toHaveBeenCalledWith(callback);
    expect(result).toBe(unsubscribe);
  });
});

Testing File Upload

// tests/upload.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { UploadService } from '../src/services/UploadService';

describe('UploadService', () => {
  let uploadService: UploadService;
  let mockSDK: any;

  beforeEach(() => {
    mockSDK = {
      storage: {
        upload: vi.fn(),
        uploadWithProgress: vi.fn()
      }
    };
    uploadService = new UploadService(mockSDK);
  });

  it('should upload file successfully', async () => {
    const file = new File(['content'], 'test.txt', { type: 'text/plain' });
    const expectedResult = { fileId: '123', url: 'https://example.com/file.txt' };

    mockSDK.storage.upload.mockResolvedValue(expectedResult);

    const result = await uploadService.uploadFile(file);

    expect(mockSDK.storage.upload).toHaveBeenCalledWith(file, {
      folder: '/uploads',
      metadata: expect.any(Object)
    });
    expect(result).toEqual(expectedResult);
  });

  it('should handle upload progress', async () => {
    const file = new File(['content'], 'test.txt');
    const progressCallback = vi.fn();

    const mockUpload = {
      onProgress: vi.fn(),
      promise: Promise.resolve({ fileId: '123' })
    };

    mockSDK.storage.uploadWithProgress.mockReturnValue(mockUpload);

    await uploadService.uploadFileWithProgress(file, progressCallback);

    expect(mockUpload.onProgress).toHaveBeenCalledWith(progressCallback);
  });

  it('should validate file size', () => {
    const oversizedFile = new File(['x'.repeat(101 * 1024 * 1024)], 'large.txt');

    expect(() => uploadService.validateFile(oversizedFile)).toThrow(
      'File size exceeds maximum allowed size'
    );
  });

  it('should validate file type', () => {
    const executableFile = new File(['content'], 'virus.exe', {
      type: 'application/x-executable'
    });

    expect(() => uploadService.validateFile(executableFile)).toThrow('File type not allowed');
  });
});

Testing React Components

// tests/components/LoginForm.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { LoginForm } from '../src/components/LoginForm';
import { AuthProvider } from '../src/components/AuthProvider';

// Mock auth hook
const mockAuth = {
  isAuthenticated: false,
  user: null,
  loading: false
};

vi.mock('../src/components/AuthProvider', () => ({
  useAuth: () => mockAuth
}));

// Mock SDK
const mockLogin = vi.fn();
vi.mock('../src/lib/sdk', () => ({
  sdk: {
    auth: {
      login: mockLogin
    }
  }
}));

describe('LoginForm', () => {
  it('should render login form', () => {
    render(<LoginForm />);

    expect(screen.getByLabelText(/username/i)).toBeInTheDocument();
    expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
    expect(screen.getByRole('button', { name: /login/i })).toBeInTheDocument();
  });

  it('should handle form submission', async () => {
    mockLogin.mockResolvedValue({ user: { id: '1' } });

    render(<LoginForm />);

    fireEvent.change(screen.getByLabelText(/username/i), {
      target: { value: 'test@example.com' }
    });
    fireEvent.change(screen.getByLabelText(/password/i), {
      target: { value: 'password' }
    });

    fireEvent.click(screen.getByRole('button', { name: /login/i }));

    await waitFor(() => {
      expect(mockLogin).toHaveBeenCalledWith({
        provider: 'srp6',
        credentials: {
          username: 'test@example.com',
          password: 'password'
        }
      });
    });
  });

  it('should display error message on login failure', async () => {
    mockLogin.mockRejectedValue(new Error('Invalid credentials'));

    render(<LoginForm />);

    fireEvent.change(screen.getByLabelText(/username/i), {
      target: { value: 'test@example.com' }
    });
    fireEvent.change(screen.getByLabelText(/password/i), {
      target: { value: 'wrong' }
    });

    fireEvent.click(screen.getByRole('button', { name: /login/i }));

    await waitFor(() => {
      expect(screen.getByText(/invalid credentials/i)).toBeInTheDocument();
    });
  });

  it('should show loading state during login', async () => {
    let resolveLogin: (value: any) => void;
    const loginPromise = new Promise(resolve => {
      resolveLogin = resolve;
    });
    mockLogin.mockReturnValue(loginPromise);

    render(<LoginForm />);

    fireEvent.click(screen.getByRole('button', { name: /login/i }));

    expect(screen.getByText(/logging in/i)).toBeInTheDocument();
    expect(screen.getByRole('button')).toBeDisabled();

    resolveLogin!({ user: { id: '1' } });
  });
});

Integration Testing

Testing with Real SDK

// tests/integration/auth.integration.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { createAsterismsSDK } from '@asterisms/sdk';
import { SRP6AuthProvider } from '@asterisms/auth-provider-srp6';

describe('Authentication Integration', () => {
  let sdk: any;

  beforeAll(async () => {
    // Use test environment configuration
    sdk = createAsterismsSDK({
      bundleId: 'com.test.app',
      rootDomain: 'test.example.com',
      navigationAdapter: {
        navigate: () => {},
        getCurrentPath: () => '/'
      },
      environment: 'test'
    });

    // Register test auth provider
    sdk.auth.registerProvider(
      'srp6',
      new SRP6AuthProvider({
        serviceUrl: 'https://auth.test.example.com'
      })
    );
  });

  afterAll(async () => {
    // Clean up
    if (sdk.auth.isAuthenticated()) {
      await sdk.auth.logout();
    }
  });

  it('should authenticate with test credentials', async () => {
    const result = await sdk.auth.login({
      provider: 'srp6',
      credentials: {
        username: 'test@example.com',
        password: 'testpassword'
      }
    });

    expect(result.user).toBeDefined();
    expect(result.user.email).toBe('test@example.com');
    expect(sdk.auth.isAuthenticated()).toBe(true);
  });

  it('should handle token refresh', async () => {
    // Assuming we're authenticated from previous test
    const initialToken = sdk.auth.getToken();

    // Force token refresh
    await sdk.auth.refreshToken();

    const newToken = sdk.auth.getToken();
    expect(newToken).not.toBe(initialToken);
    expect(sdk.auth.isAuthenticated()).toBe(true);
  });

  it('should logout successfully', async () => {
    await sdk.auth.logout();

    expect(sdk.auth.isAuthenticated()).toBe(false);
    expect(sdk.auth.getCurrentUser()).toBeNull();
  });
});

Testing File Operations

// tests/integration/storage.integration.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { createTestFile } from '../helpers/test-utils';

describe('Storage Integration', () => {
  let sdk: any;

  beforeAll(async () => {
    // Initialize SDK and authenticate
    sdk = createTestSDK();
    await authenticateTestUser(sdk);
  });

  afterAll(async () => {
    await sdk.auth.logout();
  });

  it('should upload and retrieve file', async () => {
    const testFile = createTestFile('test.txt', 'Hello, World!');

    // Upload file
    const uploadResult = await sdk.storage.upload(testFile, {
      folder: '/test-uploads'
    });

    expect(uploadResult.fileId).toBeDefined();
    expect(uploadResult.url).toBeDefined();

    // Verify file exists
    const files = await sdk.storage.list({ folder: '/test-uploads' });
    const uploadedFile = files.find((f) => f.id === uploadResult.fileId);

    expect(uploadedFile).toBeDefined();
    expect(uploadedFile.name).toBe('test.txt');

    // Clean up
    await sdk.storage.delete([uploadResult.fileId]);
  });

  it('should handle upload progress', async () => {
    const testFile = createTestFile('large-test.txt', 'x'.repeat(1024 * 1024));
    const progressEvents: any[] = [];

    const upload = sdk.storage.uploadWithProgress(testFile);

    upload.onProgress((progress: any) => {
      progressEvents.push(progress);
    });

    const result = await upload.promise;

    expect(progressEvents.length).toBeGreaterThan(0);
    expect(progressEvents[progressEvents.length - 1].percentage).toBe(100);
    expect(result.fileId).toBeDefined();

    // Clean up
    await sdk.storage.delete([result.fileId]);
  });
});

Mocking SDK Events

Event Subscription Testing

// tests/events.test.ts
import { describe, it, expect, vi } from 'vitest';
import { EventEmitter } from 'events';

// Create mock event emitter for testing
class MockSDKEventEmitter extends EventEmitter {
  onAuthStateChanged(callback: Function) {
    this.on('authStateChanged', callback);
    return () => this.off('authStateChanged', callback);
  }

  onUploadProgress(callback: Function) {
    this.on('uploadProgress', callback);
    return () => this.off('uploadProgress', callback);
  }

  simulateAuthStateChange(state: any) {
    this.emit('authStateChanged', state);
  }

  simulateUploadProgress(progress: any) {
    this.emit('uploadProgress', progress);
  }
}

describe('SDK Events', () => {
  it('should handle auth state changes', () => {
    const mockSDK = new MockSDKEventEmitter();
    const callback = vi.fn();

    const unsubscribe = mockSDK.onAuthStateChanged(callback);

    // Simulate auth state change
    mockSDK.simulateAuthStateChange({
      isAuthenticated: true,
      user: { id: '1', email: 'test@example.com' }
    });

    expect(callback).toHaveBeenCalledWith({
      isAuthenticated: true,
      user: { id: '1', email: 'test@example.com' }
    });

    // Test unsubscribe
    unsubscribe();
    mockSDK.simulateAuthStateChange({ isAuthenticated: false });

    expect(callback).toHaveBeenCalledTimes(1);
  });

  it('should handle upload progress events', () => {
    const mockSDK = new MockSDKEventEmitter();
    const progressCallback = vi.fn();

    mockSDK.onUploadProgress(progressCallback);

    // Simulate progress events
    mockSDK.simulateUploadProgress({ fileId: '123', percentage: 50 });
    mockSDK.simulateUploadProgress({ fileId: '123', percentage: 100 });

    expect(progressCallback).toHaveBeenCalledTimes(2);
    expect(progressCallback).toHaveBeenCalledWith({ fileId: '123', percentage: 50 });
    expect(progressCallback).toHaveBeenCalledWith({ fileId: '123', percentage: 100 });
  });
});

End-to-End Testing

Playwright E2E Tests

// tests/e2e/auth-flow.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Authentication Flow', () => {
  test('should login and logout successfully', async ({ page }) => {
    await page.goto('/login');

    // Fill login form
    await page.fill('[data-testid="username"]', 'test@example.com');
    await page.fill('[data-testid="password"]', 'testpassword');

    // Submit form
    await page.click('[data-testid="login-button"]');

    // Wait for redirect to dashboard
    await page.waitForURL('/dashboard');

    // Verify user is logged in
    await expect(page.locator('[data-testid="user-menu"]')).toBeVisible();
    await expect(page.locator('[data-testid="user-name"]')).toContainText('test@example.com');

    // Logout
    await page.click('[data-testid="user-menu"]');
    await page.click('[data-testid="logout-button"]');

    // Verify redirect to login
    await page.waitForURL('/login');
    await expect(page.locator('[data-testid="login-form"]')).toBeVisible();
  });

  test('should handle login errors', async ({ page }) => {
    await page.goto('/login');

    // Fill with invalid credentials
    await page.fill('[data-testid="username"]', 'invalid@example.com');
    await page.fill('[data-testid="password"]', 'wrongpassword');

    await page.click('[data-testid="login-button"]');

    // Verify error message
    await expect(page.locator('[data-testid="error-message"]')).toBeVisible();
    await expect(page.locator('[data-testid="error-message"]')).toContainText(
      'Invalid credentials'
    );
  });
});

File Upload E2E Test

// tests/e2e/file-upload.spec.ts
import { test, expect } from '@playwright/test';
import path from 'path';

test.describe('File Upload', () => {
  test.beforeEach(async ({ page }) => {
    // Login before each test
    await page.goto('/login');
    await page.fill('[data-testid="username"]', 'test@example.com');
    await page.fill('[data-testid="password"]', 'testpassword');
    await page.click('[data-testid="login-button"]');
    await page.waitForURL('/dashboard');
  });

  test('should upload file successfully', async ({ page }) => {
    await page.goto('/upload');

    // Create test file
    const testFilePath = path.join(__dirname, 'fixtures', 'test-file.txt');

    // Upload file
    await page.setInputFiles('[data-testid="file-input"]', testFilePath);
    await page.click('[data-testid="upload-button"]');

    // Wait for upload to complete
    await expect(page.locator('[data-testid="upload-progress"]')).toContainText('100%');
    await expect(page.locator('[data-testid="upload-status"]')).toContainText('Upload completed');

    // Verify file appears in file list
    await page.goto('/files');
    await expect(page.locator('[data-testid="file-list"]')).toContainText('test-file.txt');
  });

  test('should show upload progress', async ({ page }) => {
    await page.goto('/upload');

    // Create larger test file for visible progress
    const largeFilePath = path.join(__dirname, 'fixtures', 'large-file.zip');

    await page.setInputFiles('[data-testid="file-input"]', largeFilePath);
    await page.click('[data-testid="upload-button"]');

    // Verify progress is shown
    await expect(page.locator('[data-testid="upload-progress"]')).toBeVisible();

    // Wait for progress to change
    await page.waitForFunction(() => {
      const progressElement = document.querySelector('[data-testid="upload-progress"]');
      return progressElement && parseInt(progressElement.textContent || '0') > 0;
    });

    // Wait for completion
    await expect(page.locator('[data-testid="upload-progress"]')).toContainText('100%');
  });
});

Test Utilities

SDK Test Factory

// tests/helpers/sdk-factory.ts
import { createAsterismsSDK } from '@asterisms/sdk';
import { SRP6AuthProvider } from '@asterisms/auth-provider-srp6';

export function createTestSDK(overrides = {}) {
  return createAsterismsSDK({
    bundleId: 'com.test.app',
    rootDomain: 'test.example.com',
    navigationAdapter: {
      navigate: vi.fn(),
      getCurrentPath: () => '/'
    },
    environment: 'test',
    ...overrides
  });
}

export async function authenticateTestUser(sdk: any) {
  const authProvider = new SRP6AuthProvider({
    serviceUrl: 'https://auth.test.example.com'
  });

  sdk.auth.registerProvider('srp6', authProvider);

  return await sdk.auth.login({
    provider: 'srp6',
    credentials: {
      username: 'test@example.com',
      password: 'testpassword'
    }
  });
}

File Test Utilities

// tests/helpers/file-utils.ts
export function createTestFile(name: string, content: string, type = 'text/plain'): File {
  const blob = new Blob([content], { type });
  return new File([blob], name, { type });
}

export function createTestFiles(count: number): File[] {
  return Array.from({ length: count }, (_, i) =>
    createTestFile(`test-file-${i}.txt`, `Content ${i}`)
  );
}

export function createLargeTestFile(sizeInMB: number): File {
  const content = 'x'.repeat(sizeInMB * 1024 * 1024);
  return createTestFile('large-file.txt', content);
}

Event Testing Utilities

// tests/helpers/event-utils.ts
export function createEventTester() {
  const events: any[] = [];
  const subscriptions: Function[] = [];

  const on = (eventName: string, callback: Function) => {
    subscriptions.push(callback);
    return () => {
      const index = subscriptions.indexOf(callback);
      if (index > -1) {
        subscriptions.splice(index, 1);
      }
    };
  };

  const emit = (eventName: string, data: any) => {
    events.push({ eventName, data, timestamp: new Date() });
    subscriptions.forEach((callback) => callback(data));
  };

  const getEvents = () => events;
  const clearEvents = () => (events.length = 0);

  return { on, emit, getEvents, clearEvents };
}

Test Configuration

Vitest Configuration

// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    environment: 'jsdom',
    setupFiles: ['./tests/setup.ts'],
    globals: true,
    coverage: {
      reporter: ['text', 'json', 'html'],
      exclude: ['node_modules/', 'tests/', 'dist/', '*.config.*']
    }
  },
  resolve: {
    alias: {
      '@': './src'
    }
  }
});

Jest Configuration

{
  "jest": {
    "testEnvironment": "jsdom",
    "setupFilesAfterEnv": ["<rootDir>/tests/setup.ts"],
    "moduleNameMapping": {
      "^@/(.*)$": "<rootDir>/src/$1"
    },
    "coverageDirectory": "coverage",
    "collectCoverageFrom": ["src/**/*.{ts,tsx}", "!src/**/*.d.ts", "!src/**/*.test.{ts,tsx}"]
  }
}

Test Best Practices

  1. Mock External Dependencies: Always mock the SDK in unit tests
  2. Test Error Scenarios: Test both success and failure cases
  3. Use Test Data: Create consistent test data and fixtures
  4. Clean Up: Always clean up resources after tests
  5. Isolate Tests: Each test should be independent
  6. Test Events: Verify event subscriptions and unsubscriptions
  7. Performance Testing: Test with large files and datasets
  8. Security Testing: Test authentication and authorization flows

Next Steps