- 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
251 lines
8.3 KiB
Dart
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',
|
|
);
|
|
});
|
|
});
|
|
}
|