Files
webmail/lib/__tests__/classification-labels.test.ts
T

132 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest';
import type { ClassificationLabel } from '@/lib/admin/organisation-policy';
import { LABEL_PRESETS } from '@/lib/classification/presets';
import {
applyLabelPrefix,
draftLabelChoice,
draftLabelProperty,
labelHeaderLines,
labelHeaderProperties,
parseSioLabelMarking,
receivedLabel,
sioLabelValue,
textColorOn,
} from '@/lib/classification/labels';
// The headers a classification label writes (SIO-Label per RFC 7444 with
// RFC 2231 for non-ASCII markings, Sensitivity per RFC 2156, the admin's
// extra header), reading them back, and the subject prefix without doubling.
const generic = LABEL_PRESETS.find((p) => p.id === 'generic')!;
const german = LABEL_PRESETS.find((p) => p.id === 'german-public-sector')!;
const byId = (list: ClassificationLabel[], id: string) => list.find((l) => l.id === id)!;
const confidential = byId(generic.list, 'confidential');
const vsnfd = byId(german.list, 'vs-nfd');
describe('SIO-Label', () => {
it('writes an ASCII marking quoted, with readable colours', () => {
expect(sioLabelValue(confidential)).toBe(' marking="CONFIDENTIAL"; fgcolor=#000000; bgcolor=#d97706');
expect(textColorOn('#1e3a8a')).toBe('#ffffff');
expect(textColorOn('#fde68a')).toBe('#000000');
});
it('writes a non-ASCII marking with RFC 2231, never an encoded word, folded under 78 columns', () => {
const value = sioLabelValue(vsnfd);
expect(value).toContain("marking*=utf-8''VS-NUR%20F%C3%9CR%20DEN%20DIENSTGEBRAUCH;");
expect(value).not.toContain('=?');
for (const line of `SIO-Label:${value}`.split('\r\n')) expect(line.length).toBeLessThanOrEqual(78);
expect(parseSioLabelMarking(value)).toBe('VS-NUR FÜR DEN DIENSTGEBRAUCH');
});
it('continues a long encoded marking over several parameters and reads it back', () => {
const long = { ...vsnfd, marking: 'ÄÖÜ äöü ß – sehr lange Kennzeichnung für einen Test der Fortsetzung' };
const value = sioLabelValue(long);
expect(value).toMatch(/marking\*0\*=utf-8''/);
expect(value).toMatch(/marking\*1\*=/);
expect(value).not.toMatch(/%[0-9A-F]?(;|\r|$)/);
expect(parseSioLabelMarking(value)).toBe(long.marking);
});
it('reads the forms servers hand back: raw, refolded, a token, RFC 2047 inside quotes', () => {
expect(parseSioLabelMarking(' marking="INTERNAL"; fgcolor=#ffffff; bgcolor=#2563eb')).toBe('INTERNAL');
expect(parseSioLabelMarking(' marking="STRICTLY\r\n CONFIDENTIAL"; bgcolor=red')).toBe('STRICTLY CONFIDENTIAL');
expect(parseSioLabelMarking('marking=PUBLIC;fgcolor=black')).toBe('PUBLIC');
expect(parseSioLabelMarking(' marking="=?UTF-8?B?VlMtTlVSIEbDnFIgREVOIERJRU5TVEdFQlJBVUNI?="')).toBe('VS-NUR FÜR DEN DIENSTGEBRAUCH');
expect(parseSioLabelMarking(' type=":ess"; label="MQYGASkCAQM="')).toBeNull();
expect(parseSioLabelMarking(undefined)).toBeNull();
});
it('writes Sensitivity and the extra header only when the label sets them', () => {
expect(labelHeaderProperties(confidential, 'X-Classification')).toEqual({
// Without the leading space: the server writes "SIO-Label: " itself.
'header:SIO-Label:asRaw': 'marking="CONFIDENTIAL"; fgcolor=#000000; bgcolor=#d97706',
'header:Sensitivity:asText': 'Company-Confidential',
'header:X-Classification:asText': 'Confidential',
});
expect(Object.keys(labelHeaderProperties(byId(generic.list, 'public'), ''))).toEqual(['header:SIO-Label:asRaw']);
expect(labelHeaderLines(vsnfd, 'X-Classification')).toEqual([
`SIO-Label:${sioLabelValue(vsnfd)}`,
'Sensitivity: Company-Confidential',
'X-Classification: VS-NfD',
]);
expect(labelHeaderLines({ ...vsnfd, extraHeaderValue: 'Geheim für alle' }, 'X-Class')[2]).toMatch(/^X-Class: =\?UTF-8\?B\?/);
});
});
describe('received labels', () => {
const policy = { list: generic.list, defaultId: 'internal', required: false, extraHeader: 'X-Classification' };
it('are matched by marking first, case and spacing aside', () => {
expect(receivedLabel({ 'SIO-Label': ' marking="confidential "' }, policy)?.id).toBe('confidential');
expect(receivedLabel({ 'sio-label': [' marking="PUBLIC"'] }, policy)?.id).toBe('public');
});
it('then by the extra header value', () => {
expect(receivedLabel({ 'X-Classification': ' Strictly confidential' }, policy)?.id).toBe('strictly-confidential');
});
it('show nothing for an unknown marking or without labels', () => {
expect(receivedLabel({ 'SIO-Label': ' marking="TOP SECRET"' }, policy)).toBeNull();
expect(receivedLabel({ 'SIO-Label': ' marking="PUBLIC"' }, { ...policy, list: [] })).toBeNull();
});
});
describe('the subject prefix', () => {
const list = generic.list;
const strictly = byId(list, 'strictly-confidential');
it('is added once', () => {
expect(applyLabelPrefix('Budget', confidential, list)).toBe('[Confidential] Budget');
expect(applyLabelPrefix('[Confidential] Budget', confidential, list)).toBe('[Confidential] Budget');
});
it('is not doubled across reply and forward chains', () => {
expect(applyLabelPrefix('Re: [Confidential] Budget', confidential, list)).toBe('[Confidential] Re: Budget');
expect(applyLabelPrefix('AW: WG: [Confidential] Re: [Confidential] Budget', confidential, list)).toBe('[Confidential] AW: WG: Re: Budget');
});
it('changes with the label, and goes with no label', () => {
expect(applyLabelPrefix('Re: [Confidential] Budget', strictly, list)).toBe('[Strictly confidential] Re: Budget');
expect(applyLabelPrefix('[Confidential] Re: Budget', null, list)).toBe('Re: Budget');
expect(applyLabelPrefix('Re: Budget', byId(list, 'internal'), list)).toBe('Re: Budget');
});
it('knows prefixes without brackets, like VS-NfD', () => {
expect(applyLabelPrefix('AW: VS-NfD Lagebericht', vsnfd, german.list)).toBe('VS-NfD AW: Lagebericht');
expect(applyLabelPrefix('VS-NfD: Lagebericht', vsnfd, german.list)).toBe('VS-NfD Lagebericht');
// A word that only starts like a prefix stays.
expect(applyLabelPrefix('VS-NfDx test', vsnfd, german.list)).toBe('VS-NfD VS-NfDx test');
});
});
describe('drafts', () => {
it('keep the pick, "none" and nothing apart', () => {
expect(draftLabelProperty('confidential')).toEqual({ 'header:X-Bulwark-Label:asText': 'confidential' });
expect(draftLabelProperty(null)).toEqual({ 'header:X-Bulwark-Label:asText': 'none' });
expect(draftLabelProperty(undefined)).toEqual({});
expect(draftLabelChoice({ 'X-Bulwark-Label': ' confidential' })).toBe('confidential');
expect(draftLabelChoice({ 'x-bulwark-label': 'none' })).toBeNull();
expect(draftLabelChoice({})).toBeUndefined();
});
});