236 lines
7.5 KiB
JavaScript
236 lines
7.5 KiB
JavaScript
const { pointAtDevDb } = require('./setup');
|
|
pointAtDevDb();
|
|
process.env.ENABLE_LICENSING_IN_TESTS = 'true';
|
|
|
|
const crypto = require('crypto');
|
|
const express = require('express');
|
|
const request = require('supertest');
|
|
const licensingRouter = require('../src/controllers/licensing.controller');
|
|
const LicenseCache = require('../src/services/LicenseCacheService');
|
|
|
|
const { privateKey: TEST_PRIV_KEY, publicKey: TEST_PUB_KEY } = crypto.generateKeyPairSync('ed25519', {
|
|
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
|
|
publicKeyEncoding: { type: 'spki', format: 'pem' }
|
|
});
|
|
|
|
const { privateKey: OTHER_PRIV_KEY } = crypto.generateKeyPairSync('ed25519', {
|
|
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
|
|
publicKeyEncoding: { type: 'spki', format: 'pem' }
|
|
});
|
|
|
|
const TENANT_ID = '5b39bcb5-506a-456d-b7cb-91730b397cc1';
|
|
|
|
function createSignedJwt(claims, privKey = TEST_PRIV_KEY) {
|
|
const headerB64 = Buffer.from(JSON.stringify({ alg: 'EdDSA', typ: 'JWT' })).toString('base64url');
|
|
const payloadB64 = Buffer.from(JSON.stringify(claims)).toString('base64url');
|
|
const data = Buffer.from(`${headerB64}.${payloadB64}`, 'utf8');
|
|
const sigB64 = crypto.sign(null, data, privKey).toString('base64url');
|
|
return `${headerB64}.${payloadB64}.${sigB64}`;
|
|
}
|
|
|
|
describe('Offline License Artifact Ingestion Endpoint (POST /api/license/ingest)', () => {
|
|
let app;
|
|
let originalPubKey;
|
|
|
|
beforeEach(async () => {
|
|
originalPubKey = process.env.SUPERADMIN_PUBLIC_KEY;
|
|
process.env.SUPERADMIN_PUBLIC_KEY = TEST_PUB_KEY;
|
|
await LicenseCache.del(TENANT_ID);
|
|
|
|
app = express();
|
|
app.use(express.json());
|
|
app.use('/api/license', licensingRouter);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
process.env.SUPERADMIN_PUBLIC_KEY = originalPubKey;
|
|
await LicenseCache.del(TENANT_ID);
|
|
});
|
|
|
|
it('successfully verifies and ingests a valid signed .lic.json artifact', async () => {
|
|
const claims = {
|
|
iss: 'super_admin_licensing_engine',
|
|
sub: TENANT_ID,
|
|
tenant_code: 'SCH-TEST',
|
|
tenant_name: 'AFRICA-ALERT',
|
|
term: 'Term 3 Offline',
|
|
academic_year: '2026',
|
|
max_students: 2500,
|
|
max_staff: 350,
|
|
nbf: Math.floor(Date.now() / 1000) - 100,
|
|
exp: Math.floor(Date.now() / 1000) + 86400 * 90,
|
|
status: 'active'
|
|
};
|
|
const signedToken = createSignedJwt(claims);
|
|
|
|
const artifact = {
|
|
format_version: '1.0',
|
|
issued_at: new Date().toISOString(),
|
|
issued_by: 'Nextgen LMS SuperAdmin Governance Portal',
|
|
tenant: {
|
|
id: TENANT_ID,
|
|
code: 'SCH-TEST',
|
|
name: 'AFRICA-ALERT'
|
|
},
|
|
license: {
|
|
id: 'lic-offline-test-101',
|
|
license_key: 'LIC-TERM3OFFLINE-2026-TEST',
|
|
signed_license_token: signedToken,
|
|
algorithm: 'EdDSA',
|
|
term_name: 'Term 3 Offline',
|
|
academic_year: '2026',
|
|
start_date: '2026-07-01',
|
|
end_date: '2026-10-01',
|
|
grace_period_days: 7,
|
|
max_students: 2500,
|
|
max_staff: 350,
|
|
status: 'active'
|
|
},
|
|
checksum: crypto.createHash('sha256').update(signedToken).digest('hex')
|
|
};
|
|
|
|
const res = await request(app)
|
|
.post('/api/license/ingest')
|
|
.set('x-tenant-id', TENANT_ID)
|
|
.send(artifact);
|
|
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.ok).toBe(true);
|
|
expect(res.body.status).toBe('ACTIVE');
|
|
expect(res.body.license.license_key).toBe('LIC-TERM3OFFLINE-2026-TEST');
|
|
expect(res.body.license.term_name).toBe('Term 3 Offline');
|
|
});
|
|
|
|
it('rejects an artifact signed with an untrusted / invalid key', async () => {
|
|
const claims = {
|
|
iss: 'fake_super_admin',
|
|
sub: TENANT_ID,
|
|
term: 'Term Forged',
|
|
exp: Math.floor(Date.now() / 1000) + 86400 * 90
|
|
};
|
|
const forgedToken = createSignedJwt(claims, OTHER_PRIV_KEY);
|
|
|
|
const forgedArtifact = {
|
|
format_version: '1.0',
|
|
license: {
|
|
signed_license_token: forgedToken,
|
|
term_name: 'Term Forged'
|
|
}
|
|
};
|
|
|
|
const res = await request(app)
|
|
.post('/api/license/ingest')
|
|
.set('x-tenant-id', TENANT_ID)
|
|
.send(forgedArtifact);
|
|
|
|
expect(res.status).toBe(422);
|
|
expect(res.body.error).toMatch(/Cryptographic Signature Verification Failed/i);
|
|
});
|
|
|
|
it('successfully decrypts and ingests an AES-256-GCM encrypted license artifact package', async () => {
|
|
const claims = {
|
|
iss: 'super_admin_licensing_engine',
|
|
sub: TENANT_ID,
|
|
tenant_code: 'SCH-ENC',
|
|
tenant_name: 'AFRICA-ALERT',
|
|
term: 'Term 1 Encrypted',
|
|
academic_year: '2026',
|
|
max_students: 5000,
|
|
max_staff: 400,
|
|
exp: Math.floor(Date.now() / 1000) + 86400 * 90,
|
|
status: 'active'
|
|
};
|
|
const token = createSignedJwt(claims);
|
|
|
|
const rawArtifact = {
|
|
tenant: { id: TENANT_ID, code: 'SCH-ENC', name: 'AFRICA-ALERT' },
|
|
license: {
|
|
id: 'lic-enc-001',
|
|
license_key: 'LIC-ENC-2026',
|
|
signed_license_token: token,
|
|
term_name: 'Term 1 Encrypted',
|
|
academic_year: '2026',
|
|
start_date: '2026-07-01',
|
|
end_date: '2026-10-01',
|
|
max_students: 5000,
|
|
max_staff: 400,
|
|
status: 'active'
|
|
}
|
|
};
|
|
|
|
const secret = process.env.LICENSE_ARTIFACT_SECRET || TEST_PUB_KEY;
|
|
const key = crypto.createHash('sha256').update(`${secret}:${TENANT_ID}`).digest();
|
|
const iv = crypto.randomBytes(12);
|
|
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
const encrypted = Buffer.concat([cipher.update(JSON.stringify(rawArtifact), 'utf8'), cipher.final()]);
|
|
const authTag = cipher.getAuthTag();
|
|
|
|
const encryptedEnvelope = {
|
|
format_version: '2.0',
|
|
encrypted: true,
|
|
algorithm: 'AES-256-GCM + Ed25519',
|
|
tenant_id: TENANT_ID,
|
|
iv: iv.toString('hex'),
|
|
auth_tag: authTag.toString('hex'),
|
|
ciphertext: encrypted.toString('base64')
|
|
};
|
|
|
|
const res = await request(app)
|
|
.post('/api/license/ingest')
|
|
.set('x-tenant-id', TENANT_ID)
|
|
.send(encryptedEnvelope);
|
|
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.ok).toBe(true);
|
|
expect(res.body.license.term_name).toBe('Term 1 Encrypted');
|
|
});
|
|
|
|
it('rejects tampered ciphertext in an encrypted license artifact package', async () => {
|
|
const encryptedEnvelope = {
|
|
format_version: '2.0',
|
|
encrypted: true,
|
|
algorithm: 'AES-256-GCM + Ed25519',
|
|
tenant_id: TENANT_ID,
|
|
iv: crypto.randomBytes(12).toString('hex'),
|
|
auth_tag: crypto.randomBytes(16).toString('hex'),
|
|
ciphertext: Buffer.from('TAMPERED_CIPHERTEXT_BYTES').toString('base64')
|
|
};
|
|
|
|
const res = await request(app)
|
|
.post('/api/license/ingest')
|
|
.set('x-tenant-id', TENANT_ID)
|
|
.send(encryptedEnvelope);
|
|
|
|
expect(res.status).toBe(422);
|
|
expect(res.body.error).toMatch(/Artifact Decryption \/ Tamper Check Failed/i);
|
|
});
|
|
|
|
it('rejects artifact when school name does not match local school identity', async () => {
|
|
const claims = {
|
|
iss: 'super_admin_licensing_engine',
|
|
sub: TENANT_ID,
|
|
tenant_name: 'Completely Different School Academy',
|
|
term: 'Term 1',
|
|
academic_year: '2026',
|
|
exp: Math.floor(Date.now() / 1000) + 86400 * 90
|
|
};
|
|
const token = createSignedJwt(claims);
|
|
|
|
const artifact = {
|
|
tenant: { id: TENANT_ID, name: 'Completely Different School Academy' },
|
|
license: {
|
|
id: 'lic-mismatch-001',
|
|
signed_license_token: token
|
|
}
|
|
};
|
|
|
|
const res = await request(app)
|
|
.post('/api/license/ingest')
|
|
.set('x-tenant-id', TENANT_ID)
|
|
.send(artifact);
|
|
|
|
expect(res.status).toBe(422);
|
|
expect(res.body.error).toMatch(/Institution Name Mismatch/i);
|
|
});
|
|
});
|