Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
b6d9b6b
ci(coverage): unify noise policy — ignore-marker parity and generated…
ahmednfwela Jul 10, 2026
b8e0b77
ci(coverage): harvest full workspace coverage from patrol integration…
ahmednfwela Jul 10, 2026
d8a8fc9
fix(oidc_core): actually clear query/fragment when deriving issuer fr…
ahmednfwela Jul 10, 2026
e199076
fix(x509_plus): stop double-encoding OIDs in toAsn1; parse NoticeRefe…
ahmednfwela Jul 10, 2026
30569ba
fix(jose_plus): JoseHeader.critical no longer throws on headers carry…
ahmednfwela Jul 10, 2026
47a10cc
test: surface-load every package and drive unit coverage toward 95%
ahmednfwela Jul 10, 2026
77d2399
test: pin certificate fixture test to vm; make pub-proxy forward herm…
ahmednfwela Jul 10, 2026
ed7b535
ci(tests): drop --ignore-timeouts from web test runs; fail wedged bro…
ahmednfwela Jul 11, 2026
f057d51
ci(tests): bound web suite loads at 4m — wedged browser launches fail…
ahmednfwela Jul 11, 2026
3e070c4
ci(tests): exclude slow web-crypto suites from PR web runs; drop suit…
ahmednfwela Jul 11, 2026
ae9917a
ci(tests): tag pss_eddsa suite as slow-web-crypto too
ahmednfwela Jul 11, 2026
4bd9da4
ci(tests): scope PR browser testing to platform-divergent packages
ahmednfwela Jul 11, 2026
e362d42
fix(ci): bound browser suite loads — a wedged load hangs forever by d…
ahmednfwela Jul 11, 2026
1b660eb
fix(ci): step-level timeout backstop on browser test steps
ahmednfwela Jul 11, 2026
5e47045
fix(ci): serialize PR-scope browser suites; the wedge is a post-succe…
ahmednfwela Jul 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 50 additions & 6 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ jobs:
env:
TEST_PRESET: all
CHROME_FLAGS: --headless --no-sandbox --disable-dev-shm-usage --disable-gpu
# jose_plus's crypto-executor suites (keygen/sign/encrypt) take ~55min
# per browser under dart2js's ~200x slower BigInt (#371); while they
# run, other suites' loads starve. Exclude them (tagged slow-web-crypto,
# see packages/jose_plus/dart_test.yaml) from PR web runs only -- VM
# runs always execute every tag, and main-branch pushes keep full web
# crypto signal.
WEB_EXCLUDE_TAGS: ${{ github.event_name == 'pull_request' && 'slow-web-crypto' || '' }}
steps:
- name: Checkout Code
uses: actions/checkout@v7
Expand All @@ -64,9 +71,36 @@ jobs:
# melos run pana
- name: "[Verify step] Test Dart packages [VM]"
run: melos run test:vm
# PRs: browser-test only the packages with genuinely platform-divergent
# lib code (js-interop) -- everything else duplicates VM signal at the
# cost of one dart2js compile per test file (the old full sweep spent
# most of an hour per browser per leg compiling). Pushes to main run
# the full browser sweep.
# timeout-minutes on the browser steps is the backstop of last resort:
# package:test's --timeout and --suite-load-timeout only bound test
# execution and suite loads, and a browser wedge can strike OUTSIDE
# both (run 29148902320: a Firefox step hung 57+ min with the 4m load
# bound active -- so the hang was in launch/shutdown/orchestration,
# which no dart-level timer covers). The runner kills the whole
# process tree, so the step fails loudly and is rerunnable instead of
# idling to GitHub's 6h job kill. PR-scope steps finish in under a
# minute (15m = wide margin); full sweeps run ~55-66min per browser
# (150m = wide margin).
- name: "[Verify step] Test Dart packages [Chrome, PR scope]"
if: ${{ github.event_name == 'pull_request' }}
timeout-minutes: 15
run: melos run test:web:pr:chrome
- name: "[Verify step] Test Dart packages [Firefox, PR scope]"
if: ${{ github.event_name == 'pull_request' }}
timeout-minutes: 15
run: melos run test:web:pr:firefox
- name: "[Verify step] Test Dart packages [Chrome]"
if: ${{ github.event_name != 'pull_request' }}
timeout-minutes: 150
run: melos run test:web:chrome
- name: "[Verify step] Test Dart packages [Firefox]"
if: ${{ github.event_name != 'pull_request' }}
timeout-minutes: 150
run: melos run test:web:firefox
- name: Remove dart_test.yaml Files
run: melos run remove_dart_test_yaml
Expand Down Expand Up @@ -217,7 +251,7 @@ jobs:
patrol_plus test \
--coverage \
--coverage-ignore="**/*.g.dart" \
--coverage-package oidc \
--coverage-workspace \
-d "$EMU_ID" \
--dart-define=CI=true \
--dart-define=OIDC_CONFORMANCE_TOKEN=${OIDC_CONFORMANCE_TOKEN}
Expand Down Expand Up @@ -364,7 +398,7 @@ jobs:
patrol_plus test \
--coverage \
--coverage-ignore="**/*.g.dart" \
--coverage-package oidc \
--coverage-workspace \
-d "${SIMULATOR_UDID:?Simulator UDID was not exported}" \
--ios "${SIMULATOR_OS:?Simulator OS was not exported}" \
--verbose \
Expand Down Expand Up @@ -486,13 +520,13 @@ jobs:
# Work around flaky desktop integration test runs when executing
# multiple test entrypoints in a single `flutter test integration_test`.
flutter test integration_test/app_test.dart \
--coverage --branch-coverage --coverage-package "oidc*" \
--coverage --branch-coverage --coverage-package "^(oidc|jose_plus|x509_plus|crypto_keys_plus)" \
-d macos --coverage-path coverage/macos-app-coverage.info \
--dart-define=CI=true --dart-define=OIDC_CONFORMANCE_TOKEN=${{env.OIDC_CONFORMANCE_TOKEN}} \
-r expanded

flutter test integration_test/offline_mode_test.dart \
--coverage --branch-coverage --coverage-package "oidc*" \
--coverage --branch-coverage --coverage-package "^(oidc|jose_plus|x509_plus|crypto_keys_plus)" \
-d macos --coverage-path coverage/macos-offline-coverage.info \
--dart-define=CI=true --dart-define=OIDC_CONFORMANCE_TOKEN=${{env.OIDC_CONFORMANCE_TOKEN}} \
-r expanded
Expand Down Expand Up @@ -548,7 +582,7 @@ jobs:
xvfb-run --auto-servernum patrol_plus test \
--coverage \
--coverage-ignore="**/*.g.dart" \
--coverage-package oidc \
--coverage-workspace \
-d linux \
--dart-define=CI=true \
--dart-define=OIDC_CONFORMANCE_TOKEN=$OIDC_CONFORMANCE_TOKEN
Expand Down Expand Up @@ -597,7 +631,7 @@ jobs:
patrol_plus.bat test \
--coverage \
--coverage-ignore="**/*.g.dart" \
--coverage-package oidc \
--coverage-workspace \
-d windows \
--dart-define=CI=true \
--dart-define=OIDC_CONFORMANCE_TOKEN=${{ env.OIDC_CONFORMANCE_TOKEN }}
Expand Down Expand Up @@ -669,6 +703,16 @@ jobs:
find . -type f -name "*-coverage.info"
dart pub global activate combine_coverage
dart pub global run combine_coverage --repo-path="$REPO_PATH"
- name: Strip generated files from combined coverage
# Parity with the unit pipeline's coverage:combine: without this the
# final-coverage artifact and the Codecov upload include *.g.dart and
# the generated oidc_cli version.dart from integration runs.
run: |
set -e
dart pub global activate remove_from_coverage
dart pub global run remove_from_coverage:remove_from_coverage \
-f coverage/lcov.info \
-r '\.g\.dart$' -r 'oidc_cli/lib/src/version\.dart$'
# Upload
- name: Upload the final coverage as an artifact
uses: actions/upload-artifact@v7
Expand Down
10 changes: 9 additions & 1 deletion codecov.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
coverage:
status:
patch: false
project: false
project: false

# Reporting-side noise exclusion. Generated code and the example app are not
# library surface; version.dart is generated (header: "Generated code. Do not
# modify.") but does not match the *.g.dart convention.
ignore:
- "**/*.g.dart"
- "**/example/**"
- "packages/oidc_cli/lib/src/version.dart"
25 changes: 25 additions & 0 deletions packages/crypto_keys_plus/test/library_surface_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Ensures the package-name-matching entrypoint (`crypto_keys_plus.dart`) is
// actually loaded by the test suite, so its re-exported surface stays
// visible to coverage tooling even though the rest of the suite only ever
// imports the legacy `crypto_keys.dart` entrypoint.
import 'package:crypto_keys_plus/crypto_keys_plus.dart';
import 'package:test/test.dart';

void main() {
group('crypto_keys_plus library surface', () {
test(
'algorithms exposes the documented digest identifiers',
() {
// `algorithms` is the top-level singleton re-exported from
// `crypto_keys.dart` via the package-name-matching barrel. Its
// identifier names are part of the documented public contract.
expect(algorithms.digest.sha256.name, 'digest/SHA-256');
expect(algorithms.digest.sha512.name, 'digest/SHA-512');
},
);

test('curves exposes the documented P-256 curve identifier', () {
expect(curves.p256.name, 'curve/P-256');
});
});
}
168 changes: 168 additions & 0 deletions packages/crypto_keys_plus/test/pointycastle_ext_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// Targeted tests for `lib/src/pointycastle_ext.dart`. This file is combined
// (not `part of`) into the main `crypto_keys.dart` library and its classes
// are not re-exported from the package's public entrypoints, so it must be
// imported directly by its `src/` path to reach the branches below.
import 'dart:typed_data';

import 'package:crypto_keys_plus/src/pointycastle_ext.dart';
import 'package:pointycastle/export.dart' as pc;
import 'package:test/test.dart';

/// A minimal concrete [BlockCipherWithAuthenticationTag] that does NOT
/// override [BlockCipherWithAuthenticationTag.processBlocks], so calling
/// [BlockCipherWithAuthenticationTag.process] exercises the base-class
/// `processBlocks` loop directly (the only production subclass,
/// `AesCbcAuthenticatedCipherWithHash`, overrides it).
class _IdentityTaggedCipher extends BlockCipherWithAuthenticationTag {
@override
String get algorithmName => 'identity-tagged';

@override
int get blockSize => 4;

@override
int get tagLength => 2;

@override
void initParameters(pc.CipherParameters? parameters) {}

@override
Uint8List finalizeTag() => Uint8List.fromList([0xaa, 0xbb]);

@override
int processBlock(Uint8List inp, int inpOff, Uint8List out, int outOff) {
// Identity transform: copy up to `blockSize` bytes (or whatever remains)
// from `inp` at `inpOff` into `out` at `outOff`.
var n = blockSize;
if (inpOff + n > inp.length) {
n = inp.length - inpOff;
}
for (var i = 0; i < n; i++) {
out[outOff + i] = inp[inpOff + i];
}
return blockSize;
}

@override
void reset() {}
}

void main() {
group('toHex', () {
test('encodes bytes as lowercase two-digit hex, zero-padded', () {
expect(toHex([0, 255, 16, 1]), '00ff1001');
});

test('empty input yields empty string', () {
expect(toHex(const []), '');
});
});

group('BlockCipherWithAuthenticationTag base processBlocks', () {
test('encrypting: appends the finalized tag after the ciphertext', () {
final cipher = _IdentityTaggedCipher();
cipher.init(
true,
ParametersWithIVAndAad(
pc.KeyParameter(Uint8List(16)),
Uint8List(12),
Uint8List(0),
),
);
final out = cipher.process(Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]));
// 8 bytes of (identity) ciphertext + the 2-byte tag from finalizeTag().
expect(out, [1, 2, 3, 4, 5, 6, 7, 8, 0xaa, 0xbb]);
});

test('decrypting: strips and verifies the trailing tag', () {
final cipher = _IdentityTaggedCipher();
cipher.init(
false,
ParametersWithIVAndAad(
pc.KeyParameter(Uint8List(16)),
Uint8List(12),
Uint8List(0),
),
);
final combined = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8, 0xaa, 0xbb]);
expect(cipher.process(combined), [1, 2, 3, 4, 5, 6, 7, 8]);
});

test('decrypting: throws when the trailing tag does not match', () {
final cipher = _IdentityTaggedCipher();
cipher.init(
false,
ParametersWithIVAndAad(
pc.KeyParameter(Uint8List(16)),
Uint8List(12),
Uint8List(0),
),
);
final tampered = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8, 0x00, 0x00]);
expect(() => cipher.process(tampered), throwsA(anything));
});

test(
'processBlocks pads the final partial block by reading past the '
'declared length', () {
// Exercises the `inputBlocks` ceil-division branch: 5 bytes over a
// block size of 4 requires 2 iterations.
final cipher = _IdentityTaggedCipher();
cipher.init(
true,
ParametersWithIVAndAad(
pc.KeyParameter(Uint8List(16)),
Uint8List(12),
Uint8List(0),
),
);
final out = cipher.process(Uint8List.fromList([9, 8, 7, 6, 5]));
expect(out.sublist(0, 5), [9, 8, 7, 6, 5]);
expect(out.sublist(5), [0xaa, 0xbb]);
});
});

group('AesCbcAuthenticatedCipherWithHash', () {
test('algorithmName combines the underlying cipher and MAC names', () {
final cipher =
AesCbcAuthenticatedCipherWithHash(pc.HMac(pc.SHA256Digest(), 64));
expect(cipher.algorithmName, 'AES/CBC/PKCS7+SHA-256/HMAC');
});

test('blockSize matches the underlying AES-CBC block size', () {
final cipher =
AesCbcAuthenticatedCipherWithHash(pc.HMac(pc.SHA256Digest(), 64));
expect(cipher.blockSize, 16);
});

test('processBlock is defensively unimplemented', () {
final cipher =
AesCbcAuthenticatedCipherWithHash(pc.HMac(pc.SHA256Digest(), 64));
expect(
() => cipher.processBlock(Uint8List(16), 0, Uint8List(16), 0),
throwsUnsupportedError,
);
});

test('reset is defensively unimplemented', () {
final cipher =
AesCbcAuthenticatedCipherWithHash(pc.HMac(pc.SHA256Digest(), 64));
expect(() => cipher.reset(), throwsUnsupportedError);
});
});

group('AESKeyWrap', () {
test('processBlock is defensively unimplemented', () {
final wrap = AESKeyWrap();
expect(
() => wrap.processBlock(Uint8List(16), 0, Uint8List(16), 0),
throwsUnsupportedError,
);
});

test('reset is defensively unimplemented', () {
final wrap = AESKeyWrap();
expect(() => wrap.reset(), throwsUnsupportedError);
});
});
}
Loading