test: migrate _test tests to vitest tests

This commit is contained in:
KernelDeimos
2025-11-26 16:32:07 -05:00
committed by Eric Dubé
parent cb66c56a6b
commit eda1c45ffb
13 changed files with 350 additions and 236 deletions
+3 -43
View File
@@ -1,3 +1,5 @@
const BaseService = require("../services/BaseService");
/*
* Copyright (C) 2024-present Puter Technologies Inc.
*
@@ -16,7 +18,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
class ArrayUtil extends use.Library {
class ArrayUtil extends (globalThis.use?.Library ?? BaseService) {
/**
*
* @param {*} marked_map
@@ -46,48 +48,6 @@ class ArrayUtil extends use.Library {
return subject;
}
_test ({ assert }) {
// inner indices
{
const subject = [
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h',
];
// 0 1 2 3 4 5 6 7
const marked_map = [2, 5];
this.remove_marked_items(marked_map, subject);
assert(() => subject.join('') === 'abdegh');
}
// left edge
{
const subject = [
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h',
];
// 0 1 2 3 4 5 6 7
const marked_map = [0];
this.remove_marked_items(marked_map, subject);
assert(() => subject.join('') === 'bcdefgh');
}
// right edge
{
const subject = [
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h',
];
// 0 1 2 3 4 5 6 7
const marked_map = [7];
this.remove_marked_items(marked_map, subject);
assert(() => subject.join('') === 'abcdefg');
}
// both edges
{
const subject = [
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h',
];
// 0 1 2 3 4 5 6 7
const marked_map = [0, 7];
this.remove_marked_items(marked_map, subject);
assert(() => subject.join('') === 'bcdefg');
}
}
}
module.exports = ArrayUtil;
@@ -0,0 +1,56 @@
import { describe, expect, it } from 'vitest';
import { createTestKernel } from '../../tools/test.mjs';
import ArrayUtil from './ArrayUtil.js';
describe('ArrayUtil', () => {
it('should remove marked items correctly', async () => {
const testKernel = await createTestKernel({
serviceMap: {
arrayUtil: ArrayUtil,
},
});
const arrayUtil = testKernel.services?.get('arrayUtil');
// inner indices
{
const subject = [
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h',
];
// 0 1 2 3 4 5 6 7
const marked_map = [2, 5];
arrayUtil.remove_marked_items(marked_map, subject);
expect(subject.join('')).toBe('abdegh');
}
// left edge
{
const subject = [
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h',
];
// 0 1 2 3 4 5 6 7
const marked_map = [0];
arrayUtil.remove_marked_items(marked_map, subject);
expect(subject.join('')).toBe('bcdefgh');
}
// right edge
{
const subject = [
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h',
];
// 0 1 2 3 4 5 6 7
const marked_map = [7];
arrayUtil.remove_marked_items(marked_map, subject);
expect(subject.join('')).toBe('abcdefg');
}
// both edges
{
const subject = [
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h',
];
// 0 1 2 3 4 5 6 7
const marked_map = [0, 7];
arrayUtil.remove_marked_items(marked_map, subject);
expect(subject.join('')).toBe('bcdefg');
}
});
});
@@ -173,27 +173,6 @@ class CleanEmailService extends BaseService {
return true;
}
_test ({ assert }) {
const cases = [
{
email: 'bob.ross+happy-clouds@googlemail.com',
expected: 'bobross@gmail.com',
},
{
email: 'under.rated+email-service@yahoo.com',
expected: 'under.rated+email-service@yahoo.com',
},
{
email: 'the-absolute+best@protonmail.com',
expected: 'the-absolute@protonmail.com',
},
];
for ( const { email, expected } of cases ) {
const cleaned = this.clean(email);
assert.equal(cleaned, expected, `clean_email(${email}) === ${expected}`);
}
}
}
module.exports = { CleanEmailService };
@@ -0,0 +1,35 @@
import { describe, expect, it } from 'vitest';
import { createTestKernel } from '../../tools/test.mjs';
import { CleanEmailService } from './CleanEmailService.js';
describe('CleanEmailService', () => {
it('should clean email addresses correctly', async () => {
const testKernel = await createTestKernel({
serviceMap: {
'clean-email': CleanEmailService,
},
});
const cleanEmailService = testKernel.services!.get('clean-email') as CleanEmailService;
const cases = [
{
email: 'bob.ross+happy-clouds@googlemail.com',
expected: 'bobross@gmail.com',
},
{
email: 'under.rated+email-service@yahoo.com',
expected: 'under.rated+email-service@yahoo.com',
},
{
email: 'the-absolute+best@protonmail.com',
expected: 'the-absolute@protonmail.com',
},
];
for ( const { email, expected } of cases ) {
const cleaned = cleanEmailService.clean(email);
expect(cleaned).toBe(expected);
}
});
});
-25
View File
@@ -205,31 +205,6 @@ class SNSService extends BaseService {
return cert;
}
async _test ({ assert }) {
// This test case doesn't work because the specified signing cert
// from SNS is no longer served.
// const result = await this.verify_message_({
// Type: 'Notification',
// MessageId: '4c807a89-9ef9-543b-bfab-2f4ed41e91b4',
// TopicArn: 'arn:aws:sns:us-east-1:853553028582:marbot-dev-alert-Topic-8CT7ZJRNSA5Y',
// Subject: 'INSUFFICIENT_DATA: "insufficient test" in US East (N. Virginia)',
// Message: '{"AlarmName":"insufficient test","AlarmDescription":null,"AWSAccountId":"process.env.AWS_ACCOUNT_ID","NewStateValue":"INSUFFICIENT_DATA","NewStateReason":"tets","StateChangeTime":"2019-08-09T10:19:19.614+0000","Region":"US East (N. Virginia)","OldStateValue":"OK","Trigger":{"MetricName":"CallCount2","Namespace":"AWS/Usage","StatisticType":"Statistic","Statistic":"AVERAGE","Unit":null,"Dimensions":[{"value":"API","name":"Type"},{"value":"PutMetricData","name":"Resource"},{"value":"CloudWatch","name":"Service"},{"value":"None","name":"Class"}],"Period":300,"EvaluationPeriods":1,"ComparisonOperator":"GreaterThanThreshold","Threshold":1.0,"TreatMissingData":"- TreatMissingData: missing","EvaluateLowSampleCountPercentile":""}}',
// Timestamp: '2019-08-09T10:19:19.644Z',
// SignatureVersion: '1',
// Signature: 'gnCKAUYX6YlBW3dkOmrSFvdB6r82Q2He+7uZV9072sdCP0DSaR46ka/4ymSdDfqilqxjJ9hajd9l7j8ZsL98vYdUbut/1IJ2hsuALF9nd/HwNLPPWvKXaK/Y3Hp57izOpeBAkuR6koitSbXX50lEj7FraaMVQfpexm01z7IUcx4vCCvZBTdQLbkWw+TYWkWNsMrqarW39zy474SmTBCSZlz1eoV6tCwYk2Z2G2awiXpnfsQRRZvHn4ot176oY+ADAFJ0sIa44effQXq+tAWE6/Z3M5rjtfg6OULDM+NGEmnVZL3xyWK8bIzB48ZclQo3ZsvLPGmCNQLlFpaP/3fGGg==',
// SigningCertURL: 'https://sns.us-east-1.amazonaws.com/SimpleNotificationService-6aad65c2f9911b05cd53efda11f913f9.pem',
// UnsubscribeURL: 'https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:853553028582:marbot-dev-alert-Topic-8CT7ZJRNSA5Y:86a160f0-c3c5-4ae1-ae50-2903eede0af1'
// }, { test_mode: true });
// If this example validates, we did something wrong
// assert.equal(result, false, 'does not validate cloudonaut example');
// Uncomment when a mock exists
// {
// const result = await this.verify_message_(TEST_MESSAGE);
// assert.equal(result, true, 'validates working example');
// }
}
}
module.exports = {
@@ -0,0 +1,21 @@
import { describe, expect, it } from 'vitest';
import { createTestKernel } from '../../tools/test.mjs';
import { SNSService } from './SNSService.js';
describe('SNSService', () => {
it('should have empty test (test case commented out)', async () => {
const testKernel = await createTestKernel({
serviceMap: {
'sns': SNSService,
},
});
const snsService = testKernel.services!.get('sns') as SNSService;
// The original test case doesn't work because the specified signing cert
// from SNS is no longer served. The test was commented out in the _test method.
// This test just ensures the service can be constructed and tested.
expect(snsService).toBeInstanceOf(SNSService);
});
});
@@ -180,39 +180,6 @@ class AntiCSRFService extends BaseService {
return require('crypto').randomBytes(32).toString('hex');
}
/**
* Runs unit tests for the AntiCSRFService functionality.
* Tests token generation, expiration, and consumption behavior.
*
* @param {Object} params - Test parameters
* @param {Function} params.assert - Assertion function for testing
*/
_test ({ assert }) {
// Do this several times, like a user would
for ( let i = 0 ; i < 30 ; i++ ) {
// Generate 30 tokens
const tokens = [];
for ( let j = 0 ; j < 30 ; j++ ) {
tokens.push(this.create_token('session'));
}
// Only the last 10 should be valid
const results_for_stale_tokens = [];
for ( let j = 0 ; j < 20 ; j++ ) {
const result = this.consume_token('session', tokens[j]);
results_for_stale_tokens.push(result);
}
assert(() => results_for_stale_tokens.every(v => v === false));
// The last 10 should be valid
const results_for_valid_tokens = [];
for ( let j = 20 ; j < 30 ; j++ ) {
const result = this.consume_token('session', tokens[j]);
results_for_valid_tokens.push(result);
}
assert(() => results_for_valid_tokens.every(v => v === true));
// A completely arbitrary token should not be valid
assert(() => this.consume_token('session', 'arbitrary') === false);
}
}
}
module.exports = {
@@ -0,0 +1,41 @@
import { describe, expect, it } from 'vitest';
import { createTestKernel } from '../../../tools/test.mjs';
import { AntiCSRFService } from './AntiCSRFService.js';
describe('AntiCSRFService', () => {
it('should handle token generation, expiration, and consumption correctly', async () => {
const testKernel = await createTestKernel({
serviceMap: {
'anti-csrf': AntiCSRFService,
},
});
const antiCSRFService = testKernel.services!.get('anti-csrf') as AntiCSRFService;
// Do this several times, like a user would
for ( let i = 0 ; i < 30 ; i++ ) {
// Generate 30 tokens
const tokens = [];
for ( let j = 0 ; j < 30 ; j++ ) {
tokens.push(antiCSRFService.create_token('session'));
}
// Only the last 10 should be valid
const results_for_stale_tokens = [];
for ( let j = 0 ; j < 20 ; j++ ) {
const result = antiCSRFService.consume_token('session', tokens[j]);
results_for_stale_tokens.push(result);
}
expect(results_for_stale_tokens.every(v => v === false)).toBe(true);
// The last 10 should be valid
const results_for_valid_tokens = [];
for ( let j = 20 ; j < 30 ; j++ ) {
const result = antiCSRFService.consume_token('session', tokens[j]);
results_for_valid_tokens.push(result);
}
expect(results_for_valid_tokens.every(v => v === true)).toBe(true);
// A completely arbitrary token should not be valid
expect(antiCSRFService.consume_token('session', 'arbitrary')).toBe(false);
}
});
});
@@ -225,52 +225,6 @@ class TokenService extends BaseService {
return decompressed;
}
_test ({ assert }) {
const U1 = '843f1d83-3c30-48c7-8964-62aff1a912d0';
const U2 = '42e9c36b-8a53-4c3e-8e18-fe549b10a44d';
const U3 = 'app-c22ef816-edb6-47c5-8c41-31c6520fa9e6';
// Test compression
{
const context = this.compression.auth;
const payload = {
uuid: U1,
type: 'session',
user_uid: U2,
app_uid: U3,
};
const compressed = this._compress_payload(context, payload);
assert(() => compressed.u === uuid_compression().encode(U1));
assert(() => compressed.t === 's');
assert(() => compressed.uu === uuid_compression().encode(U2));
assert(() => compressed.au === uuid_compression('app-').encode(U3));
}
// Test decompression
{
const context = this.compression.auth;
const payload = {
u: uuid_compression().encode(U1),
t: 's',
uu: uuid_compression().encode(U2),
au: uuid_compression('app-').encode(U3),
};
const decompressed = this._decompress_payload(context, payload);
assert(() => decompressed.uuid === U1);
assert(() => decompressed.type === 'session');
assert(() => decompressed.user_uid === U2);
assert(() => decompressed.app_uid === U3);
}
// Test UUID preservation
{
const payload = { uuid: U1 };
const compressed = this._compress_payload(this.compression.auth, payload);
const decompressed = this._decompress_payload(this.compression.auth, compressed);
assert(() => decompressed.uuid === U1);
}
}
}
module.exports = { TokenService };
@@ -0,0 +1,94 @@
import { describe, expect, it } from 'vitest';
import { createTestKernel } from '../../../tools/test.mjs';
import { TokenService } from './TokenService.js';
// Helper function to match the uuid_compression logic from TokenService
const uuid_compression = (prefix?: string) => ({
encode: (v: string) => {
if ( prefix ) {
if ( ! v.startsWith(prefix) ) {
throw new Error(`Expected ${prefix} prefix`);
}
v = v.slice(prefix.length);
}
const undecorated = v.replace(/-/g, '');
const base64 = Buffer
.from(undecorated, 'hex')
.toString('base64');
return base64;
},
decode: (v: string) => {
// if already a uuid, return that
if ( v.includes('-') ) return v;
const undecorated = Buffer
.from(v, 'base64')
.toString('hex');
return (prefix ?? '') + [
undecorated.slice(0, 8),
undecorated.slice(8, 12),
undecorated.slice(12, 16),
undecorated.slice(16, 20),
undecorated.slice(20),
].join('-');
},
});
describe('TokenService', () => {
it('should compress and decompress payloads correctly', async () => {
const testKernel = await createTestKernel({
serviceMap: {
'token': TokenService,
},
});
const tokenService = testKernel.services!.get('token') as TokenService;
const U1 = '843f1d83-3c30-48c7-8964-62aff1a912d0';
const U2 = '42e9c36b-8a53-4c3e-8e18-fe549b10a44d';
const U3 = 'app-c22ef816-edb6-47c5-8c41-31c6520fa9e6';
// Test compression
{
const context = tokenService.compression!.auth;
const payload = {
uuid: U1,
type: 'session',
user_uid: U2,
app_uid: U3,
};
const compressed = tokenService._compress_payload(context, payload);
expect(compressed.u).toBe(uuid_compression().encode(U1));
expect(compressed.t).toBe('s');
expect(compressed.uu).toBe(uuid_compression().encode(U2));
expect(compressed.au).toBe(uuid_compression('app-').encode(U3));
}
// Test decompression
{
const context = tokenService.compression!.auth;
const payload = {
u: uuid_compression().encode(U1),
t: 's',
uu: uuid_compression().encode(U2),
au: uuid_compression('app-').encode(U3),
};
const decompressed = tokenService._decompress_payload(context, payload);
expect(decompressed.uuid).toBe(U1);
expect(decompressed.type).toBe('session');
expect(decompressed.user_uid).toBe(U2);
expect(decompressed.app_uid).toBe(U3);
}
// Test UUID preservation
{
const payload = { uuid: U1 };
const compressed = tokenService._compress_payload(tokenService.compression!.auth, payload);
const decompressed = tokenService._decompress_payload(tokenService.compression!.auth, compressed);
expect(decompressed.uuid).toBe(U1);
}
});
});
@@ -465,73 +465,6 @@ class HTTPThumbnailService extends BaseService {
this.constructor.SUPPORTED_MIMETYPES = Object.keys(mime_set);
}
async _test ({ assert }) {
/**
* Runs unit tests for the HTTPThumbnailService.
*
* @param {Object} options - An object containing test options.
* @param {assert} options.assert - An assertion function for making test assertions.
*
* @note This method sets up a testing environment by:
* - Disabling error reporting.
* - Muting logging operations.
* - Setting the service to test mode.
* - Testing the recycling behavior of ThumbnailOperation.
* - Executing thumbnailing jobs in various scenarios to ensure correct behavior.
*/
this.errors.report = () => {
};
// Pseudo-logger to prevent errors from being thrown when this service
// is running under the test kernel.
this.log = {
info: () => {
},
error: () => {
},
noticeme: () => {
},
};
// Thumbnail operation eventually recycles
{
const thop = new ThumbnailOperation(null);
for ( let i = 0 ; i < ThumbnailOperation.MAX_RECYCLE_COUNT ; i++ ) {
/**
* Tests the recycling behavior of ThumbnailOperation.
*
* @param {Object} test - An object containing assertion methods.
* @param {Function} test.assert - Assertion function to check conditions.
*/
assert.equal(thop.recycle(), true, `recycle ${i}`);
}
assert.equal(thop.recycle(), false, 'recycle max');
}
this.test_mode = true;
// Hunch:
// Request and await the thumbnailing of a few files
for ( let i = 0 ; i < 3 ; i++ ) {
const job = new ThumbnailOperation({ behavior: 'ok' });
this.queue.push(job);
}
this.test_checked_exec = false;
await this.exec_();
assert.equal(this.queue.length, 0, 'queue emptied');
assert.equal(this.test_checked_exec, true, 'checked exec');
// test with failed job
const job = new ThumbnailOperation({ behavior: 'fail' });
this.queue.push(job);
this.test_checked_exec = false;
await this.exec_();
assert.equal(this.queue.length, 0, 'queue emptied');
assert.equal(this.test_checked_exec, true, 'checked exec');
this.test_mode = false;
}
}
module.exports = {
@@ -0,0 +1,84 @@
import { describe, expect, it } from 'vitest';
import { createTestKernel } from '../../../tools/test.mjs';
import { HTTPThumbnailService } from './HTTPThumbnailService.js';
// We need to access ThumbnailOperation, but it's not exported
// Let's recreate it here for testing purposes
const { TeePromise } = require('@heyputer/putility').libs.promise;
class ThumbnailOperation extends TeePromise {
static MAX_RECYCLE_COUNT = 3;
constructor (file: any) {
super();
this.file = file;
this.recycle_count = 0;
}
recycle () {
this.recycle_count++;
if ( this.recycle_count > this.constructor.MAX_RECYCLE_COUNT ) {
this.resolve(undefined);
return false;
}
return true;
}
}
describe('HTTPThumbnailService', () => {
it('should handle thumbnail operations correctly', async () => {
const testKernel = await createTestKernel({
serviceMap: {
'thumbs-http': HTTPThumbnailService,
},
});
const thumbnailService = testKernel.services!.get('thumbs-http') as HTTPThumbnailService;
// Mock error reporting and logging
thumbnailService.errors.report = () => {
};
thumbnailService.log = {
info: () => {
},
error: () => {
},
noticeme: () => {
},
};
// Thumbnail operation eventually recycles
{
const thop = new ThumbnailOperation(null);
for ( let i = 0 ; i < ThumbnailOperation.MAX_RECYCLE_COUNT ; i++ ) {
expect(thop.recycle()).toBe(true);
}
expect(thop.recycle()).toBe(false);
}
thumbnailService.test_mode = true;
// Request and await the thumbnailing of a few files
for ( let i = 0 ; i < 3 ; i++ ) {
const job = new ThumbnailOperation({ behavior: 'ok' });
thumbnailService.queue.push(job);
}
thumbnailService.test_checked_exec = false;
await thumbnailService.exec_();
expect(thumbnailService.queue.length).toBe(0);
expect(thumbnailService.test_checked_exec).toBe(true);
// test with failed job
const job = new ThumbnailOperation({ behavior: 'fail' });
thumbnailService.queue.push(job);
thumbnailService.test_checked_exec = false;
await thumbnailService.exec_();
expect(thumbnailService.queue.length).toBe(0);
expect(thumbnailService.test_checked_exec).toBe(true);
thumbnailService.test_mode = false;
});
});
+16 -1
View File
@@ -106,6 +106,7 @@ export class TestKernel extends AdvancedBase {
useapi: this.useapi,
['runtime-modules']: this.runtimeModuleRegistry,
}, 'app');
this.root_context = root_context;
globalThis.root_context = root_context;
root_context.arun(async () => {
@@ -132,7 +133,9 @@ export class TestKernel extends AdvancedBase {
['module']: module,
external: false,
});
await module.install(mod_context);
await this.root_context.arun(async () => {
await module.install(mod_context);
});
}
// Real kernel initializes services here, but in this test kernel
@@ -269,7 +272,10 @@ if ( import.meta.main ) {
export const createTestKernel = async ({
serviceMap,
initLevelString = 'construct',
}) => {
const initLevelMap = { CONSTRUCT: 1 };
const initLevel = initLevelMap[(`${initLevelString}`).toUpperCase()];
const testKernel = new TestKernel();
testKernel.add_module(new Core2Module());
for ( const [name, service] of Object.entries(serviceMap) ) {
@@ -282,5 +288,14 @@ export const createTestKernel = async ({
}
testKernel.boot();
await testKernel.services.ready;
const service_names = Object.keys(testKernel.services.instances_);
for ( const name of service_names ) {
const ins = testKernel.services.instances_[name];
// Fix context
ins.context = testKernel.root_context;
if ( initLevel >= initLevelMap.CONSTRUCT ) {
await ins.construct();
}
}
return testKernel;
};