QA for B2B Enterprise Software: Testing at Scale

QA for B2B Enterprise Software: Testing at Scale

Enterprise software quality assurance is a different discipline from testing a consumer SaaS. The stakes are different, the features are different, and the consequences of failure are different. When a consumer app has a bug, a user might churn. When enterprise software fails, it can disrupt the operations of an organization with thousands of employees, trigger SLA violations, and generate legal liability.

Enterprise buyers evaluate software differently too. They run security questionnaires, penetration tests, compliance audits, and proof-of-concept engagements before signing contracts. Your QA process must be rigorous enough that the answers to those questions are "yes, and we have test evidence to prove it."

This guide covers the enterprise-specific features that QA teams most often overlook: SSO, SCIM provisioning, audit logs, role-based access control, data import/export, admin portals, multi-region compliance, and performance under enterprise-scale load.

Testing SSO (Single Sign-On)

Enterprise customers rarely allow their employees to sign in with username and password. SAML 2.0 and OIDC (OpenID Connect) SSO is typically a hard requirement. Testing SSO means testing the integration, not just the UI.

SAML 2.0 Integration Tests

// saml.test.js
const { generateSAMLResponse } = require('./helpers/saml-test-helper');

describe('SAML SSO Integration', () => {
  const idpEntityId = 'https://test-idp.example.com';
  const spEntityId = 'https://yourapp.example.com';

  it('should redirect to IdP for authentication', async () => {
    const res = await request(app)
      .get('/auth/saml/login')
      .query({ org: 'enterprise-org-slug' });

    expect(res.status).toBe(302);
    expect(res.headers.location).toContain('test-idp.example.com');
    expect(res.headers.location).toContain('SAMLRequest');
  });

  it('should create user session on valid SAML response', async () => {
    const samlResponse = generateSAMLResponse({
      issuer: idpEntityId,
      nameId: 'jane.doe@enterprise.com',
      attributes: {
        email: 'jane.doe@enterprise.com',
        firstName: 'Jane',
        lastName: 'Doe',
        groups: ['engineering', 'admin'],
      },
    });

    const res = await request(app)
      .post('/auth/saml/callback')
      .send({ SAMLResponse: samlResponse, RelayState: 'some-relay-state' });

    expect(res.status).toBe(302);
    expect(res.headers.location).not.toContain('/login');

    const user = await db('users').where({ email: 'jane.doe@enterprise.com' }).first();
    expect(user).toBeDefined();
    expect(user.sso_provider).toBe('saml');
  });

  it('should reject tampered SAML assertions', async () => {
    const validResponse = generateSAMLResponse({
      issuer: idpEntityId,
      nameId: 'attacker@evil.com',
    });

    // Tamper with the assertion
    const tamperedResponse = validResponse.replace(
      'attacker@evil.com',
      'admin@enterprise.com'
    );

    const res = await request(app)
      .post('/auth/saml/callback')
      .send({ SAMLResponse: tamperedResponse });

    expect([400, 401]).toContain(res.status);
    expect(res.body.error).toMatch(/invalid|signature/i);
  });

  it('should enforce just-in-time provisioning for new SSO users', async () => {
    const newEmail = 'newuser@enterprise.com';
    const samlResponse = generateSAMLResponse({
      issuer: idpEntityId,
      nameId: newEmail,
      attributes: { email: newEmail, firstName: 'New', lastName: 'User' },
    });

    await request(app)
      .post('/auth/saml/callback')
      .send({ SAMLResponse: samlResponse });

    const user = await db('users').where({ email: newEmail }).first();
    expect(user).toBeDefined();
    expect(user.org_id).toBe(enterpriseOrgId);
  });
});

Testing SCIM Provisioning

SCIM (System for Cross-domain Identity Management) allows enterprise identity providers to automatically provision and deprovision users. This is non-negotiable for large enterprises with IT-managed access control.

describe('SCIM Provisioning', () => {
  const scimHeaders = {
    'Authorization': `Bearer ${scimProvisioningToken}`,
    'Content-Type': 'application/scim+json',
  };

  it('should provision a new user via SCIM', async () => {
    const scimUser = {
      schemas: ['urn:ietf:params:scim:schemas:core:2.0:User'],
      userName: 'scim.user@enterprise.com',
      name: { givenName: 'SCIM', familyName: 'User' },
      emails: [{ value: 'scim.user@enterprise.com', primary: true }],
      active: true,
    };

    const res = await request(app)
      .post('/scim/v2/Users')
      .set(scimHeaders)
      .send(scimUser);

    expect(res.status).toBe(201);
    expect(res.body.id).toBeDefined();
    expect(res.body.schemas).toContain('urn:ietf:params:scim:schemas:core:2.0:User');

    const user = await db('users').where({ email: 'scim.user@enterprise.com' }).first();
    expect(user).toBeDefined();
    expect(user.active).toBe(true);
  });

  it('should deprovision user when SCIM sets active=false', async () => {
    const scimUserId = await getScimUserId('existing@enterprise.com');

    const res = await request(app)
      .patch(`/scim/v2/Users/${scimUserId}`)
      .set(scimHeaders)
      .send({
        schemas: ['urn:ietf:params:scim:api:messages:2.0:PatchOp'],
        Operations: [{ op: 'replace', path: 'active', value: false }],
      });

    expect(res.status).toBe(200);

    const user = await db('users').where({ email: 'existing@enterprise.com' }).first();
    expect(user.active).toBe(false);

    // User should not be able to log in
    const loginRes = await request(app)
      .post('/auth/login')
      .send({ email: 'existing@enterprise.com', password: 'password123' });

    expect(loginRes.status).toBe(401);
  });

  it('should sync SCIM group memberships to application roles', async () => {
    const adminUserId = await getScimUserId('admin@enterprise.com');

    await request(app)
      .put(`/scim/v2/Groups/${adminGroupId}`)
      .set(scimHeaders)
      .send({
        schemas: ['urn:ietf:params:scim:schemas:core:2.0:Group'],
        displayName: 'Administrators',
        members: [{ value: adminUserId }],
      });

    const user = await db('users').where({ email: 'admin@enterprise.com' }).first();
    const roles = await db('user_roles').where({ user_id: user.id });
    expect(roles.map(r => r.role)).toContain('admin');
  });
});

Testing Audit Logs

Audit logs are a compliance requirement for enterprise customers in regulated industries. Every action that modifies data, accesses sensitive information, or changes permissions must be logged.

describe('Audit Logging', () => {
  it('should log user login events with IP and user agent', async () => {
    await request(app)
      .post('/auth/login')
      .set('X-Forwarded-For', '203.0.113.100')
      .set('User-Agent', 'TestBrowser/1.0')
      .send({ email: 'user@enterprise.com', password: 'correct-password' });

    const auditEntry = await db('audit_logs')
      .where({ actor_email: 'user@enterprise.com', event_type: 'user.login' })
      .orderBy('created_at', 'desc')
      .first();

    expect(auditEntry).toBeDefined();
    expect(auditEntry.ip_address).toBe('203.0.113.100');
    expect(auditEntry.user_agent).toContain('TestBrowser');
    expect(auditEntry.result).toBe('success');
  });

  it('should log failed login attempts', async () => {
    await request(app)
      .post('/auth/login')
      .send({ email: 'user@enterprise.com', password: 'wrong-password' });

    const auditEntry = await db('audit_logs')
      .where({ actor_email: 'user@enterprise.com', event_type: 'user.login_failed' })
      .orderBy('created_at', 'desc')
      .first();

    expect(auditEntry).toBeDefined();
    expect(auditEntry.result).toBe('failure');
  });

  it('should log all data access for sensitive resources', async () => {
    const userToken = await loginAsUser('analyst@enterprise.com');

    await request(app)
      .get(`/api/reports/${sensitiveReportId}`)
      .set('Authorization', `Bearer ${userToken}`);

    const auditEntry = await db('audit_logs')
      .where({ event_type: 'resource.accessed', resource_id: sensitiveReportId })
      .first();

    expect(auditEntry).toBeDefined();
    expect(auditEntry.actor_email).toBe('analyst@enterprise.com');
    expect(auditEntry.resource_type).toBe('report');
  });

  it('should not allow audit logs to be deleted or modified by any user', async () => {
    const adminToken = await loginAsUser('admin@enterprise.com');
    const auditEntryId = await db('audit_logs').select('id').first().then(r => r.id);

    const deleteRes = await request(app)
      .delete(`/api/audit-logs/${auditEntryId}`)
      .set('Authorization', `Bearer ${adminToken}`);

    expect([403, 404, 405]).toContain(deleteRes.status);

    const entry = await db('audit_logs').where({ id: auditEntryId }).first();
    expect(entry).toBeDefined();
  });

  it('should export audit logs in compliance-ready format', async () => {
    const adminToken = await loginAsUser('admin@enterprise.com');

    const res = await request(app)
      .get('/api/audit-logs/export')
      .set('Authorization', `Bearer ${adminToken}`)
      .query({ format: 'csv', from: '2024-01-01', to: '2024-12-31' });

    expect(res.status).toBe(200);
    expect(res.headers['content-type']).toMatch(/text\/csv/);
    expect(res.headers['content-disposition']).toMatch(/attachment/);
  });
});

Testing Role-Based Access Control (RBAC)

Enterprise software typically has multiple permission levels: super admin, org admin, manager, member, read-only. Every combination needs testing.

describe('Role-Based Access Control', () => {
  const roles = ['super_admin', 'org_admin', 'manager', 'member', 'viewer'];

  it('should enforce RBAC matrix for project management', async () => {
    const permissions = {
      create_project: ['super_admin', 'org_admin', 'manager'],
      delete_project: ['super_admin', 'org_admin'],
      edit_project: ['super_admin', 'org_admin', 'manager'],
      view_project: ['super_admin', 'org_admin', 'manager', 'member', 'viewer'],
      export_project: ['super_admin', 'org_admin', 'manager', 'member'],
    };

    for (const [permission, allowedRoles] of Object.entries(permissions)) {
      for (const role of roles) {
        const token = await loginWithRole(role);
        const { endpoint, method } = getEndpointForPermission(permission);

        const res = await request(app)[method](endpoint)
          .set('Authorization', `Bearer ${token}`);

        const expected = allowedRoles.includes(role) ? 200 : 403;
        expect(res.status).toBe(expected);
      }
    }
  });

  it('should prevent privilege escalation via API', async () => {
    const managerToken = await loginWithRole('manager');

    // Manager tries to promote themselves to admin
    const res = await request(app)
      .patch(`/api/users/${managerId}`)
      .set('Authorization', `Bearer ${managerToken}`)
      .send({ role: 'super_admin' });

    expect(res.status).toBe(403);

    const user = await db('users').where({ id: managerId }).first();
    expect(user.role).toBe('manager');
  });
});

Testing Data Import and Export

Enterprise data portability is a compliance and trust requirement. Test that import handles malformed data gracefully and export is complete and verifiable.

describe('Data Import/Export', () => {
  it('should import valid CSV and create records', async () => {
    const csvContent = `name,email,department
Alice Smith,alice@company.com,Engineering
Bob Jones,bob@company.com,Marketing`;

    const res = await request(app)
      .post('/api/users/import')
      .set('Authorization', `Bearer ${adminToken}`)
      .attach('file', Buffer.from(csvContent), { filename: 'users.csv', contentType: 'text/csv' });

    expect(res.status).toBe(200);
    expect(res.body.imported).toBe(2);
    expect(res.body.failed).toBe(0);
  });

  it('should report row-level errors without failing entire import', async () => {
    const csvWithErrors = `name,email,department
Valid User,valid@company.com,Engineering
Missing Email,,Marketing
Duplicate,alice@company.com,Sales`;

    const res = await request(app)
      .post('/api/users/import')
      .set('Authorization', `Bearer ${adminToken}`)
      .attach('file', Buffer.from(csvWithErrors), { filename: 'users.csv', contentType: 'text/csv' });

    expect(res.status).toBe(207); // Multi-status
    expect(res.body.imported).toBe(1);
    expect(res.body.failed).toBe(2);
    expect(res.body.errors[0].row).toBe(2);
    expect(res.body.errors[0].reason).toMatch(/email/i);
  });

  it('should export all org data and verify completeness', async () => {
    const projectCount = await db('projects').where({ org_id: orgId }).count();

    const res = await request(app)
      .get('/api/org/export')
      .set('Authorization', `Bearer ${adminToken}`)
      .query({ format: 'json' });

    expect(res.status).toBe(200);
    expect(res.body.projects.length).toBe(parseInt(projectCount[0].count));
    expect(res.body.exported_at).toBeDefined();
    expect(res.body.schema_version).toBeDefined();
  });
});

Performance Testing at Enterprise Scale

Enterprise customers bring volume. A feature that works fine for a 10-person team may crawl or fail for a 10,000-person organization.

// k6 enterprise scale test
import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  scenarios: {
    enterprise_load: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        { duration: '5m', target: 500 },   // Ramp to 500 concurrent users
        { duration: '15m', target: 500 },  // Hold at enterprise-scale load
        { duration: '5m', target: 0 },     // Wind down
      ],
    },
  },
  thresholds: {
    http_req_duration: ['p99<2000', 'p95<1000', 'p50<500'],
    http_req_failed: ['rate<0.01'],
  },
};

export default function () {
  // Simulate enterprise user browsing the admin portal
  const res = http.get('https://app.example.com/api/v1/users?limit=100', {
    headers: { Authorization: `Bearer ${enterpriseAdminToken}` },
  });

  check(res, {
    'status 200': r => r.status === 200,
    'returns 100 users': r => JSON.parse(r.body).data.length === 100,
    'response time ok': r => r.timings.duration < 1000,
  });

  sleep(1 + Math.random() * 2);
}

Multi-Region Compliance Testing

Enterprise customers in regulated industries need to ensure data stays within geographic boundaries. Test that your data residency controls work.

describe('Data Residency', () => {
  it('should route EU tenant data to EU region only', async () => {
    const euTenant = await createTenant({ region: 'eu-west-1' });
    const token = await loginAsTenant(euTenant.id);

    await request(app)
      .post('/api/projects')
      .set('Authorization', `Bearer ${token}`)
      .send({ name: 'EU Project' });

    // Verify the record was written to the EU database, not US
    const euDbProject = await euDatabase('projects')
      .where({ tenant_id: euTenant.id })
      .first();
    const usDbProject = await usDatabase('projects')
      .where({ tenant_id: euTenant.id })
      .first();

    expect(euDbProject).toBeDefined();
    expect(usDbProject).toBeUndefined();
  });
});

Checklist for Enterprise QA Readiness

Before declaring enterprise readiness, verify test coverage for these areas:

  • SSO (SAML 2.0 and OIDC) with tampered assertion rejection
  • SCIM provisioning and deprovisioning with group-to-role mapping
  • Audit log completeness, immutability, and export
  • RBAC matrix for every permission in your system
  • Privilege escalation prevention
  • Data import with row-level error reporting
  • Complete data export for portability
  • Performance tests at 10x expected enterprise user count
  • Data residency routing verification
  • Session timeout and forced logout from admin portal

Enterprise QA is not a phase before launch — it is an ongoing program. The security questionnaires and compliance audits your sales team faces are only passable if your QA process produces current, specific, verifiable evidence. Tests are that evidence.

Read more