Ocideck/test/classification_enforcement_policy_test.dart
Brenno de Winter 173f1a3f26
Some checks are pending
CI / Format · Analyze · Test (push) Waiting to run
CI / Format · Analyze · Test (pull_request) Waiting to run
Add TLP classification enforcement with visual marking and export metadata.
Extend export gates with optional floor and required classification, stamp PDF/PPTX/HTML metadata, and show banners and watermarks WYSIWYG across editor, presenter, and export.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 10:35:55 +02:00

163 lines
5.8 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:ocideck/models/deck.dart';
import 'package:ocideck/models/settings.dart';
import 'package:ocideck/services/classification_enforcement_policy.dart';
import 'package:ocideck/services/classification_policy.dart';
void main() {
group('ClassificationEnforcementPolicy', () {
test('without rules every level is allowed', () {
const policy = ClassificationEnforcementPolicy();
expect(policy.hasGate, isFalse);
for (final level in TlpLevel.values) {
expect(policy.evaluate(level).allowed, isTrue, reason: level.name);
}
});
group('release ceiling (plafond)', () {
test('allows levels at or below the ceiling', () {
const policy = ClassificationEnforcementPolicy(
maxReleaseLevel: TlpLevel.amber,
);
expect(policy.hasGate, isTrue);
for (final level in [
TlpLevel.none,
TlpLevel.clear,
TlpLevel.green,
TlpLevel.amber,
]) {
expect(policy.evaluate(level).allowed, isTrue, reason: level.name);
}
});
test('blocks levels above the ceiling', () {
const policy = ClassificationEnforcementPolicy(
maxReleaseLevel: TlpLevel.amber,
);
for (final level in [TlpLevel.amberStrict, TlpLevel.red]) {
final decision = policy.evaluate(level);
expect(decision.allowed, isFalse, reason: level.name);
expect(decision.reason, contains(level.label));
expect(decision.reason, contains(TlpLevel.amber.label));
}
});
test('a ceiling of none only allows unclassified decks', () {
const policy = ClassificationEnforcementPolicy(
maxReleaseLevel: TlpLevel.none,
);
expect(policy.evaluate(TlpLevel.none).allowed, isTrue);
expect(policy.evaluate(TlpLevel.clear).allowed, isFalse);
});
});
group('minimum level (vloer)', () {
test('blocks decks below the required minimum', () {
const policy = ClassificationEnforcementPolicy(
minRequiredLevel: TlpLevel.green,
);
expect(policy.evaluate(TlpLevel.none).allowed, isFalse);
expect(policy.evaluate(TlpLevel.clear).allowed, isFalse);
expect(policy.evaluate(TlpLevel.green).allowed, isTrue);
expect(policy.evaluate(TlpLevel.amber).allowed, isTrue);
});
test('explains when the deck is unclassified', () {
const policy = ClassificationEnforcementPolicy(
minRequiredLevel: TlpLevel.green,
);
final decision = policy.evaluate(TlpLevel.none);
expect(decision.reason, contains('niet geclassificeerd'));
expect(decision.reason, contains(TlpLevel.green.label));
});
});
group('requireClassification', () {
test('blocks unclassified decks when enabled', () {
const policy = ClassificationEnforcementPolicy(
requireClassification: true,
);
expect(policy.hasGate, isTrue);
expect(policy.evaluate(TlpLevel.none).allowed, isFalse);
expect(policy.evaluate(TlpLevel.clear).allowed, isTrue);
});
test('does not block classified decks on its own', () {
const policy = ClassificationEnforcementPolicy(
requireClassification: true,
);
for (final level in TlpLevel.values.where((l) => l != TlpLevel.none)) {
expect(policy.evaluate(level).allowed, isTrue, reason: level.name);
}
});
});
test('floor is checked before ceiling', () {
const policy = ClassificationEnforcementPolicy(
minRequiredLevel: TlpLevel.green,
maxReleaseLevel: TlpLevel.amber,
);
final low = policy.evaluate(TlpLevel.clear);
expect(low.allowed, isFalse);
expect(low.reason, contains('minimum'));
final high = policy.evaluate(TlpLevel.red);
expect(high.allowed, isFalse);
expect(high.reason, contains('vrijgaveniveau'));
});
group('fromAppSettings', () {
test('maps stored keys and flags', () {
const settings = AppSettings(
maxReleaseExportTlpKey: 'amber',
minRequiredExportTlpKey: 'green',
requireClassificationOnExport: true,
classificationWatermarkEnabled: true,
);
final policy = ClassificationEnforcementPolicy.fromAppSettings(settings);
expect(policy.maxReleaseLevel, TlpLevel.amber);
expect(policy.minRequiredLevel, TlpLevel.green);
expect(policy.requireClassification, isTrue);
expect(policy.hasGate, isTrue);
});
test('defaults leave enforcement off', () {
final policy = ClassificationEnforcementPolicy.fromAppSettings(
const AppSettings(),
);
expect(policy.hasGate, isFalse);
expect(policy.evaluate(TlpLevel.red).allowed, isTrue);
});
});
test('releasePolicy matches the legacy ClassificationPolicy', () {
const enforcement = ClassificationEnforcementPolicy(
maxReleaseLevel: TlpLevel.green,
minRequiredLevel: TlpLevel.clear,
requireClassification: true,
);
const legacy = ClassificationPolicy(maxReleaseLevel: TlpLevel.green);
for (final level in TlpLevel.values) {
expect(
enforcement.releasePolicy.evaluate(level).allowed,
legacy.evaluate(level).allowed,
reason: level.name,
);
}
});
test('fromMaxReleaseKey is backward compatible with fromKey', () {
final enforcement = ClassificationEnforcementPolicy.fromMaxReleaseKey(
TlpLevel.amber.key,
);
final legacy = ClassificationPolicy.fromKey(TlpLevel.amber.key);
for (final level in TlpLevel.values) {
expect(
enforcement.evaluate(level).allowed,
legacy.evaluate(level).allowed,
reason: level.name,
);
}
});
});
}