Skip to content

feat(functions): Add emulator support for integration tests - #1897

Open
inlined wants to merge 5 commits into
mainfrom
security-audit-functions-emulator
Open

feat(functions): Add emulator support for integration tests#1897
inlined wants to merge 5 commits into
mainfrom
security-audit-functions-emulator

Conversation

@inlined

@inlined inlined commented Jul 30, 2026

Copy link
Copy Markdown
Member

This PR performs a security audit and modernizes functions integration test configuration. It also integrates Cloud Functions emulator support into the C++ SDK client integration tests.

- Modernize package.json dependencies in integration tests for functions, app_check, and messaging.
- Resolve vulnerability in uuid via dependency overrides.
- Introduce start.js to spin up local functions emulator dynamically on port 5005.
- Strip http:// and https:// prefixes from emulator host strings for iOS to prevent crashes on native iOS client initialization.
- Parse FUNCTIONS_EMULATOR_HOST environment variable and --functions_emulator_host flag, and connect functions clients in integration tests to the emulator.
@wiz-9635d3485b

wiz-9635d3485b Bot commented Jul 30, 2026

Copy link
Copy Markdown

Wiz Scan Summary

Scanner Findings
Vulnerability Finding Vulnerabilities -
Data Finding Sensitive Data -
Secret Finding Secrets -
IaC Misconfiguration IaC Misconfigurations -
SAST Finding SAST Findings 1 Medium
Software Management Finding Software Management Findings -
Total 1 Medium

View scan details in Wiz

To detect these findings earlier in the dev lifecycle, try the Wiz Code extension for VS Code, JetBrains, or Visual Studio.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds support for running integration tests against the Firebase Functions emulator by introducing a startup script (start.js), updating dependencies, and implementing emulator configuration logic across multiple test suites. Feedback focuses on improving the robustness of the new code: ensuring the temporary directory in start.js is cleaned up on failure or exit, verifying the emulator child process status in waitForPort to prevent false positives when a port is already in use, and validating that the parsed port in functions_ios.mm is within the valid range (1-65535).

Comment on lines +25 to +97
async function main() {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'firebase-functions-'));
console.log(`Creating functions in ${tempDir}`);

// Create functions subdirectory
fs.mkdirSync(path.join(tempDir, 'functions'), { recursive: true });

// Copy files
fs.copyFileSync(path.join(scriptDir, 'functions', 'index.js'), path.join(tempDir, 'functions', 'index.js'));
fs.copyFileSync(path.join(scriptDir, 'functions', 'package.json'), path.join(tempDir, 'functions', 'package.json'));
fs.copyFileSync(path.join(scriptDir, 'firebase.json'), path.join(tempDir, 'firebase.json'));

// npm install in functions directory
console.log('Installing dependencies...');
await runCommand(os.platform() === 'win32' ? 'npm.cmd' : 'npm', ['install'], { cwd: path.join(tempDir, 'functions') });

// Start the server
console.log('Starting the emulator...');
const logFile = path.join(tempDir, 'firebase-emulator.log');
const out = fs.openSync(logFile, 'a');
const err = fs.openSync(logFile, 'a');

const child = spawn(
os.platform() === 'win32' ? 'npx.cmd' : 'npx',
['firebase', 'emulators:start', '--only', 'functions', '--project', 'functions-integration-test'],
{
cwd: tempDir,
detached: isSynchronous,
stdio: isSynchronous ? ['ignore', out, err] : 'inherit'
}
);

if (isSynchronous) {
child.unref();
}

// Wait for the emulator to be ready
console.log('Waiting for emulator to start...');
const ready = await waitForPort(5005, '127.0.0.1', 30);
if (!ready) {
console.error('Emulator failed to start within 30 seconds.');
if (fs.existsSync(logFile)) {
console.error(fs.readFileSync(logFile, 'utf8'));
}
process.exit(1);
}
console.log('Emulator is ready!');

if (!isSynchronous) {
console.log(`Functions emulator now running in ${tempDir}.`);
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});

rl.question('*** Press Enter to stop the server. ***\n', () => {
rl.close();
child.kill();
process.exit(0);
});

// Handle termination signals
const cleanup = () => {
child.kill();
process.exit(0);
};
process.on('SIGINT', cleanup);
process.on('SIGTERM', cleanup);
} else {
// Exit parent process, leaving the detached child running
process.exit(0);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The temporary directory tempDir created by fs.mkdtempSync is never cleaned up if the script fails during setup (e.g., if npm install fails or the emulator fails to start), or when the script exits in interactive mode. Since this directory contains a copy of the functions and a full node_modules folder, this can quickly consume significant disk space on CI servers and developer machines. Wrapping the main execution in a try...catch block and cleaning up tempDir on failure or exit ensures proper resource management.

async function main() {
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'firebase-functions-'));
  console.log(`Creating functions in ${tempDir}`);

  try {
    // Create functions subdirectory
    fs.mkdirSync(path.join(tempDir, 'functions'), { recursive: true });

    // Copy files
    fs.copyFileSync(path.join(scriptDir, 'functions', 'index.js'), path.join(tempDir, 'functions', 'index.js'));
    fs.copyFileSync(path.join(scriptDir, 'functions', 'package.json'), path.join(tempDir, 'functions', 'package.json'));
    fs.copyFileSync(path.join(scriptDir, 'firebase.json'), path.join(tempDir, 'firebase.json'));

    // npm install in functions directory
    console.log('Installing dependencies...');
    await runCommand(os.platform() === 'win32' ? 'npm.cmd' : 'npm', ['install'], { cwd: path.join(tempDir, 'functions') });

    // Start the server
    console.log('Starting the emulator...');
    const logFile = path.join(tempDir, 'firebase-emulator.log');
    const out = fs.openSync(logFile, 'a');
    const err = fs.openSync(logFile, 'a');

    const child = spawn(
      os.platform() === 'win32' ? 'npx.cmd' : 'npx',
      ['firebase', 'emulators:start', '--only', 'functions', '--project', 'functions-integration-test'],
      {
        cwd: tempDir,
        detached: isSynchronous,
        stdio: isSynchronous ? ['ignore', out, err] : 'inherit'
      }
    );

    if (isSynchronous) {
      child.unref();
    }

    // Wait for the emulator to be ready
    console.log('Waiting for emulator to start...');
    const ready = await waitForPort(5005, '127.0.0.1', 30, child);
    if (!ready) {
      console.error('Emulator failed to start within 30 seconds.');
      if (fs.existsSync(logFile)) {
        console.error(fs.readFileSync(logFile, 'utf8'));
      }
      if (!isSynchronous) {
        child.kill();
      }
      throw new Error('Emulator failed to start');
    }
    console.log('Emulator is ready!');

    if (!isSynchronous) {
      console.log(`Functions emulator now running in ${tempDir}.`);
      const rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout
      });
      
      rl.question('*** Press Enter to stop the server. ***\n', () => {
        rl.close();
        child.kill();
        try {
          fs.rmSync(tempDir, { recursive: true, force: true });
        } catch (err) {}
        process.exit(0);
      });

      // Handle termination signals
      const cleanup = () => {
        child.kill();
        try {
          fs.rmSync(tempDir, { recursive: true, force: true });
        } catch (err) {}
        process.exit(0);
      };
      process.on('SIGINT', cleanup);
      process.on('SIGTERM', cleanup);
    } else {
      // Exit parent process, leaving the detached child running
      process.exit(0);
    }
  } catch (err) {
    try {
      fs.rmSync(tempDir, { recursive: true, force: true });
    } catch (cleanupErr) {
      console.error(`Failed to clean up temp directory ${tempDir}:`, cleanupErr);
    }
    throw err;
  }
}

Comment on lines +109 to +129
function waitForPort(port, host, retries) {
const check = () => new Promise((resolve, reject) => {
const s = new net.Socket();
s.setTimeout(1000);
s.on('connect', () => { s.destroy(); resolve(true); });
s.on('error', () => resolve(false));
s.on('timeout', () => { s.destroy(); resolve(false); });
s.connect(port, host);
});

return new Promise(async (resolve) => {
for (let i = 0; i < retries; i++) {
if (await check()) {
resolve(true);
return;
}
await new Promise(r => setTimeout(r, 1000));
}
resolve(false);
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If the emulator port (5005) is already in use by another process, waitForPort will immediately succeed because it can establish a socket connection, even though the emulator child process failed to start and exited. Passing the child process to waitForPort and checking child.exitCode !== null allows the script to fail fast and report the failure correctly instead of falsely reporting that the emulator is ready.

function waitForPort(port, host, retries, child) {
  const check = () => new Promise((resolve, reject) => {
    const s = new net.Socket();
    s.setTimeout(1000);
    s.on('connect', () => { s.destroy(); resolve(true); });
    s.on('error', () => resolve(false));
    s.on('timeout', () => { s.destroy(); resolve(false); });
    s.connect(port, host);
  });

  return new Promise(async (resolve) => {
    for (let i = 0; i < retries; i++) {
      if (child && child.exitCode !== null) {
        resolve(false);
        return;
      }
      if (await check()) {
        if (child && child.exitCode !== null) {
          resolve(false);
          return;
        }
        resolve(true);
        return;
      }
      await new Promise(r => setTimeout(r, 1000));
    }
    resolve(false);
  });
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines 94 to 96
std::string port_str = origin_str.substr(pos+1, std::string::npos);
int port = atoi(port_str.c_str());
[impl_.get()->get() useEmulatorWithHost:@(host.c_str()) port:port];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The port parsed from origin_str is converted using atoi, which returns 0 if the port string is invalid or non-numeric (e.g., localhost:abc). Passing 0 or an out-of-range port to the native SDK can lead to unexpected behavior. Adding validation to ensure the port is within the valid range (1-65535) improves robustness.

  std::string port_str = origin_str.substr(pos+1, std::string::npos);
  int port = atoi(port_str.c_str());
  if (port <= 0 || port > 65535) {
    LogError("Functions::UseFunctionsEmulator: Invalid port specified");
    return;
  }
  [impl_.get()->get() useEmulatorWithHost:@(host.c_str()) port:port];

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant