Files
0pt/tests/connectors/migration-studio.test.ts
T
admin 655a71ebd7
CI (SIAX Cloud) / integration (push) Has been skipped
masterplan-lock / check (push) Successful in 7s
masterplan-lock / check (pull_request) Successful in 5s
Security Scan (SIAX Cloud) / security (push) Successful in 15s
CI (SIAX Cloud) / security (push) Successful in 17s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 13s
CI (SIAX Cloud) / security (pull_request) Successful in 15s
CI (SIAX Cloud) / quality (push) Successful in 36s
CI (SIAX Cloud) / quality (pull_request) Successful in 37s
CI (SIAX Cloud) / integration (pull_request) Successful in 43s
feat(HSP-085): migration-studio — T01 ej importerbart döljs EJ ur nämnaren (ratio = importable/total), T02 session-resume med tenant-check + checkpoint + resolved conflicts, T03 coverage-claim SCOPED (aldrig komplett M365-ersättning), 8 tester
2026-09-22 22:18:01 +02:00

99 lines
4.1 KiB
TypeScript

/**
* `migration-studio.test.ts` — HSP-085 acceptance:
* T01: an EJ importerbart objekt döljs inte ur nämnaren.
* T02: en uppgift kan återupptas efter sessionbyte.
* T03: en importerad CRM-lista marknadsförs inte som komplett M365-ersättning.
* Runner: vitest (0pt-konventionen).
*/
import { describe, expect, it } from 'vitest';
import {
computeImportRatio,
resumeMigration,
buildCoverageClaim,
type MigrationSourceObject,
type MigrationSession,
} from '../../src/connectors/migration-studio';
function sourceSet(): MigrationSourceObject[] {
return [
{ objectId: 'obj_1', name: 'CRM-lista: kunder', importable: true, tenantId: 't_1' },
{ objectId: 'obj_2', name: 'CRM-lista: leads', importable: true, tenantId: 't_1' },
{ objectId: 'obj_3', name: 'M365 mail-box', importable: false, nonImportableReason: 'mail är utanför migrationsscopet (p0st äger mail)', tenantId: 't_1' },
{ objectId: 'obj_4', name: 'Cross-tenant doc', importable: false, nonImportableReason: 'annan tenant', tenantId: 't_other' },
];
}
describe('HSP-085-T01: ej importerbart objekt döljs inte ur nämnaren', () => {
it('the ratio counts EVERY source object in the denominator (non-importable stays)', () => {
const ratio = computeImportRatio(sourceSet());
expect(ratio.totalSourceObjects).toBe(4);
expect(ratio.importableCount).toBe(2);
expect(ratio.nonImportableCount).toBe(2);
// 2/4 = 0.5 — NOT 2/2 = 1.0 (which would be hiding the failures).
expect(ratio.ratio).toBe(0.5);
});
it('the non-importable objects are ALWAYS listed with their reasons', () => {
const ratio = computeImportRatio(sourceSet());
expect(ratio.nonImportable).toHaveLength(2);
expect(ratio.nonImportable.some((n) => n.reason.includes('mail'))).toBe(true);
expect(ratio.nonImportable.some((n) => n.reason.includes('annan tenant'))).toBe(true);
});
it('a fully-importable source gives ratio 1.0 (honest, no non-importable to hide)', () => {
const ratio = computeImportRatio([
{ objectId: 'a', name: 'a', importable: true, tenantId: 't' },
]);
expect(ratio.ratio).toBe(1);
expect(ratio.nonImportable).toEqual([]);
});
});
describe('HSP-085-T02: uppgift kan återupptas efter sessionbyte', () => {
const stored: MigrationSession = {
migrationId: 'mig_1',
tenantId: 't_1',
checkpointCursor: 'batch_3',
resolvedConflicts: ['conflict_a', 'conflict_b'],
state: 'conflict_review',
};
it('a session change RESUMES from the stored checkpoint + resolved conflicts', () => {
const result = resumeMigration(stored, 't_1');
expect(result.state).toBe('resumed');
expect(result.checkpointCursor).toBe('batch_3');
expect(result.resolvedConflicts).toEqual(['conflict_a', 'conflict_b']);
});
it('a session change to a DIFFERENT tenant starts FRESH (no cross-tenant resume)', () => {
const result = resumeMigration(stored, 't_other');
expect(result.state).toBe('fresh');
expect(result.checkpointCursor).toBe('0');
});
it('no stored session starts fresh', () => {
const result = resumeMigration(undefined, 't_1');
expect(result.state).toBe('fresh');
expect(result.checkpointCursor).toBe('0');
});
});
describe('HSP-085-T03: CRM-lista marknadsförs inte som komplett M365-ersättning', () => {
it('the coverage claim is SCOPED to what was imported (never a generic total)', () => {
const ratio = computeImportRatio(sourceSet());
const claim = buildCoverageClaim('CRM-listor', ratio);
expect(claim.scope).toContain('CRM-listor');
expect(claim.scope).toContain('2 av 4');
expect(claim.scope).toContain('ej importerbarta');
// NEVER claims a generic total badge.
expect(claim.scope.toLowerCase()).not.toContain('complete');
expect(claim.scope.toLowerCase()).not.toContain('komplett microsoft');
});
it('the claim carries the NON-IMPORTABLE count (the honest denominator)', () => {
const ratio = computeImportRatio(sourceSet());
const claim = buildCoverageClaim('CRM-listor', ratio);
expect(claim.nonImportableCount).toBe(2);
expect(claim.totalSourceObjects).toBe(4);
});
});