import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:appsok/src/features/settings/jenkins_web_login_page.dart'; import 'package:appsok/src/features/settings/settings_page.dart' show JenkinsWebLoginResult; void main() { group('parseCrumbResult', () { test('returns crumb for valid response', () { const json = '{"crumbRequestField":"Jenkins-Crumb","crumb":"abc123","_class":"hudson.security.csrf.DefaultCrumbIssuer"}'; final result = parseCrumbResult(json); expect(result, isNotNull); expect(result!.crumbRequestField, 'Jenkins-Crumb'); expect(result.crumb, 'abc123'); }); test('returns null when crumbRequestField is empty', () { const json = '{"crumbRequestField":"","crumb":"abc123"}'; expect(parseCrumbResult(json), isNull); }); test('returns null when crumb is empty', () { const json = '{"crumbRequestField":"Jenkins-Crumb","crumb":""}'; expect(parseCrumbResult(json), isNull); }); test('returns null for malformed JSON', () { expect(parseCrumbResult('not-json'), isNull); }); test('returns null for empty string', () { expect(parseCrumbResult(''), isNull); }); }); group('parseTokenResult', () { test('returns token for valid response', () { const json = '{"status":"ok","data":{"tokenName":"AppSok macOS 1234","tokenUuid":"uuid-1","tokenValue":"mytoken99"}}'; final result = parseTokenResult(json); expect(result, isNotNull); expect(result!.tokenValue, 'mytoken99'); }); test('returns null when status is not ok', () { const json = '{"status":"error","data":{"tokenName":"t","tokenUuid":"u","tokenValue":"v"}}'; expect(parseTokenResult(json), isNull); }); test('returns null when tokenValue is empty', () { const json = '{"status":"ok","data":{"tokenName":"t","tokenUuid":"u","tokenValue":""}}'; expect(parseTokenResult(json), isNull); }); test('returns null when data is missing', () { const json = '{"status":"ok"}'; expect(parseTokenResult(json), isNull); }); test('returns null for malformed JSON', () { expect(parseTokenResult('{bad json'), isNull); }); }); group('parseBridgeTokenResult', () { String makeComposite({ String crumbField = 'Jenkins-Crumb', String crumb = 'crumbval', String status = 'ok', String tokenValue = 'tok123', }) { final crumbJson = jsonEncode({ 'crumbRequestField': crumbField, 'crumb': crumb, }); final tokenJson = jsonEncode({ 'status': status, 'data': { 'tokenName': 'AppSok macOS 0', 'tokenUuid': 'uuid', 'tokenValue': tokenValue, }, }); return jsonEncode({'crumb': crumbJson, 'token': tokenJson}); } test('returns parsed crumb and token for valid composite JSON', () { final result = parseBridgeTokenResult(makeComposite()); expect(result, isNotNull); expect(result!.crumb.crumbRequestField, 'Jenkins-Crumb'); expect(result.crumb.crumb, 'crumbval'); expect(result.token.tokenValue, 'tok123'); }); test('returns null when crumb part is invalid', () { final composite = jsonEncode({ 'crumb': jsonEncode({'crumbRequestField': '', 'crumb': ''}), 'token': jsonEncode({ 'status': 'ok', 'data': { 'tokenName': 't', 'tokenUuid': 'u', 'tokenValue': 'tok', }, }), }); expect(parseBridgeTokenResult(composite), isNull); }); test('returns null when token status is error', () { final result = parseBridgeTokenResult(makeComposite(status: 'error')); expect(result, isNull); }); test('returns null for malformed outer JSON', () { expect(parseBridgeTokenResult('{bad'), isNull); }); test('returns null for null literal string', () { expect(parseBridgeTokenResult('null'), isNull); }); }); group('buildTokenBridgeJs', () { test('contains crumb path in generated JS', () { final js = buildTokenBridgeJs( '/crumbIssuer/api/json', '/user/toki/descriptorByName/jenkins.security.ApiTokenProperty/generateNewToken', 'AppSok macOS 0', ); expect(js, contains('/crumbIssuer/api/json')); }); test('contains token path in generated JS', () { final js = buildTokenBridgeJs( '/crumbIssuer/api/json', '/user/toki/descriptorByName/jenkins.security.ApiTokenProperty/generateNewToken', 'AppSok macOS 0', ); expect( js, contains( '/user/toki/descriptorByName/jenkins.security.ApiTokenProperty/generateNewToken', ), ); }); test('uses same-origin credentials in generated JS', () { final js = buildTokenBridgeJs( '/crumbIssuer/api/json', '/user/toki/generateNewToken', 'AppSok macOS 0', ); expect(js, contains("credentials: 'same-origin'")); }); test('token name is placed inside URLSearchParams value not raw JS', () { // The token name goes into URLSearchParams({ newTokenName: ... }) as a // JSON-encoded string value, not as raw JS — no eval/injection risk. final js = buildTokenBridgeJs( '/crumbIssuer/api/json', '/user/toki/generateNewToken', 'AppSok macOS 1234', ); expect(js, contains('newTokenName')); expect(js, contains('AppSok macOS 1234')); // Token name is inside a quoted JSON string, not unquoted. expect(js, contains('"AppSok macOS 1234"')); }); test('percent-encoded username round-trips in token path', () { final username = 'ada lovelace'; final encoded = Uri.encodeComponent(username); final tokenPath = '/user/$encoded/descriptorByName/jenkins.security.ApiTokenProperty/generateNewToken'; final js = buildTokenBridgeJs('/crumbIssuer/api/json', tokenPath, 'AppSok macOS 0'); expect(js, contains('ada%20lovelace')); }); }); group('JenkinsWebLoginResult apiToken contract', () { test('JenkinsWebLoginResult carries apiToken from bridge result', () { // Verifies the single-pop contract: _issueToken embeds apiToken into // JenkinsWebLoginResult via a single Navigator.pop. const result = JenkinsWebLoginResult( username: 'toki', displayName: 'Toki Lab', apiToken: 'issued-token-xyz', ); expect(result.apiToken, 'issued-token-xyz'); expect(result.username, 'toki'); }); test('JenkinsWebLoginResult apiToken defaults to null when not provided', () { const result = JenkinsWebLoginResult(username: 'toki'); expect(result.apiToken, isNull); }); test('parseBridgeTokenResult produces a tokenValue that maps to apiToken', () { // Simulates the bridge result → JenkinsWebLoginResult.apiToken path. final composite = jsonEncode({ 'crumb': jsonEncode({ 'crumbRequestField': 'Jenkins-Crumb', 'crumb': 'crumbval', }), 'token': jsonEncode({ 'status': 'ok', 'data': { 'tokenName': 'AppSok macOS 0', 'tokenUuid': 'uuid', 'tokenValue': 'bridge-token-abc', }, }), }); final bridgeResult = parseBridgeTokenResult(composite); expect(bridgeResult, isNotNull); final webResult = JenkinsWebLoginResult( username: 'toki', apiToken: bridgeResult!.token.tokenValue, ); expect(webResult.apiToken, 'bridge-token-abc'); }); }); group('jenkinsUri (token path construction)', () { test('encodes username with space for token generate path', () { final uri = jenkinsUri( 'https://jenkins.example.com', 'user/${Uri.encodeComponent("ada lovelace")}/descriptorByName/jenkins.security.ApiTokenProperty/generateNewToken', ); expect(uri.path, contains('ada%20lovelace')); }); test('context path URL produces correct token path', () { final uri = jenkinsUri( 'https://jenkins.example.com/jenkins', 'user/toki/descriptorByName/jenkins.security.ApiTokenProperty/generateNewToken', ); expect( uri.path, '/jenkins/user/toki/descriptorByName/jenkins.security.ApiTokenProperty/generateNewToken', ); }); }); }