Skills Development AppFolio CI Integration Testing Setup

AppFolio CI Integration Testing Setup

v20260423
appfolio-ci-integration
A comprehensive guide and implementation for configuring the CI/CD pipeline for AppFolio property management APIs. It utilizes a two-tier testing strategy: fast, mocked unit tests for quick feedback, and slower integration tests running against the AppFolio sandbox environment on main branch merges. This ensures robust validation of API interactions and prevents contract drift before production.
Get Skill
146 downloads
Overview

AppFolio CI Integration

Overview

Configure CI pipelines that validate AppFolio property management API integrations using a two-tier strategy. Unit tests mock the AppFolio REST client to verify tenant lookup, work order creation, and property listing logic without consuming API quota. Integration tests run against the AppFolio sandbox environment on main-branch merges only, using Basic Auth credentials stored as GitHub secrets. This keeps PR feedback fast and free while catching real API contract drift before production deploys.

GitHub Actions Workflow

# .github/workflows/appfolio-tests.yml
name: AppFolio API Tests
on: [push, pull_request]

jobs:
  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - run: npm ci
      - run: npm run lint && npm run typecheck
      - run: npm test -- --testPathPattern=unit  # No API credentials needed

  integration-tests:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    needs: unit-tests
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - run: npm ci
      - run: npm test -- --testPathPattern=integration
        env:
          APPFOLIO_CLIENT_ID: ${{ secrets.APPFOLIO_CLIENT_ID }}
          APPFOLIO_CLIENT_SECRET: ${{ secrets.APPFOLIO_CLIENT_SECRET }}
          APPFOLIO_BASE_URL: ${{ secrets.APPFOLIO_SANDBOX_URL }}

Mock-Based Unit Tests

// tests/unit/work-order-service.test.ts
import { describe, it, expect, vi } from 'vitest';
import { createWorkOrder } from '../../src/services/work-order-service';
import * as appfolioClient from '../../src/lib/appfolio-client';

vi.mock('../../src/lib/appfolio-client');

describe('WorkOrderService', () => {
  it('creates a maintenance work order for a property', async () => {
    vi.mocked(appfolioClient.post).mockResolvedValue({
      id: 'wo-4821',
      property_id: 'prop-100',
      category: 'Plumbing',
      status: 'Open',
    });

    const result = await createWorkOrder('prop-100', 'Plumbing', 'Leaking faucet unit 3B');
    expect(result.status).toBe('Open');
    expect(appfolioClient.post).toHaveBeenCalledWith('/work_orders', {
      property_id: 'prop-100',
      category: 'Plumbing',
      description: 'Leaking faucet unit 3B',
    });
  });
});

Integration Tests

// tests/integration/tenant-lookup.test.ts
import { describe, it, expect } from 'vitest';
import { AppFolioClient } from '../../src/lib/appfolio-client';

const canRun = process.env.APPFOLIO_CLIENT_ID && process.env.APPFOLIO_CLIENT_SECRET;

describe.skipIf(!canRun)('AppFolio Tenant Lookup (live sandbox)', () => {
  const client = new AppFolioClient({
    clientId: process.env.APPFOLIO_CLIENT_ID!,
    clientSecret: process.env.APPFOLIO_CLIENT_SECRET!,
    baseUrl: process.env.APPFOLIO_BASE_URL!,
  });

  it('lists tenants for a known property', async () => {
    const tenants = await client.get('/tenants', { property_id: 'prop-100' });
    expect(Array.isArray(tenants)).toBe(true);
    expect(tenants[0]).toHaveProperty('lease_status');
  });
});

CI Cost Management

// tests/helpers/api-budget.ts
let callCount = 0;
const MAX_CALLS_PER_RUN = 25; // AppFolio sandbox has 100 req/min rate limit

export function trackApiCall(): void {
  callCount++;
  if (callCount > MAX_CALLS_PER_RUN) {
    throw new Error(
      `CI API budget exceeded: ${callCount}/${MAX_CALLS_PER_RUN} calls. ` +
      'Reduce integration test scope or split across jobs.'
    );
  }
}

export function getCallCount(): number { return callCount; }

Error Handling

CI IssueCauseFix
401 Unauthorized in integration jobExpired or rotated sandbox credentialsRegenerate APPFOLIO_CLIENT_ID and APPFOLIO_CLIENT_SECRET in GitHub Secrets
429 Too Many RequestsSandbox rate limit (100 req/min) hit by parallel testsRun integration tests with --maxWorkers=1
Tenant list emptySandbox data periodically reset by AppFolioSeed test property via POST /properties in a beforeAll hook
Typecheck fails on API responseAppFolio schema updated without noticeRegenerate types from OpenAPI spec, update interfaces
Integration job skippedBranch protection rule not matching refs/heads/mainVerify workflow if condition matches your default branch name

Resources

Next Steps

See appfolio-deploy-integration.

Info
Category Development
Name appfolio-ci-integration
Version v20260423
Size 5.01KB
Updated At 2026-04-28
Language