Some checks failed
Security / Security check (push) Failing after 2s
Review follow-ups on the auth-status work: - Both daemon-start paths classified an auth-rejected hub as "daemon dead" via healthy() and showed a start-failure dialog while the shell banner above correctly blamed the token. They now share daemonAnswers(): only an unreachable probe counts as down. - An auth-rejected poll now re-reads ~/.chain/hub-auth-token and reconnects when the file changed, so a token fixed outside Studio (CLI, editor) heals the connection without a restart — previously the client kept the stale in-memory token forever and the banner's own advice could not work. - An endpoint switch resets the failure streak, so a stale in-flight probe can no longer let the unreachable banner blame the new endpoint for the old one's misses. - The auth-policy panel re-queries when the hub token is saved or cleared in the panel above (reloadTick), instead of keeping a stale admin-denied hint; it also renders the hub's new reload_required flag as a pending-reload warning (DE+EN). - today-pipeline.md still documented ~/.fai/today after the rename; the FAB theme comment now states the both-themes intent. flutter analyze clean; 71 tests green including four new ones. Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
209 lines
5.8 KiB
Dart
209 lines
5.8 KiB
Dart
// Settings → Security → hub access policy panel. Verifies the
|
|
// read-only rendering of the AuthStatus snapshot (validator,
|
|
// anonymous warning, token entries with scope grants and env-var
|
|
// state, JWT parameters) plus the live-reload affordance and the
|
|
// admin-scope refusal story — all through the loader test seams,
|
|
// no hub required.
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'package:chain_studio/data/hub.dart';
|
|
import 'package:chain_studio/l10n/app_localizations.dart';
|
|
import 'package:chain_studio/widgets/hub_auth_policy_panel.dart';
|
|
|
|
Widget _host(HubAuthPolicyPanel panel) => MaterialApp(
|
|
locale: const Locale('de'),
|
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
|
supportedLocales: AppLocalizations.supportedLocales,
|
|
home: Scaffold(body: SingleChildScrollView(child: panel)),
|
|
);
|
|
|
|
HubAuthPolicy _staticPolicy() => const HubAuthPolicy(
|
|
validator: 'static',
|
|
anonymousAllowed: false,
|
|
tokens: [
|
|
HubAuthTokenEntry(
|
|
name: 'digiscout-prod',
|
|
tokenEnv: 'CHAIN_TOKEN_DIGISCOUT',
|
|
envSet: true,
|
|
scopes: ['read', 'execute:llm.*'],
|
|
rateLimitPerMinute: 120,
|
|
),
|
|
HubAuthTokenEntry(
|
|
name: 'ci-reader',
|
|
tokenEnv: 'CHAIN_TOKEN_CI',
|
|
envSet: false,
|
|
scopes: ['read'],
|
|
rateLimitPerMinute: 0,
|
|
),
|
|
],
|
|
);
|
|
|
|
void main() {
|
|
testWidgets('static policy renders tokens, grants and env state', (
|
|
tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
_host(HubAuthPolicyPanel(loader: () async => _staticPolicy())),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(
|
|
find.text('Prüfverfahren: statische Token-Liste'),
|
|
findsOneWidget,
|
|
);
|
|
expect(find.text('digiscout-prod'), findsOneWidget);
|
|
// Fine-grained T5 grant rendered verbatim as a chip.
|
|
expect(find.text('execute:llm.*'), findsOneWidget);
|
|
expect(find.text('120/min'), findsOneWidget);
|
|
expect(
|
|
find.textContaining('CHAIN_TOKEN_DIGISCOUT ist gesetzt'),
|
|
findsOneWidget,
|
|
);
|
|
// The rotation footgun is called out, not hidden.
|
|
expect(find.textContaining('CHAIN_TOKEN_CI FEHLT'), findsOneWidget);
|
|
// No anonymous warning when tokens exist.
|
|
expect(find.textContaining('anonyme Aufrufe'), findsNothing);
|
|
});
|
|
|
|
testWidgets('anonymous hub shows the plain-language warning', (
|
|
tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
_host(
|
|
HubAuthPolicyPanel(
|
|
loader: () async => const HubAuthPolicy(
|
|
validator: 'static',
|
|
anonymousAllowed: true,
|
|
tokens: [],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.textContaining('anonyme Aufrufe'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('jwt policy renders parameters with not-checked fallback', (
|
|
tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
_host(
|
|
HubAuthPolicyPanel(
|
|
loader: () async => const HubAuthPolicy(
|
|
validator: 'jwt-rs256',
|
|
anonymousAllowed: false,
|
|
tokens: [],
|
|
jwt: HubJwtValidatorInfo(
|
|
keySource: '/etc/chain/idp.pem',
|
|
audience: 'chain-hub',
|
|
issuer: '',
|
|
scopeClaim: 'scope',
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(
|
|
find.textContaining('JWT (RS256)'),
|
|
findsOneWidget,
|
|
);
|
|
expect(find.text('/etc/chain/idp.pem'), findsOneWidget);
|
|
expect(find.text('chain-hub'), findsOneWidget);
|
|
// Empty issuer → honest "not checked", not an empty cell.
|
|
expect(find.text('wird nicht geprüft'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('reload button reports the new token count', (tester) async {
|
|
var reloads = 0;
|
|
await tester.pumpWidget(
|
|
_host(
|
|
HubAuthPolicyPanel(
|
|
loader: () async => _staticPolicy(),
|
|
reloader: () async {
|
|
reloads++;
|
|
return 2;
|
|
},
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.tap(find.text('Tokens neu laden'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(reloads, 1);
|
|
expect(find.text('Neu geladen — 2 Token aktiv.'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('permission denied maps to the admin-scope story', (
|
|
tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
_host(
|
|
HubAuthPolicyPanel(
|
|
loader: () async =>
|
|
throw Exception('gRPC Error (code: 7, codeName: PERMISSION_DENIED)'),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.textContaining('admin-Recht'), findsOneWidget);
|
|
expect(find.text('Erneut versuchen'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('reload-required policy shows the pending-reload hint', (
|
|
tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
_host(
|
|
HubAuthPolicyPanel(
|
|
loader: () async => const HubAuthPolicy(
|
|
validator: 'static',
|
|
anonymousAllowed: false,
|
|
tokens: [],
|
|
reloadRequired: true,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(
|
|
find.textContaining('erzwingt noch den alten Stand'),
|
|
findsOneWidget,
|
|
);
|
|
});
|
|
|
|
testWidgets('a bumped reloadTick re-queries the policy', (tester) async {
|
|
var loads = 0;
|
|
Widget hostWithTick(int tick) => _host(
|
|
HubAuthPolicyPanel(
|
|
loader: () async {
|
|
loads++;
|
|
return _staticPolicy();
|
|
},
|
|
reloadTick: tick,
|
|
),
|
|
);
|
|
|
|
await tester.pumpWidget(hostWithTick(0));
|
|
await tester.pumpAndSettle();
|
|
expect(loads, 1);
|
|
|
|
// Same tick: no reload on unrelated rebuilds.
|
|
await tester.pumpWidget(hostWithTick(0));
|
|
await tester.pumpAndSettle();
|
|
expect(loads, 1);
|
|
|
|
// Bumped tick (token saved next door): the panel re-queries.
|
|
await tester.pumpWidget(hostWithTick(1));
|
|
await tester.pumpAndSettle();
|
|
expect(loads, 2);
|
|
});
|
|
}
|