fase(17): licensing module with RSA validation

This commit is contained in:
debian
2026-03-08 05:20:54 -04:00
parent 1f1678af17
commit 5a28270dc9
45 changed files with 1789 additions and 48 deletions

View File

@@ -0,0 +1,195 @@
import { LicensePlan } from '../../src/modules/licensing/domain/value-objects/LicensePlan';
import {
FeatureEntitlement,
FREE_FEATURES,
PRO_FEATURES,
ENTERPRISE_FEATURES,
} from '../../src/modules/licensing/domain/value-objects/FeatureEntitlement';
import { License } from '../../src/modules/licensing/domain/entities/License';
import { LicenseService } from '../../src/modules/licensing/application/LicenseService';
import { ILicenseValidator } from '../../src/modules/licensing/domain/ports/ILicenseValidator';
import { Result, Ok, Err } from '../../src/shared/domain/Result';
import { UniqueId } from '../../src/shared/domain/UniqueId';
// ─── LicensePlan ─────────────────────────────────────────────────────────────
describe('LicensePlan', () => {
it('creates free plan', () => {
const plan = LicensePlan.free();
expect(plan.isFree).toBe(true);
expect(plan.isPro).toBe(false);
expect(plan.isEnterprise).toBe(false);
expect(plan.toString()).toBe('free');
});
it('creates pro plan', () => {
const plan = LicensePlan.pro();
expect(plan.isPro).toBe(true);
expect(plan.isFree).toBe(false);
});
it('creates enterprise plan', () => {
const plan = LicensePlan.enterprise();
expect(plan.isEnterprise).toBe(true);
});
it('parses from string', () => {
expect(LicensePlan.fromString('pro').isPro).toBe(true);
expect(LicensePlan.fromString('enterprise').isEnterprise).toBe(true);
});
it('throws on invalid plan string', () => {
expect(() => LicensePlan.fromString('invalid')).toThrow();
});
it('equals comparison works', () => {
expect(LicensePlan.free().equals(LicensePlan.free())).toBe(true);
expect(LicensePlan.free().equals(LicensePlan.pro())).toBe(false);
});
});
// ─── FeatureEntitlement ───────────────────────────────────────────────────────
describe('FeatureEntitlement', () => {
it('free features do not include pro features', () => {
const free = FeatureEntitlement.forFeatures(FREE_FEATURES);
expect(free.has('exploration:basic')).toBe(true);
expect(free.has('reports:pdf')).toBe(false);
expect(free.has('integrations:slack')).toBe(false);
expect(free.has('auth:sso')).toBe(false);
});
it('pro features include free features', () => {
const pro = FeatureEntitlement.forFeatures(PRO_FEATURES);
expect(pro.has('exploration:basic')).toBe(true);
expect(pro.has('reports:pdf')).toBe(true);
expect(pro.has('integrations:slack')).toBe(true);
expect(pro.has('auth:sso')).toBe(false);
});
it('enterprise features include all features', () => {
const ent = FeatureEntitlement.forFeatures(ENTERPRISE_FEATURES);
expect(ent.has('auth:sso')).toBe(true);
expect(ent.has('auth:ldap')).toBe(true);
expect(ent.has('branding:whitelabel')).toBe(true);
});
});
// ─── License entity ──────────────────────────────────────────────────────────
describe('License entity', () => {
it('createFree returns a free plan license', () => {
const license = License.createFree();
expect(license.plan.isFree).toBe(true);
expect(license.isExpired).toBe(false);
expect(license.isValid).toBe(true);
});
it('free license has only free features', () => {
const license = License.createFree();
expect(license.hasFeature('exploration:basic')).toBe(true);
expect(license.hasFeature('reports:pdf')).toBe(false);
});
it('expired license falls back to free features', () => {
const license = License.reconstitute(
{
plan: LicensePlan.pro(),
organizationName: 'Test',
email: 'test@test.com',
expiresAt: new Date('2020-01-01'),
issuedAt: new Date('2019-01-01'),
signature: 'sig',
rawKey: 'key',
},
UniqueId.create()
);
expect(license.isExpired).toBe(true);
expect(license.isValid).toBe(false);
expect(license.hasFeature('reports:pdf')).toBe(false);
});
it('pro license has pro features', () => {
const license = License.reconstitute(
{
plan: LicensePlan.pro(),
organizationName: 'Acme',
email: 'admin@acme.com',
expiresAt: null,
issuedAt: new Date(),
signature: 'sig',
rawKey: 'key',
},
UniqueId.create()
);
expect(license.isValid).toBe(true);
expect(license.hasFeature('reports:pdf')).toBe(true);
expect(license.hasFeature('integrations:slack')).toBe(true);
expect(license.hasFeature('auth:sso')).toBe(false);
});
});
// ─── LicenseService ───────────────────────────────────────────────────────────
describe('LicenseService', () => {
function makeProLicense(): License {
return License.reconstitute(
{
plan: LicensePlan.pro(),
organizationName: 'Acme',
email: 'admin@acme.com',
expiresAt: null,
issuedAt: new Date(),
signature: 'sig',
rawKey: 'key',
},
UniqueId.create()
);
}
it('starts with free license', () => {
const validator: ILicenseValidator = {
validate: async () => Err('should not be called'),
};
const service = new LicenseService(validator);
const status = service.getStatus();
expect(status.plan).toBe('free');
expect(status.isValid).toBe(true);
});
it('activate with valid key updates current license', async () => {
const proLicense = makeProLicense();
const validator: ILicenseValidator = {
validate: async () => Ok(proLicense),
};
const service = new LicenseService(validator);
const result = await service.activate('valid-key');
expect(result.ok).toBe(true);
expect(service.getStatus().plan).toBe('pro');
expect(service.hasFeature('reports:pdf')).toBe(true);
});
it('activate with invalid key returns error and keeps free license', async () => {
const validator: ILicenseValidator = {
validate: async () => Err('Invalid license key: signature verification failed'),
};
const service = new LicenseService(validator);
const result = await service.activate('invalid-key');
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error).toContain('signature verification failed');
}
expect(service.getStatus().plan).toBe('free');
});
it('hasFeature checks current license entitlements', () => {
const validator: ILicenseValidator = {
validate: async () => Err('unused'),
};
const service = new LicenseService(validator);
expect(service.hasFeature('exploration:basic')).toBe(true);
expect(service.hasFeature('auth:sso')).toBe(false);
});
});