From d55bedace0890fbcd87ee71164a62ea0e1754247 Mon Sep 17 00:00:00 2001 From: Yuri Mizushima Date: Thu, 15 Oct 2026 15:14:28 +0900 Subject: [PATCH] test(auth): verify natural exit with token suppliers --- tests/process_exit.test.js | 102 +++++++++++++++++++++++++++ tests/process_exit/runner.js | 53 ++++++++++++++ tests/process_exit/token_supplier.js | 39 ++++++++++ 3 files changed, 194 insertions(+) create mode 100644 tests/process_exit.test.js create mode 100644 tests/process_exit/runner.js create mode 100644 tests/process_exit/token_supplier.js diff --git a/tests/process_exit.test.js b/tests/process_exit.test.js new file mode 100644 index 0000000..d72ee24 --- /dev/null +++ b/tests/process_exit.test.js @@ -0,0 +1,102 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const path = require('path'); +const { spawn } = require('child_process'); + +const scenariosDir = path.join(__dirname, 'process_exit'); +const runner = path.join(scenariosDir, 'runner.js'); + +const expectNaturalExit = (script, args) => new Promise((resolve, reject) => { + const child = spawn(process.execPath, [runner, script, ...args], { + stdio: ['ignore', 'pipe', 'pipe', 'ipc'], + }); + let stdout = ''; + let stderr = ''; + let spawnError; + let protocolFailure; + let timedOut; + let completed = false; + let disconnectedAfterCompletion = false; + let naturalExitTimer; + + const watchdogTimer = setTimeout(() => { + timedOut = completed ? 'IPC disconnect' : 'scenario completion'; + child.kill('SIGKILL'); + }, 60000); + + child.stdout.on('data', (data) => { + stdout += data.toString(); + }); + child.stderr.on('data', (data) => { + stderr += data.toString(); + }); + child.on('message', (message) => { + if (message && message.type === 'scenario-complete') { + completed = true; + } + }); + child.on('disconnect', () => { + if (!completed) { + protocolFailure = 'IPC disconnected before scenario completion'; + child.kill('SIGKILL'); + return; + } + + disconnectedAfterCompletion = true; + clearTimeout(watchdogTimer); + naturalExitTimer = setTimeout(() => { + timedOut = 'natural exit after scenario completion and IPC disconnect'; + child.kill('SIGKILL'); + }, 10000); + }); + child.on('error', (error) => { + spawnError = error; + clearTimeout(watchdogTimer); + clearTimeout(naturalExitTimer); + }); + child.on('close', (code, signal) => { + clearTimeout(watchdogTimer); + clearTimeout(naturalExitTimer); + if (timedOut || spawnError || protocolFailure || !completed + || !disconnectedAfterCompletion || code !== 0 || signal !== null) { + reject(new Error(`Child did not exit naturally: ${JSON.stringify({ + timedOut, + spawnError: spawnError && spawnError.message, + protocolFailure, + completed, + disconnectedAfterCompletion, + code, + signal, + stdout, + stderr, + })}`)); + return; + } + resolve(); + }); +}); + +describe('AuthenticationToken supplier process exit', () => { + const scenario = path.join(scenariosDir, 'token_supplier.js'); + + test.each(['sync', 'async'])('%s supplier exits after closing a client', async (supplier) => { + await expectNaturalExit(scenario, [supplier]); + }, 75000); +}); diff --git a/tests/process_exit/runner.js b/tests/process_exit/runner.js new file mode 100644 index 0000000..7148a08 --- /dev/null +++ b/tests/process_exit/runner.js @@ -0,0 +1,53 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const disconnect = () => { + if (process.connected) { + process.disconnect(); + } +}; + +const fail = (error) => { + console.error(error); + process.exitCode = 1; + disconnect(); +}; + +const runScenario = async () => { + if (typeof process.send !== 'function') { + throw new Error('Scenario runner requires an IPC channel'); + } + + // eslint-disable-next-line global-require, import/no-dynamic-require + const scenario = require(process.argv[2]); + if (typeof scenario !== 'function') { + throw new TypeError('Scenario module must export a function'); + } + await scenario(...process.argv.slice(3)); +}; + +runScenario().then(() => { + process.send({ type: 'scenario-complete' }, (error) => { + if (error) { + fail(error); + return; + } + disconnect(); + }); +}).catch(fail); diff --git a/tests/process_exit/token_supplier.js b/tests/process_exit/token_supplier.js new file mode 100644 index 0000000..6c5bbf4 --- /dev/null +++ b/tests/process_exit/token_supplier.js @@ -0,0 +1,39 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const Pulsar = require('../../index'); + +const suppliers = { + sync: () => 'dummy-token', + async: async () => 'dummy-token', +}; + +module.exports = async (mode) => { + const supplier = suppliers[mode]; + if (!supplier) { + throw new Error(`Unknown supplier: ${mode}`); + } + + global.auth = new Pulsar.AuthenticationToken({ token: supplier }); + const client = new Pulsar.Client({ + serviceUrl: 'pulsar://localhost:6650', + authentication: global.auth, + }); + await client.close(); +};