Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
7a7725f
chore: bootstrap swarm original prompt
May 12, 2026
c48c39d
chore: persist swarm plan
May 12, 2026
e25f67b
Harden v3 sensitive HTTP triggers
May 12, 2026
7cabe80
Harden v4 sensitive HTTP triggers
May 12, 2026
c4acdf6
Merge remote-tracking branch 'origin/swarm/23b2164a/worker/task-2' in…
May 12, 2026
8f5379b
Document security posture change
May 12, 2026
513a40c
Add security regression coverage
May 12, 2026
0536962
Merge remote-tracking branch 'origin/swarm/23b2164a/worker/task-4' in…
May 12, 2026
25e2938
chore: persist review round 1
May 12, 2026
923a519
chore: strip evidence store for clean PR
May 12, 2026
ce88773
fix: align v4 output validation with v3 and use set-based method comp…
Copilot May 12, 2026
48fa0d9
fix: wire security regression check into CI and document FUNCTIONS_TE…
Copilot May 12, 2026
d75426f
fix: remove unnecessary escape sequences in v4 template literals
Copilot May 12, 2026
541edaa
fix: add hasValidOutputEnvelope to v4-oldConfig and remove useless es…
Copilot May 12, 2026
30b7377
fix: skip SQL/CosmosDB validation tests for v3 model
Copilot May 12, 2026
1ee55ca
fix: use argv model when skipping v3 invalid request tests
Copilot May 13, 2026
595f886
fix: move this.skip() out of async boundary so Mocha catches it synch…
Copilot May 13, 2026
c38e97e
chore: bootstrap swarm original prompt
May 13, 2026
1cd3128
chore: persist swarm plan
May 13, 2026
b98186f
chore: persist swarm plan
May 13, 2026
7ae360c
chore: persist swarm plan
May 13, 2026
875af8e
chore: persist swarm plan
May 13, 2026
e28fb8c
Simplify v3 input handlers
May 13, 2026
50756b3
Simplify v4 input handlers
May 13, 2026
20a7e8e
Narrow Cosmos/SQL E2Es
May 13, 2026
48d9bd1
Merge remote-tracking branch 'origin/swarm/3ef2be7c/worker/task-2' in…
May 13, 2026
9c7cd27
Merge remote-tracking branch 'origin/swarm/3ef2be7c/worker/task-3' in…
May 13, 2026
6066504
chore: persist review round 1
May 13, 2026
decfb51
chore: strip evidence store for clean PR
May 13, 2026
4c4cc08
Merge pull request #2 from larohra/swarm/3ef2be7c/integration
larohra May 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 64 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,64 @@
# Azure Functions Node.js E2E Tests

This repo contains end-to-end tests for Node.js on Azure Functions. These are automated tests designed to run regularly against prerelease versions of all the various pieces that make up the Node.js experience on Azure Functions, including:

- [Azure Functions Host](https://github.com/Azure/azure-functions-host)
- [Azure Functions Core Tools](https://github.com/Azure/azure-functions-core-tools)
- [Node.js Worker](https://github.com/Azure/azure-functions-nodejs-worker)
- [Node.js Library](https://github.com/Azure/azure-functions-nodejs-library)

## Pipeline

Here is the general flow of the pipeline:

1. Install node modules and build both the tests themselves and the test apps
2. Emulate several resources in Azure that will be used for testing different bindings
3. Run the tests. A few notes:
1. These are run in parallel by OS, but in serial by Node.js version and programming model version. Theoretically every combination could be run in parallel, but that would use a ton of Azure Pipelines agents
2. The primary method of validation is to run core tools against the test app and validate the output
4. Shutdown the emulated Azure resources (automatic)
5. Upload test results

## Code of Conduct

This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
# Azure Functions Node.js E2E Tests

This repo contains end-to-end tests for Node.js on Azure Functions. These are automated tests designed to run regularly against prerelease versions of all the various pieces that make up the Node.js experience on Azure Functions, including:

- [Azure Functions Host](https://github.com/Azure/azure-functions-host)
- [Azure Functions Core Tools](https://github.com/Azure/azure-functions-core-tools)
- [Node.js Worker](https://github.com/Azure/azure-functions-nodejs-worker)
- [Node.js Library](https://github.com/Azure/azure-functions-nodejs-library)

## Pipeline

Here is the general flow of the pipeline:

1. Install node modules and build both the tests themselves and the test apps
2. Emulate several resources in Azure that will be used for testing different bindings
3. Run the tests. A few notes:
1. These are run in parallel by OS, but in serial by Node.js version and programming model version. Theoretically every combination could be run in parallel, but that would use a ton of Azure Pipelines agents
2. The primary method of validation is to run core tools against the test app and validate the output
4. Shutdown the emulated Azure resources (automatic)
5. Upload test results

## Security posture for resource-backed HTTP routes

This repository is a localhost/emulator end-to-end harness. The test pipeline runs Azure Functions Core Tools on `127.0.0.1` and exercises bindings against local or emulated dependencies instead of deploying a public Azure Function App from this repo.

That operating model lowers immediate exposure in this repository, but the reported findings are still actionable for any Azure-hosted deployment of the same functions. The affected routes are wired to real Table, Cosmos DB, SQL, Storage Queue, and Service Bus bindings, so anonymous access in a hosted app can still read from or write to backing resources.

Some of those binding-coverage routes originally used anonymous auth as a historical convenience so the E2E tests could call them with simple `fetch` requests while coverage was being expanded. That was never meant to be a recommended production posture.

Sensitive resource-backed HTTP routes now require function-level auth. This is a deliberate breaking change for deployed callers that previously invoked those routes anonymously. Hosted callers must now send a function key, and should follow the tightened contract of using `GET` for read endpoints and `POST` for write endpoints.

Azure Functions Core Tools does not enforce function-key auth for local runs, so local regression protection comes primarily from static route checks and validation tests rather than from a local auth challenge.

### Hosted invocation examples

For Azure-hosted runs, include the function key either as the `code` query string parameter:

```bash
curl "https://<function-app>.azurewebsites.net/api/httpTriggerTableInput/<rowKey>?code=<function-key>"
```

or as the `x-functions-key` header:

```bash
curl -H "x-functions-key: <function-key>" "https://<function-app>.azurewebsites.net/api/httpTriggerTableInput/<rowKey>"
```

The same requirement applies to hosted `POST` requests for the resource-backed output routes.

### Automated test helper

The test helper `getFuncUrl()` automatically appends `?code=<key>` to every URL when the `FUNCTIONS_TEST_KEY` environment variable is set. This means that the same test suite runs against both local Core Tools (no key needed—auth is not enforced locally) and hosted Azure Function Apps (key is injected via the env var) without any code changes:

```bash
# Local: no env var needed — Core Tools ignores auth
npm run testAllExceptServiceBus

# Hosted: set FUNCTIONS_TEST_KEY to your function's default key
FUNCTIONS_TEST_KEY=<function-key> npm run testAllExceptServiceBus
```

## Code of Conduct

This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
6 changes: 4 additions & 2 deletions app/v3-oldConfig/httpTriggerCosmosDBInput/function.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
{
"bindings": [
{
"authLevel": "anonymous",
"authLevel": "function",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": ["get", "post"]
"methods": [
"get"
]
},
{
"type": "http",
Expand Down
6 changes: 4 additions & 2 deletions app/v3-oldConfig/httpTriggerCosmosDBOutput/function.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
{
"bindings": [
{
"authLevel": "anonymous",
"authLevel": "function",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": ["get", "post"]
"methods": [
"post"
]
},
{
"type": "http",
Expand Down
6 changes: 4 additions & 2 deletions app/v3/httpTriggerCosmosDBInput/function.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
{
"bindings": [
{
"authLevel": "anonymous",
"authLevel": "function",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": ["get", "post"]
"methods": [
"get"
]
},
{
"type": "http",
Expand Down
33 changes: 18 additions & 15 deletions app/v3/httpTriggerCosmosDBInput/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.

import { AzureFunction, Context, HttpRequest } from '@azure/functions';

const httpTriggerCosmosDBInput: AzureFunction = async function (
context: Context,
_request: HttpRequest
): Promise<void> {
context.res = {
body: context.bindings.inputDoc.testData,
};
};

export default httpTriggerCosmosDBInput;
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.

import { AzureFunction, Context } from '@azure/functions';
import { isMissingReadResult } from '../utils/httpValidation';

const httpTriggerCosmosDBInput: AzureFunction = async function (context: Context): Promise<void> {
if (isMissingReadResult(context.bindings.inputDoc)) {
context.res = { status: 404 };
return;
}

context.res = {
body: context.bindings.inputDoc.testData,
};
};

export default httpTriggerCosmosDBInput;
6 changes: 4 additions & 2 deletions app/v3/httpTriggerCosmosDBOutput/function.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
{
"bindings": [
{
"authLevel": "anonymous",
"authLevel": "function",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": ["get", "post"]
"methods": [
"post"
]
},
{
"type": "http",
Expand Down
35 changes: 21 additions & 14 deletions app/v3/httpTriggerCosmosDBOutput/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.

import { AzureFunction, Context, HttpRequest } from '@azure/functions';

const httpTriggerCosmosDBOutput: AzureFunction = async function (
context: Context,
request: HttpRequest
): Promise<void> {
context.bindings.outputDoc = request.body;
context.res = { body: 'done' };
};

export default httpTriggerCosmosDBOutput;
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.

import { AzureFunction, Context, HttpRequest } from '@azure/functions';
import { getJsonBody, hasItemsWithRequiredStringFields } from '../utils/httpValidation';

const httpTriggerCosmosDBOutput: AzureFunction = async function (
context: Context,
request: HttpRequest
): Promise<void> {
const body = getJsonBody(request);
if (!hasItemsWithRequiredStringFields(body, ['id', 'testData'])) {
context.res = { status: 400 };
return;
}

context.bindings.outputDoc = body;
context.res = { body: 'done' };
};

export default httpTriggerCosmosDBOutput;
6 changes: 4 additions & 2 deletions app/v3/httpTriggerServiceBusOutput/function.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
{
"bindings": [
{
"authLevel": "anonymous",
"authLevel": "function",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": ["get", "post"]
"methods": [
"post"
]
},
{
"type": "http",
Expand Down
35 changes: 21 additions & 14 deletions app/v3/httpTriggerServiceBusOutput/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.

import { AzureFunction, Context, HttpRequest } from '@azure/functions';

const httpTriggerServiceBusOutput: AzureFunction = async function (
context: Context,
request: HttpRequest
): Promise<void> {
context.bindings.outputMsg = request.body.output;
context.res = { body: 'done' };
};

export default httpTriggerServiceBusOutput;
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.

import { AzureFunction, Context, HttpRequest } from '@azure/functions';
import { getJsonBody, hasValidOutputEnvelope } from '../utils/httpValidation';

const httpTriggerServiceBusOutput: AzureFunction = async function (
context: Context,
request: HttpRequest
): Promise<void> {
const body = getJsonBody(request);
if (!hasValidOutputEnvelope(body)) {
context.res = { status: 400 };
return;
}

context.bindings.outputMsg = body.output;
context.res = { body: 'done' };
};

export default httpTriggerServiceBusOutput;
8 changes: 5 additions & 3 deletions app/v3/httpTriggerSqlInput/function.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
{
"bindings": [
{
"authLevel": "anonymous",
"authLevel": "function",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": ["get", "post"]
"methods": [
"get"
]
},
{
"type": "http",
Expand All @@ -23,4 +25,4 @@
}
],
"scriptFile": "../dist/httpTriggerSqlInput/index.js"
}
}
33 changes: 20 additions & 13 deletions app/v3/httpTriggerSqlInput/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.

import { AzureFunction, Context, HttpRequest } from '@azure/functions';

const httpTriggerSqlInput: AzureFunction = async function (context: Context, _request: HttpRequest): Promise<void> {
context.log(`httpTriggerSqlInput was triggered`);
context.res = {
body: context.bindings.inputItem,
};
};

export default httpTriggerSqlInput;
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.

import { AzureFunction, Context } from '@azure/functions';
import { isMissingReadResult } from '../utils/httpValidation';

const httpTriggerSqlInput: AzureFunction = async function (context: Context): Promise<void> {
context.log(`httpTriggerSqlInput was triggered`);

if (isMissingReadResult(context.bindings.inputItem)) {
context.res = { status: 404 };
return;
}

context.res = {
body: context.bindings.inputItem,
};
};

export default httpTriggerSqlInput;
8 changes: 5 additions & 3 deletions app/v3/httpTriggerSqlOutput/function.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
{
"bindings": [
{
"authLevel": "anonymous",
"authLevel": "function",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": ["get", "post"]
"methods": [
"post"
]
},
{
"type": "http",
Expand All @@ -21,4 +23,4 @@
}
],
"scriptFile": "../dist/httpTriggerSqlOutput/index.js"
}
}
35 changes: 21 additions & 14 deletions app/v3/httpTriggerSqlOutput/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.

import { AzureFunction, Context, HttpRequest } from '@azure/functions';

const httpTriggerSqlOutput: AzureFunction = async function (context: Context, request: HttpRequest): Promise<void> {
context.log(`httpTriggerSqlOutput was triggered`);
context.bindings.outputItem = request.body;
context.res = {
status: 201,
};
};

export default httpTriggerSqlOutput;
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.

import { AzureFunction, Context, HttpRequest } from '@azure/functions';
import { getJsonBody, hasItemsWithRequiredStringFields } from '../utils/httpValidation';

const httpTriggerSqlOutput: AzureFunction = async function (context: Context, request: HttpRequest): Promise<void> {
const body = getJsonBody(request);
if (!hasItemsWithRequiredStringFields(body, ['id', 'testData'])) {
context.res = { status: 400 };
return;
}

context.log(`httpTriggerSqlOutput was triggered`);
context.bindings.outputItem = body;
context.res = {
status: 201,
};
};

export default httpTriggerSqlOutput;
Loading