fix(#80): only URL/port gate a broker enable; firmware owns the rest

On-hardware feedback: JWT owner defaults to the device pubkey and IATA override
is optional by definition, so gating a save on them is wrong -- they have
under-the-hood firmware defaults.

BrokerConfig.enableError now checks only the structural fields with no default:
URL present, port in range. The JWT field checks are gone; the firmware enforces
JWT completeness with its defaults, and a genuinely-bad enable surfaces the
firmware's reason via the failure messaging added earlier. The editor's pre-save
check and the list's quick Enable both follow.

Tests updated to the new contract (a JWT broker with blank owner/audience now
enables; a structurally-complete quick Enable confirms). Full suite green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
pull/99/head
Strycher 4 weeks ago
parent 0c737f2052
commit 2ac0755361

@ -150,10 +150,8 @@ class BrokerConfig {
String? get enableError { String? get enableError {
if (url.isEmpty) return 'URL is required'; if (url.isEmpty) return 'URL is required';
if (port < 1 || port > 65535) return 'Port must be between 1 and 65535'; if (port < 1 || port > 65535) return 'Port must be between 1 and 65535';
if (authType == BrokerAuthType.jwt && // JWT owner (device pubkey), IATA override, etc. are firmware-defaulted, so
(jwtAudience.isEmpty || jwtOwner.isEmpty)) { // the client does NOT gate them the firmware enforces with its defaults.
return 'JWT auth needs an audience and an owner';
}
return null; return null;
} }

@ -113,14 +113,12 @@ class _BrokerEditorScreenState extends State<BrokerEditorScreen> {
_changedFields().isNotEmpty || _enabled != _baseline.enabled; _changedFields().isNotEmpty || _enabled != _baseline.enabled;
/// Pre-enable validation, shared with the list's quick Enable via /// Pre-enable validation, shared with the list's quick Enable via
/// [BrokerConfig.enableError]. Builds a tentative config from the form. /// [BrokerConfig.enableError]. Only structural fields (URL, port) gate a save
/// the firmware enforces the rest via its own defaults.
String? _validate() => BrokerConfig( String? _validate() => BrokerConfig(
slot: _baseline.slot, slot: _baseline.slot,
url: _url.text.trim(), url: _url.text.trim(),
port: int.tryParse(_port.text.trim()) ?? -1, port: int.tryParse(_port.text.trim()) ?? -1,
authType: _authType,
jwtAudience: _jwtAudience.text.trim(),
jwtOwner: _jwtOwner.text.trim(),
).enableError; ).enableError;
void _snack(String msg, {bool isError = false}) { void _snack(String msg, {bool isError = false}) {

@ -203,7 +203,9 @@ void main() {
await _drainSnack(tester); await _drainSnack(tester);
}); });
testWidgets('enabling with incomplete JWT is blocked', (tester) async { testWidgets('a JWT broker with blank owner/audience still enables', (
tester,
) async {
final fake = _FakeSvc(); final fake = _FakeSvc();
await _open( await _open(
tester, tester,
@ -212,7 +214,8 @@ void main() {
slot: 2, slot: 2,
url: 'h', url: 'h',
port: 1883, port: 1883,
authType: BrokerAuthType.jwt, // disabled, blank audience/owner authType:
BrokerAuthType.jwt, // blank owner/audience -> firmware default
), ),
); );
@ -221,8 +224,14 @@ void main() {
await tester.tap(find.byTooltip('Save')); await tester.tap(find.byTooltip('Save'));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(fake.saveCalls, isEmpty); expect(
expect(find.textContaining('needs an audience'), findsOneWidget); fake.saveCalls,
hasLength(1),
reason:
'the client no longer gates JWT fields — the firmware enforces them '
'with its defaults (owner -> device pubkey)',
);
expect(fake.saveCalls.single.enable, isTrue);
await _drainSnack(tester); await _drainSnack(tester);
}); });
} }

@ -102,12 +102,8 @@ void main() {
expect(fake.clearCalls, contains(2)); expect(fake.clearCalls, contains(2));
}); });
testWidgets('quick Enable refuses an incomplete broker with a reason', ( testWidgets('a successful quick Enable confirms', (tester) async {
tester, final fake = _FakeSvc(const [BrokerConfig(slot: 2, url: 'a', port: 1883)]);
) async {
final fake = _FakeSvc(const [
BrokerConfig(slot: 2, url: 'a', port: 1883, authType: BrokerAuthType.jwt),
]);
await _pump(tester, fake); await _pump(tester, fake);
await tester.longPress(find.text('[2] a')); await tester.longPress(find.text('[2] a'));
@ -115,12 +111,8 @@ void main() {
await tester.tap(find.text('Enable')); await tester.tap(find.text('Enable'));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect( expect(fake.setCalls, contains('2.enabled=1'));
fake.setCalls, expect(find.textContaining('enabled'), findsOneWidget);
isEmpty,
reason: 'an incomplete broker must not be enabled',
);
expect(find.textContaining("Can't enable"), findsOneWidget);
await tester.pump(const Duration(seconds: 5)); await tester.pump(const Duration(seconds: 5));
}); });

Loading…
Cancel
Save

Powered by TurnKey Linux.