appsok/test/jenkins_web_auth_bridge_test.dart
toki 7ad88fb236 feat: jenkins credential milestone update and reauth UI implementation
- Update jenkins-credential.md milestone progress
- Implement ReAuthUi and related fixes
- Archive completed subtasks (02+01, 03)
- Add new 04+03_reauth_ui subtask with PLAN and CODE_REVIEW
- Update app, settings, and session related files
- Add auth bridge and app auth tests
2026-06-13 17:03:42 +09:00

251 lines
8.3 KiB
Dart

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',
);
});
});
}