Files
0pt/tests/connectors/migration-mapping.test.ts
admin 23d05d9379
CI (SIAX Cloud) / integration (push) Has been skipped
masterplan-lock / check (push) Successful in 4s
masterplan-lock / check (pull_request) Successful in 5s
CI (SIAX Cloud) / security (pull_request) Successful in 15s
Security Scan (SIAX Cloud) / security (push) Successful in 23s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 18s
CI (SIAX Cloud) / integration (pull_request) Successful in 26s
CI (SIAX Cloud) / security (push) Successful in 39s
CI (SIAX Cloud) / quality (pull_request) Successful in 36s
CI (SIAX Cloud) / quality (push) Successful in 1m7s
feat(HSP-082): migration-mapping — decideEntityMerge (T01 identiska namn EJ ihopslagna utan bevis), transformRecord (T02 åäö byte-exakt + UTC-normalisering med original-offset), aclBlockingCheck (T03 saknad ACL-mappning BLOCKERANDE), 10 tester
2026-09-22 15:37:35 +02:00

107 lines
4.4 KiB
TypeScript

/**
* `migration-mapping.test.ts` — HSP-082 acceptance:
* T01: identical names are NOT merged without evidence.
* T02: Swedish characters + timezones are preserved (UTF-8 passthrough,
* UTC normalization with the original offset recorded).
* T03: a missing ACL mapping is BLOCKING for protected objects.
*/
import { describe, expect, it } from 'vitest';
import {
decideEntityMerge,
transformRecord,
aclBlockingCheck,
type MappingEntity,
type ProtectedObjectId,
} from '../../src/connectors/migration-mapping';
describe('HSP-082-T01: identiska namn slås inte ihop utan bevis', () => {
it('two entities with the SAME name and NO evidence are both BLOCKED pending proof', () => {
const decisions = decideEntityMerge([
{ sourceId: 's1', targetId: 't1', displayName: 'Support' },
{ sourceId: 's2', targetId: 't2', displayName: 'Support' },
]);
for (const d of decisions) {
expect(d.decision).toBe('blocked_pending_evidence');
expect(d.reason).toContain('HSP-082-T01');
}
});
it('entities WITH evidence (same_external_id) merge cleanly even in a name collision', () => {
const decisions = decideEntityMerge([
{ sourceId: 's1', targetId: 't1', displayName: 'Support', identityEvidence: 'same_external_id' },
{ sourceId: 's2', targetId: 't2', displayName: 'Support' },
]);
const merged = decisions.find((d) => d.sourceId === 's1')!;
expect(merged.decision).toBe('merged');
expect(merged.reason).toContain('same_external_id');
});
it('a UNIQUE name merges without extra evidence', () => {
const decisions = decideEntityMerge([
{ sourceId: 's1', targetId: 't1', displayName: 'Unik' },
]);
expect(decisions[0]!.decision).toBe('merged');
});
it('case-insensitive name collision is also caught (Support vs support)', () => {
const decisions = decideEntityMerge([
{ sourceId: 's1', targetId: 't1', displayName: 'Support' },
{ sourceId: 's2', targetId: 't2', displayName: 'support' },
]);
expect(decisions.every((d) => d.decision === 'blocked_pending_evidence')).toBe(true);
});
});
describe('HSP-082-T02: svenska tecken och tidszoner bevaras', () => {
it('Swedish characters pass through byte-exactly (no normalization/decomposition)', () => {
const result = transformRecord({ name: 'Blåbärsvägen 32 — Göteborg, ÅÄÖåäö' });
expect(result.record['name']).toBe('Blåbärsvägen 32 — Göteborg, ÅÄÖåäö');
expect((result.record['name'] as string).includes('ÅÄÖ')).toBe(true);
expect((result.record['name'] as string).includes('åäö')).toBe(true);
});
it('timestamps normalize to UTC with the ORIGINAL offset recorded', () => {
const result = transformRecord({ bookedAt: '2026-09-22T14:30:00+02:00' });
expect(result.timestamps).toHaveLength(1);
const t = result.timestamps[0]!;
expect(t.original).toBe('2026-09-22T14:30:00+02:00');
expect(t.originalOffset).toBe('+02:00');
expect(t.normalized).toBe('2026-09-22T12:30:00.000Z');
expect(result.record['bookedAt']).toBe('2026-09-22T12:30:00.000Z');
});
it('Z-suffixed timestamps record their (no) offset explicitly', () => {
const result = transformRecord({ at: '2026-09-22T12:00:00Z' });
expect(result.timestamps[0]!.originalOffset).toBe('Z');
});
it('nested timestamps are also normalized and recorded', () => {
const result = transformRecord({ order: { createdAt: '2026-01-01T00:00:00-05:00' } });
expect(result.timestamps).toHaveLength(1);
expect(result.timestamps[0]!.normalized).toBe('2026-01-01T05:00:00.000Z');
});
});
describe('HSP-082-T03: saknad ACL-mappning är blockerande för berört skyddat objekt', () => {
const objects: ProtectedObjectId[] = [
{ objectId: 'obj_protected', protected: true },
{ objectId: 'obj_public', protected: false },
];
it('a protected object WITHOUT an ACL mapping is BLOCKED; unprotected proceeds', () => {
const result = aclBlockingCheck(objects, []);
expect(result.proceed).toEqual(['obj_public']);
expect(result.blocked[0]!.objectId).toBe('obj_protected');
expect(result.blocked[0]!.reason).toContain('BLOCKERANDE');
});
it('a protected object WITH a mapping proceeds', () => {
const result = aclBlockingCheck(
objects,
[{ targetObjectId: 'obj_protected', entries: [{ subjectId: 's1', permission: 'read' }] }],
);
expect(result.blocked).toEqual([]);
expect(result.proceed).toContain('obj_protected');
});
});