auto-claude: subtask-5-2 - End-to-end verification of gRPC integration

Added comprehensive test suite and documentation for gRPC integration:
- test/api/grpc_config_test.dart: Unit tests for GrpcConfig
- test/api/grpc_client_test.dart: Unit tests for GrpcCqrsApiClient
- test/api/api_mode_config_test.dart: Unit tests for ApiModeConfig
- test/e2e/GRPC_E2E_VERIFICATION.md: Manual E2E testing guide

All 18 tests pass. Flutter analyze shows no issues.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Mathias Beaulieu-Duncan 2026-01-20 13:15:32 -05:00
parent a60f92c56d
commit 4bbf225aeb
4 changed files with 400 additions and 0 deletions

View File

@ -0,0 +1,67 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:planb_logistic/providers/providers.dart';
void main() {
group('ApiMode', () {
test('has correct values', () {
expect(ApiMode.values.length, equals(2));
expect(ApiMode.values, contains(ApiMode.http));
expect(ApiMode.values, contains(ApiMode.grpc));
});
});
group('ApiModeConfig', () {
test('development config defaults to HTTP', () {
const config = ApiModeConfig.development;
expect(config.mode, equals(ApiMode.http));
expect(config.isHttp, isTrue);
expect(config.isGrpc, isFalse);
expect(config.fallbackToHttpOnError, isTrue);
});
test('developmentGrpc config uses gRPC', () {
const config = ApiModeConfig.developmentGrpc;
expect(config.mode, equals(ApiMode.grpc));
expect(config.isGrpc, isTrue);
expect(config.isHttp, isFalse);
expect(config.fallbackToHttpOnError, isTrue);
});
test('production config defaults to HTTP', () {
const config = ApiModeConfig.production;
expect(config.mode, equals(ApiMode.http));
expect(config.isHttp, isTrue);
expect(config.isGrpc, isFalse);
expect(config.fallbackToHttpOnError, isFalse);
});
test('productionGrpc config uses gRPC without fallback', () {
const config = ApiModeConfig.productionGrpc;
expect(config.mode, equals(ApiMode.grpc));
expect(config.isGrpc, isTrue);
expect(config.isHttp, isFalse);
expect(config.fallbackToHttpOnError, isFalse);
});
test('custom config can be created', () {
const config = ApiModeConfig(
mode: ApiMode.grpc,
fallbackToHttpOnError: false,
);
expect(config.mode, equals(ApiMode.grpc));
expect(config.isGrpc, isTrue);
expect(config.fallbackToHttpOnError, isFalse);
});
test('default fallbackToHttpOnError is true', () {
const config = ApiModeConfig(mode: ApiMode.grpc);
expect(config.fallbackToHttpOnError, isTrue);
});
});
}

View File

@ -0,0 +1,85 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:planb_logistic/api/grpc_client.dart';
import 'package:planb_logistic/api/grpc_config.dart';
void main() {
group('GrpcCqrsApiClient', () {
test('can be created with development config', () {
final client = GrpcCqrsApiClient(
config: GrpcConfig.development,
authService: null,
);
expect(client.config, equals(GrpcConfig.development));
expect(client.isConnected, isFalse);
});
test('can be created with production config', () {
final client = GrpcCqrsApiClient(
config: GrpcConfig.production,
authService: null,
);
expect(client.config, equals(GrpcConfig.production));
expect(client.isConnected, isFalse);
});
test('channel is created lazily', () {
final client = GrpcCqrsApiClient(
config: GrpcConfig.development,
authService: null,
);
expect(client.isConnected, isFalse);
// Access the channel to trigger lazy initialization
final channel = client.channel;
expect(channel, isNotNull);
expect(client.isConnected, isTrue);
});
test('delivery client is created lazily', () {
final client = GrpcCqrsApiClient(
config: GrpcConfig.development,
authService: null,
);
// Access delivery client (implicitly creates channel first)
final deliveryClient = client.deliveryClient;
expect(deliveryClient, isNotNull);
expect(client.isConnected, isTrue);
});
test('shutdown clears channel and client', () async {
final client = GrpcCqrsApiClient(
config: GrpcConfig.development,
authService: null,
);
// Initialize the channel
final _ = client.channel;
expect(client.isConnected, isTrue);
// Shutdown
await client.shutdown();
expect(client.isConnected, isFalse);
});
test('terminate clears channel and client', () async {
final client = GrpcCqrsApiClient(
config: GrpcConfig.development,
authService: null,
);
// Initialize the channel
final _ = client.channel;
expect(client.isConnected, isTrue);
// Terminate
await client.terminate();
expect(client.isConnected, isFalse);
});
});
}

View File

@ -0,0 +1,56 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:planb_logistic/api/grpc_config.dart';
void main() {
group('GrpcConfig', () {
test('development config has correct values', () {
const config = GrpcConfig.development;
expect(config.host, equals('192.168.88.228'));
expect(config.port, equals(5011));
expect(config.useTls, isFalse);
expect(config.allowSelfSignedCertificate, isTrue);
expect(config.timeout, equals(const Duration(seconds: 30)));
expect(config.address, equals('192.168.88.228:5011'));
});
test('production config has correct values', () {
const config = GrpcConfig.production;
expect(config.host, equals('grpc-route.goutezplanb.com'));
expect(config.port, equals(443));
expect(config.useTls, isTrue);
expect(config.allowSelfSignedCertificate, isFalse);
expect(config.timeout, equals(const Duration(seconds: 30)));
expect(config.address, equals('grpc-route.goutezplanb.com:443'));
});
test('custom config can be created', () {
const config = GrpcConfig(
host: 'custom.example.com',
port: 9000,
timeout: Duration(seconds: 60),
useTls: true,
allowSelfSignedCertificate: true,
);
expect(config.host, equals('custom.example.com'));
expect(config.port, equals(9000));
expect(config.useTls, isTrue);
expect(config.allowSelfSignedCertificate, isTrue);
expect(config.timeout, equals(const Duration(seconds: 60)));
expect(config.address, equals('custom.example.com:9000'));
});
test('default values are correctly applied', () {
const config = GrpcConfig(
host: 'test.example.com',
port: 443,
);
expect(config.useTls, isTrue);
expect(config.allowSelfSignedCertificate, isFalse);
expect(config.timeout, equals(const Duration(seconds: 30)));
});
});
}

View File

@ -0,0 +1,192 @@
# gRPC Integration E2E Verification Guide
This document provides steps for verifying the gRPC integration end-to-end.
## Prerequisites
1. gRPC backend server running at `192.168.88.228:5011`
2. Flutter environment configured
3. Valid test credentials for authentication
## Step 1: Enable gRPC Mode
To enable gRPC mode, override the `apiModeConfigProvider` in the app's `ProviderScope`:
### Option A: Code Change (for testing)
Edit `lib/main.dart` to add the provider override:
```dart
import 'package:planb_logistic/providers/providers.dart';
void main() {
runApp(
ProviderScope(
overrides: [
// Enable gRPC mode with fallback to HTTP
apiModeConfigProvider.overrideWithValue(ApiModeConfig.developmentGrpc),
],
child: const MyApp(),
),
);
}
```
### Option B: Environment-Based Toggle
The app can also be configured to check an environment variable or build flag:
```dart
final useGrpc = const bool.fromEnvironment('USE_GRPC', defaultValue: false);
final apiMode = useGrpc ? ApiModeConfig.developmentGrpc : ApiModeConfig.development;
```
Then run with:
```bash
flutter run -d chrome --dart-define=USE_GRPC=true
```
## Step 2: Login to App
1. Launch the app in Chrome browser:
```bash
flutter run -d chrome
```
2. Click "Login with Keycloak" button
3. Enter valid credentials
4. Wait for redirect back to the app
**Expected:** User is authenticated and redirected to the main app.
## Step 3: Navigate to Routes Page
1. After login, navigate to the Routes page
2. The app should fetch delivery routes via gRPC
**Expected Behavior:**
- Routes list should populate with delivery route data
- No console errors related to gRPC
- In browser DevTools console, you should see:
- No "gRPC failed" messages (unless backend is unavailable)
- If gRPC fails and fallback is enabled, you may see: "gRPC failed, falling back to HTTP: ..."
**Verification Points:**
- Routes display with correct data (id, name, deliveries count)
- Loading indicator shows during fetch
- Error state handled gracefully if backend unavailable
## Step 4: Select a Route - Verify Deliveries Load
1. Click on a route from the list
2. Navigate to the deliveries page for that route
**Expected Behavior:**
- Deliveries list should populate with delivery data for the selected route
- Each delivery should show:
- Delivery index/number
- Customer name
- Address information
- Status (delivered/pending/skipped)
- Warehouse delivery should appear at the end of the list
**Verification Points:**
- Correct number of deliveries loaded
- All delivery fields properly mapped from gRPC response
- Warehouse delivery appended correctly
## Step 5: Mark a Delivery Complete
1. Select an uncompleted delivery
2. Mark it as complete (tap completion button)
**Expected Behavior:**
- Command sent via gRPC to `completeDelivery` endpoint
- Delivery status updates to "completed"
- UI refreshes to show new status
**Verification Points:**
- No error messages
- Delivery marked as completed in the list
- Timestamp recorded for completion
## Step 6: Check Browser Console for gRPC Logs
Open browser DevTools (F12) and check the Console tab for:
### Success Indicators:
- No error messages related to gRPC
- Successful data loading without fallback messages
### Warning Indicators (acceptable):
- "gRPC failed, falling back to HTTP: ..." - indicates gRPC failed but HTTP fallback worked
### Error Indicators (need investigation):
- "UNAUTHENTICATED" errors - token issue
- "UNAVAILABLE" errors - backend not reachable
- "DEADLINE_EXCEEDED" errors - timeout
- Unhandled exceptions
## Verification Checklist
| Step | Check | Status |
|------|-------|--------|
| 1 | gRPC mode enabled via provider override | [ ] |
| 2 | Login successful | [ ] |
| 3 | Routes load (via gRPC or HTTP fallback) | [ ] |
| 4 | Deliveries load for selected route | [ ] |
| 5 | Complete delivery command works | [ ] |
| 6 | No critical console errors | [ ] |
## Troubleshooting
### gRPC Backend Unavailable
If the gRPC backend at `192.168.88.228:5011` is not reachable:
1. **With fallback enabled (default):** App falls back to HTTP automatically
2. **Without fallback:** App shows error state
To test with HTTP only:
```dart
apiModeConfigProvider.overrideWithValue(ApiModeConfig.development)
```
### Authentication Issues
If you see UNAUTHENTICATED errors:
1. Check that the auth token is valid
2. Try logging out and back in
3. Verify the token is being sent in gRPC metadata
### Connection Timeout
If requests are timing out:
1. Check network connectivity to `192.168.88.228:5011`
2. Verify the backend server is running
3. Check firewall/VPN settings
## Production Configuration
For production deployment, use:
```dart
apiModeConfigProvider.overrideWithValue(ApiModeConfig.productionGrpc)
```
This connects to `grpc-route.goutezplanb.com:443` with TLS enabled.
## Test Commands
```bash
# Run unit tests (no backend required)
flutter test
# Run with gRPC enabled
flutter run -d chrome --dart-define=USE_GRPC=true
# Check gRPC backend is reachable (requires grpcurl)
grpcurl -plaintext 192.168.88.228:5011 list
```