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>
163 lines
5.8 KiB
Dart
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,
|
|
);
|
|
}
|
|
});
|
|
});
|
|
}
|