callback-runtime-smoke-test.mjs 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. import assert from 'node:assert/strict';
  2. import crypto from 'node:crypto';
  3. import fs from 'node:fs';
  4. import os from 'node:os';
  5. import path from 'node:path';
  6. import { pathToFileURL } from 'node:url';
  7. import { decryptPayload, EnterpriseRelayRuntime } from '../runtime/callback-service/src/enterprise-relay-client.mjs';
  8. import { loadRuntimeConfig, resolveRuntimeMode } from '../runtime/callback-service/src/config-loader.mjs';
  9. import {
  10. clearRuntimeStopRequest,
  11. readRuntimeState,
  12. readRuntimeStopRequest,
  13. requestRuntimeStop,
  14. writeRuntimeState,
  15. } from '../runtime/callback-service/src/runtime-state.mjs';
  16. function encryptV2(payload, publicKey) {
  17. const aesKey = crypto.randomBytes(32);
  18. const iv = crypto.randomBytes(12);
  19. const cipher = crypto.createCipheriv('aes-256-gcm', aesKey, iv);
  20. const ciphertext = Buffer.concat([cipher.update(payload, 'utf8'), cipher.final()]);
  21. const envelope = {
  22. key: crypto.publicEncrypt({ key: publicKey, oaepHash: 'sha256' }, aesKey).toString('base64'),
  23. iv: iv.toString('base64'),
  24. tag: cipher.getAuthTag().toString('base64'),
  25. ciphertext: ciphertext.toString('base64'),
  26. };
  27. return `v2:${Buffer.from(JSON.stringify(envelope)).toString('base64')}`;
  28. }
  29. const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'qiwei-runtime-smoke-'));
  30. try {
  31. const configPath = path.join(tempRoot, 'runtime-config.mjs');
  32. fs.writeFileSync(configPath, "export default { edition: 'personal', dashboard: { port: 4399 } };\n", 'utf8');
  33. const loaded = await loadRuntimeConfig({ workspaceRoot: tempRoot, configPath });
  34. assert.equal(loaded.mode, 'personal');
  35. assert.equal(loaded.config.dashboard.port, 4399);
  36. assert.equal(loaded.config.enterprise.relay.batchSize, 100);
  37. assert.equal(resolveRuntimeMode({ edition: 'enterprise' }), 'enterprise');
  38. const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 });
  39. const payload = JSON.stringify({ cmd: 15000, content: 'runtime-smoke' });
  40. const encrypted = encryptV2(payload, publicKey);
  41. assert.equal(decryptPayload(encrypted, privateKey), payload);
  42. const relayConfig = { batchSize: 10, waitMs: 1000, retryMinMs: 250, retryMaxMs: 1000 };
  43. const relayContext = {
  44. baseUrl: 'http://relay.test',
  45. apiSecret: 'test-secret',
  46. privateKey,
  47. tenantId: 'tenant-test',
  48. guid: 'guid-test',
  49. };
  50. let ackBody = null;
  51. let processedEnvelope = null;
  52. const successFetch = async (url, options) => {
  53. if (url.endsWith('/api/relay/poll')) {
  54. return new Response(JSON.stringify({ events: [{ eventId: 'event-1', encryptedPayload: encrypted }] }), {
  55. status: 200,
  56. headers: { 'Content-Type': 'application/json' },
  57. });
  58. }
  59. ackBody = JSON.parse(options.body);
  60. return new Response(JSON.stringify({ ackedCount: 1 }), {
  61. status: 200,
  62. headers: { 'Content-Type': 'application/json' },
  63. });
  64. };
  65. const relay = new EnterpriseRelayRuntime({
  66. config: relayConfig,
  67. fetchImpl: successFetch,
  68. processor: async envelope => { processedEnvelope = envelope; },
  69. });
  70. const relayResult = await relay.pollOnce(relayContext);
  71. assert.equal(relayResult.acked, 1);
  72. assert.deepEqual(ackBody.eventIds, ['event-1']);
  73. assert.equal(processedEnvelope.data[0].content, 'runtime-smoke');
  74. let failureAckCalled = false;
  75. const failureRelay = new EnterpriseRelayRuntime({
  76. config: relayConfig,
  77. fetchImpl: async url => {
  78. if (url.endsWith('/api/relay/ack')) failureAckCalled = true;
  79. return new Response(JSON.stringify({ events: [{ eventId: 'event-2', encryptedPayload: encrypted }] }), {
  80. status: 200,
  81. headers: { 'Content-Type': 'application/json' },
  82. });
  83. },
  84. processor: async () => { throw new Error('processor-test-failure'); },
  85. });
  86. await assert.rejects(() => failureRelay.pollOnce(relayContext), /retained 1 failed event/);
  87. assert.equal(failureAckCalled, false);
  88. const statePath = path.join(tempRoot, 'runtime-state.json');
  89. writeRuntimeState({ pid: 123, status: 'running', apiSecret: 'hidden', components: { relay: { token: 'hidden' } } }, statePath);
  90. const state = readRuntimeState(statePath);
  91. assert.equal(state.status, 'running');
  92. assert.equal('apiSecret' in state, false);
  93. assert.equal('token' in state.components.relay, false);
  94. requestRuntimeStop(statePath);
  95. assert.equal(readRuntimeStopRequest(statePath).targetPid, 123);
  96. clearRuntimeStopRequest(statePath);
  97. assert.deepEqual(readRuntimeStopRequest(statePath), {});
  98. process.stdout.write(`${JSON.stringify({ status: 'ok', checks: 19 }, null, 2)}\n`);
  99. } finally {
  100. fs.rmSync(tempRoot, { recursive: true, force: true });
  101. }