Provider verification workflow for contract testing
Implements provider-side verification of consumer contracts using Pact. Triggers when working on provider verification tasks, planning verification approaches, or following contract testing best practices.
/plugin marketplace add melodic-software/claude-code-plugins/plugin install contract-testing@melodic-softwareThis skill is limited to using the following tools:
Use this skill when:
Implement provider-side verification of consumer contracts using Pact.
Before implementing provider verification:
docs-management skill for verification patternsPROVIDER VERIFICATION PROCESS:
┌────────────────────────────────────────────────────────────────┐
│ PROVIDER VERIFICATION │
├────────────────────────────────────────────────────────────────┤
│ │
│ 1. FETCH CONTRACTS │
│ ┌──────────────────────────────────────────────┐ │
│ │ Get contracts from Pact Broker │ │
│ │ - For specific consumers │ │
│ │ - For specific branches/environments │ │
│ │ - Using consumer version selectors │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ 2. SETUP PROVIDER STATES │
│ ┌──────────────────────────────────────────────┐ │
│ │ For each interaction: │ │
│ │ - Parse provider state from contract │ │
│ │ - Execute state handler │ │
│ │ - Seed required data │ │
│ │ - Configure dependencies │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ 3. REPLAY INTERACTIONS │
│ ┌──────────────────────────────────────────────┐ │
│ │ For each interaction: │ │
│ │ - Send request to real provider │ │
│ │ - Capture actual response │ │
│ │ - Compare against expected │ │
│ │ - Apply matching rules │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ 4. PUBLISH RESULTS │
│ ┌──────────────────────────────────────────────┐ │
│ │ Report verification results to Pact Broker │ │
│ │ - Tag with provider version │ │
│ │ - Include branch/environment info │ │
│ │ - Enable can-i-deploy checks │ │
│ └──────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────┘
// Provider verification test
// File: OrdersApi.Provider.Tests/OrdersApiProviderTests.cs
using PactNet;
using PactNet.Verifier;
using Microsoft.AspNetCore.Mvc.Testing;
public class OrdersApiProviderTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
private readonly ITestOutputHelper _output;
public OrdersApiProviderTests(
WebApplicationFactory<Program> factory,
ITestOutputHelper output)
{
_factory = factory;
_output = output;
}
[Fact]
public void VerifyPacts_WithAllConsumers()
{
// Arrange
var config = new PactVerifierConfig
{
LogLevel = PactLogLevel.Information,
Outputters = new[] { new XUnitOutput(_output) }
};
// Start the provider API
using var server = _factory.CreateDefaultClient();
var providerUri = _factory.Server.BaseAddress;
// Act & Assert
using var verifier = new PactVerifier("OrdersApi", config);
verifier
.WithHttpEndpoint(providerUri)
.WithProviderStateUrl(new Uri(providerUri, "/provider-states"))
.WithPactBrokerSource(new Uri("https://pact-broker.example.com"), options =>
{
options.TokenAuthentication("your-token");
options.ConsumerVersionSelectors(
new ConsumerVersionSelector
{
MainBranch = true
},
new ConsumerVersionSelector
{
DeployedOrReleased = true
}
);
options.PublishVerificationResults(
providerVersion: Environment.GetEnvironmentVariable("GIT_SHA") ?? "local",
providerBranch: Environment.GetEnvironmentVariable("GIT_BRANCH"));
})
.Verify();
}
}
// Provider state endpoint
// File: OrdersApi/Controllers/ProviderStatesController.cs
[ApiController]
[Route("provider-states")]
public class ProviderStatesController : ControllerBase
{
private readonly OrdersDbContext _context;
private readonly IServiceProvider _serviceProvider;
public ProviderStatesController(
OrdersDbContext context,
IServiceProvider serviceProvider)
{
_context = context;
_serviceProvider = serviceProvider;
}
[HttpPost]
public async Task<IActionResult> SetupState([FromBody] ProviderStateRequest request)
{
// Clear existing test data
await CleanupTestData();
// Handle the specific state
await HandleState(request.State, request.Params);
return Ok();
}
private async Task HandleState(string state, Dictionary<string, object>? parameters)
{
switch (state)
{
case "an order with id 123 exists":
await SeedOrder("123", parameters);
break;
case "no order with id non-existent exists":
// No setup needed - just ensure it doesn't exist
break;
case "customer customer-789 exists":
await SeedCustomer("customer-789", parameters);
break;
case "an order was created":
// For message pacts - seed the event data
await SeedOrderCreatedEvent(parameters);
break;
case "the system has orders matching search criteria":
await SeedMultipleOrders(parameters);
break;
default:
throw new NotSupportedException($"Unknown provider state: {state}");
}
}
private async Task SeedOrder(string orderId, Dictionary<string, object>? parameters)
{
var order = new Order
{
Id = orderId,
CustomerId = GetParam(parameters, "customerId", "customer-456"),
Status = GetParam(parameters, "status", "Pending"),
Items = new List<OrderItem>
{
new OrderItem
{
ProductId = "prod-1",
Quantity = 1,
Price = 99.99m
}
},
Total = 99.99m,
CreatedAt = DateTime.UtcNow
};
_context.Orders.Add(order);
await _context.SaveChangesAsync();
}
private async Task SeedCustomer(string customerId, Dictionary<string, object>? parameters)
{
var customer = new Customer
{
Id = customerId,
Name = GetParam(parameters, "name", "Test Customer"),
Email = GetParam(parameters, "email", "test@example.com")
};
_context.Customers.Add(customer);
await _context.SaveChangesAsync();
}
private async Task CleanupTestData()
{
// Remove test data to ensure clean state
_context.Orders.RemoveRange(_context.Orders);
_context.Customers.RemoveRange(_context.Customers);
await _context.SaveChangesAsync();
}
private static T GetParam<T>(Dictionary<string, object>? parameters, string key, T defaultValue)
{
if (parameters == null || !parameters.TryGetValue(key, out var value))
return defaultValue;
return (T)Convert.ChangeType(value, typeof(T));
}
}
public class ProviderStateRequest
{
public string State { get; set; } = string.Empty;
public Dictionary<string, object>? Params { get; set; }
}
CONSUMER VERSION SELECTORS:
Purpose: Specify which consumer pacts to verify
┌─────────────────────────────────────────────────────────────────┐
│ Selector │ Meaning │
├─────────────────────────────────────────────────────────────────┤
│ MainBranch = true │ Latest from main/master branch │
│ DeployedOrReleased │ Currently deployed versions │
│ Branch = "feature-x" │ Specific branch │
│ Consumer = "OrdersUI" │ Specific consumer only │
│ MatchingBranch = true │ Same branch as provider │
│ Latest = true │ Latest version (any branch) │
│ Tag = "production" │ Tagged with specific tag │
└─────────────────────────────────────────────────────────────────┘
COMMON PATTERNS:
CI Pipeline (feature branch):
- MainBranch = true (always verify against main)
- MatchingBranch = true (verify against same feature branch)
- DeployedOrReleased = true (verify against production)
Release Pipeline:
- DeployedOrReleased = true (all deployed versions)
- MainBranch = true (latest main)
Development:
- Latest = true, Consumer = "OrdersUI" (specific consumer)
STATE HANDLER PATTERNS:
1. DATA SEEDING
- Create required entities in database
- Set up relationships
- Initialize with specific values
2. FEATURE FLAGS
- Enable/disable features
- Configure behavior modes
3. EXTERNAL SERVICE MOCKS
- Configure mock responses
- Simulate external dependencies
4. TIME MANIPULATION
- Set system time for date-sensitive tests
- Configure clock providers
5. AUTHENTICATION
- Set up test users
- Configure permissions
BEST PRACTICES:
DO:
✓ Isolate test data (don't affect real data)
✓ Clean up between states
✓ Support parameterized states
✓ Keep states deterministic
✓ Document state requirements
DON'T:
✗ Share state between tests
✗ Rely on existing data
✗ Use real external services
✗ Create side effects
✗ Make states order-dependent
// Using test containers for isolation
// File: OrdersApi.Provider.Tests/IsolatedProviderTests.cs
public class IsolatedProviderTests : IAsyncLifetime
{
private readonly PostgreSqlContainer _postgres;
private WebApplicationFactory<Program> _factory = null!;
public IsolatedProviderTests()
{
_postgres = new PostgreSqlBuilder()
.WithImage("postgres:16-alpine")
.Build();
}
public async Task InitializeAsync()
{
await _postgres.StartAsync();
_factory = new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
// Replace database with test container
services.RemoveAll<DbContextOptions<OrdersDbContext>>();
services.AddDbContext<OrdersDbContext>(options =>
options.UseNpgsql(_postgres.GetConnectionString()));
});
});
// Apply migrations
using var scope = _factory.Services.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<OrdersDbContext>();
await context.Database.MigrateAsync();
}
public async Task DisposeAsync()
{
await _factory.DisposeAsync();
await _postgres.DisposeAsync();
}
[Fact]
public void VerifyPacts()
{
// Test implementation using isolated database
}
}
VERIFICATION OUTPUT:
Verifying a pact between OrdersClient and OrdersApi
Given an order with id 123 exists
a request for an existing order
with GET /orders/123
returns a response which
has status code 200 ✓
has a matching body ✓
includes headers ✓
Given no order with id non-existent exists
a request for a non-existent order
with GET /orders/non-existent
returns a response which
has status code 404 ✓
has a matching body ✓
Given customer customer-789 exists
a request to create a new order
with POST /orders
returns a response which
has status code 201 ✓
has a matching body ✓
includes headers ✓
3 interactions, 0 failures
FAILURE EXAMPLE:
Given an order with id 123 exists
a request for an existing order
with GET /orders/123
returns a response which
has status code 200 ✓
has a matching body
$.status -> Expected "Pending" but got "Processing" ✗
1 interaction, 1 failure
# GitHub Actions example
# .github/workflows/provider-verification.yml
name: Provider Verification
on:
push:
branches: [main, 'feature/**']
workflow_dispatch:
inputs:
consumer:
description: 'Specific consumer to verify'
required: false
jobs:
verify:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Restore
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Verify Pacts
env:
GIT_SHA: ${{ github.sha }}
GIT_BRANCH: ${{ github.ref_name }}
PACT_BROKER_BASE_URL: ${{ secrets.PACT_BROKER_URL }}
PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}
run: dotnet test --filter "Category=Pact"
- name: Can I Deploy?
run: |
pact-broker can-i-deploy \
--pacticipant OrdersApi \
--version ${{ github.sha }} \
--to-environment production
COMMON VERIFICATION FAILURES:
1. STATE NOT FOUND
Error: Unknown provider state: "order exists"
Fix: Implement the state handler or check state name spelling
2. DATA NOT SEEDED
Error: 404 when expecting 200
Fix: Verify state handler seeds data correctly
3. RESPONSE MISMATCH
Error: Expected "Pending" but got "Processing"
Fix: Check state sets correct values, or use matchers in contract
4. MISSING FIELDS
Error: $.newField was not found
Fix: Add field to response or update contract to use optional matcher
5. STATE CLEANUP ISSUES
Error: Duplicate key violation
Fix: Ensure cleanup runs before each state setup
DEBUGGING TIPS:
• Enable verbose logging in verifier
• Log state handler execution
• Check database state after setup
• Verify provider URL is correct
• Ensure authentication is configured
# Provider Verification Assessment: [Provider Name]
## Provider Profile
- **Provider:** [Name]
- **Consumers:** [List]
- **Test Environment:** [Local/CI/Isolated]
## State Handlers
| State | Handler | Data Seeded |
|-------|---------|-------------|
| [State] | [Method] | [What data] |
## Verification Configuration
- **Consumer Selectors:** [MainBranch, DeployedOrReleased, etc.]
- **Publish Results:** [Yes/No]
- **Provider Version:** [Source]
- **Branch Tracking:** [Yes/No]
## Test Isolation
- [ ] Database isolated per test run
- [ ] External services mocked
- [ ] State cleanup between interactions
- [ ] No shared mutable state
## CI/CD Integration
- [ ] Verification in pipeline
- [ ] Results published to broker
- [ ] Can-I-Deploy gate configured
- [ ] Branch protection rules set
## Issues and Gaps
| Issue | Severity | Resolution |
|-------|----------|------------|
| [Issue] | [H/M/L] | [Plan] |
When implementing provider verification:
For detailed guidance:
Last Updated: 2025-12-26
Master defensive Bash programming techniques for production-grade scripts. Use when writing robust shell scripts, CI/CD pipelines, or system utilities requiring fault tolerance and safety.