// 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); }); }