test: Add test for implicit evaluation loop (#19200)

This commit is contained in:
Mutasem Aldmour
2025-09-05 09:51:28 +02:00
committed by GitHub
parent 2ce0911186
commit 6d76733200
15 changed files with 910 additions and 61 deletions

View File

@@ -101,21 +101,63 @@ test.describe('Proxy tests @capability:proxy', () => {
The ProxyServer service supports recording HTTP requests for test mocking and replay. All proxied requests are automatically recorded by the mock server as described in the [Mock Server documentation](https://www.mock-server.com/proxy/record_and_replay.html). The ProxyServer service supports recording HTTP requests for test mocking and replay. All proxied requests are automatically recorded by the mock server as described in the [Mock Server documentation](https://www.mock-server.com/proxy/record_and_replay.html).
```typescript #### Recording Expectations
// Record all requests
await proxyServer.recordExpectations();
// Record requests with matching criteria ```typescript
await proxyServer.recordExpectations({ // Record all requests (the request is simplified/cleansed to method/path/body/query)
method: 'POST', await proxyServer.recordExpectations('test-folder');
path: '/api/workflows',
queryStringParameters: { // Record with filtering and options
'userId': ['123'] await proxyServer.recordExpectations('test-folder', {
host: 'googleapis.com', // Filter by host (partial match)
dedupe: true, // Remove duplicate requests
raw: false // Save cleaned requests (default)
});
// Record raw requests with all headers and metadata
await proxyServer.recordExpectations('test-folder', {
raw: true // Save complete original requests
});
// Record requests matching specific criteria
await proxyServer.recordExpectations('test-folder', {
pathOrRequestDefinition: {
method: 'POST',
path: '/api/workflows'
} }
}); });
``` ```
Recorded expectations are saved as JSON files in the `expectations/` directory with unique names based on the request details. When the ProxyServer fixture initializes, all saved expectations are automatically loaded and mocked for subsequent test runs. #### Loading and Using Recorded Expectations
Recorded expectations are saved as JSON files in the `expectations/` directory. To use them in tests, you must explicitly load them:
```typescript
test('should use recorded expectations', async ({ proxyServer }) => {
// Load expectations from a specific folder
await proxyServer.loadExpectations('test-folder');
// Your test code here - requests will be mocked using loaded expectations
});
```
#### Important: Cleanup Expectations
**Remember to clean up expectations before or after test runs:**
```typescript
test.beforeEach(async ({ proxyServer }) => {
// Clear any existing expectations before test
await proxyServer.clearAllExpectations();
});
test.afterEach(async ({ proxyServer }) => {
// Or clear expectations after test
await proxyServer.clearAllExpectations();
});
```
This prevents expectations from one test affecting others and ensures test isolation.
## Writing Tests ## Writing Tests
For guidelines on writing new tests, see [CONTRIBUTING.md](./CONTRIBUTING.md). For guidelines on writing new tests, see [CONTRIBUTING.md](./CONTRIBUTING.md).

View File

@@ -0,0 +1,37 @@
{
"httpRequest": {
"method": "POST",
"path": "/token"
},
"httpResponse": {
"statusCode": 200,
"reasonPhrase": "OK",
"headers": {
"X-XSS-Protection": ["0"],
"X-Frame-Options": ["SAMEORIGIN"],
"X-Content-Type-Options": ["nosniff"],
"Vary": ["Origin", "X-Origin", "Referer"],
"Server": ["scaffolding on HTTPServer2"],
"Date": ["Thu, 04 Sep 2025 14:07:57 GMT"],
"Content-Type": ["application/json; charset=UTF-8"],
"Alt-Svc": ["h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000"]
},
"body": {
"type": "JSON",
"json": {
"access_token": "mock_access_token_fjWwvFFZMgzvj8i08j8coeyRZz_p57s_Vqjk3v0kzrmOr2MJMUVYYlOII5Zq8BFU08drBzsz50Q-6f3S1MBt2dO3vYkzw1Ml7jnykQmUSz1wVce-M9zefdaFVeVuc1rjIviipVeO3ojF95FvghMMwnVvUF55ppzI3n_vRM1hM5sipbora7xs5Y0qaOQrX_-6SF_j99_bWI_Og-y5OdoapprB_aXdkaOn8U816ac5ldp8r7g2bdU0oqwukwxUvyyubc-h6Zc3WIdtXcn7BXmtUVWMMwZ4uBqtr6rUq2YkocVSV0sf2yRZjy1Wdp8aFo0wRk2i0V55mOcvVVskQ07w9",
"expires_in": 3599,
"token_type": "Bearer"
},
"rawBytes": "eyJhY2Nlc3NfdG9rZW4iOiJ5YTI5LmMuYzBBU1JLMEdZcmU5R2VfNVYtRlJNS0UtRi0yT2wwNVZQdHppQVhGODh2aGFqbm9pQUc4Xzhib1hadmFVSnVvUUJjQnluUUl6ajdwckk1c3c3Y3RGMEpISWpGdWUyOGRXc25pZ00yeDVPOU40R3FRb05kc1VKUF9ibFJHN1VSNEtuMF9YcnV0Y3A3bEZ5cmZrT2R0Vk8yeU5FMzFuaUJFYmJYeDRhRTRlaGVNME1ISkJaNlV5aDFzQjV5d2Q1akl4NENzdTRxNHJEemI4SFlYZ3FqZUd2THJsc3AyRndNaFFGd1VkR2hnVWIxdGZVTE0wT3FaajdEV0I4aGozeTJ0Tk5KUmhCWXZvZ3lEaF9STmdaNi1Bb2xNM1B1eHRjam9mTG1WMHZZY2lsZVMwc3ByUEQ2eEVqWHpIMlNndld1NDM1VXBfdU96NVVEbVljbUgzZ3JjTndmdmpJTWNMUk5LSndoWHkySGx1SEpRZktUZThpUGEtN05uTTh4aEt4TEczODlDYjRyczFsNVJhZmJZUXAtaHRWV3lSVlctcTZKYWY3NlJuOEpZQnNKNXhkRlg3WXczMXNoWWJSbG84dzV6NGhleHJ3cGp3c29sV3N0NHdNb1UwdDRreU80c3h4ajBhQm5oWnprOHc3cE1CSjlTbDFrSWd2V3F3MnBGVVJZWFYzaFk0aGtWck9PSlVkVUoyZGVjSVZmMFgxV3NmNV9sdDJZN09lVzlidllYcVNZa2NZMTJfUzhRQi13NF8tT1YxNmhrX1diMkJKVV9hcWc4aVdrVk1wNDZJcUl1ZlEtMW9Tb3djY3dtTzFXV3NfVWc4d3I3Vm40OVVabVZGV0JRNDM2VWR6MHFVZWRhNzFTWTJyc25vVy1tMi1XeHhXSXdsNVltdmZqV3d2RkZaTWd6dmo4aTA4ajhjb2V5Ulp6X3A1N3NfVnFqazN2MGt6cm1PcjJNSk1VVllZbE9JSTVacThCRlUwOGRyQnpzejUwUS02ZjNTMU1CdDJkTzN2WWt6dzFNbDdqbnlrUW1VU3oxd1ZjZS1NOXplZmRhRlZlVnVjMXJqSXZpaXBWZU8zb2pGOTVGdmdoTU13blZ2VUY1NXBwekkzbl92Uk0xaE01c2lwYm9yYTd4czVZMHFhT1FyWF8tNlNGX2o5OV9iV0lfT2cteTVPZG9hcHByQl9hWGRrYU9uOFU4MTZhYzVsZHA4cjdnMmJkVTBvcXd1a3d4VXZ5eXViYy1oNlpjM1dJZHRYY243QlhtdFVWV01Nd1o0dUJxdHI2clVxMllrb2NWU1Ywc2YyeVJaankxV2RwOGFGbzB3UmsyaTBWNTVtT2N2VlZza1EwN3c5IiwiZXhwaXJlc19pbiI6MzU5OSwidG9rZW5fdHlwZSI6IkJlYXJlciJ9"
}
},
"id": "1756994893546-oauth2.googleapis.com-POST-_token-58f6fdf2.json",
"priority": 0,
"timeToLive": {
"unlimited": true
},
"times": {
"unlimited": true
}
}

View File

@@ -0,0 +1,64 @@
{
"httpRequest": {
"method": "GET",
"path": "/v4/spreadsheets/1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs",
"queryStringParameters": {
"fields": ["sheets.properties"]
}
},
"httpResponse": {
"statusCode": 200,
"reasonPhrase": "OK",
"headers": {
"x-l2-request-path": ["l2-managed-6"],
"X-XSS-Protection": ["0"],
"X-Frame-Options": ["SAMEORIGIN"],
"X-Content-Type-Options": ["nosniff"],
"Vary": ["Origin", "X-Origin", "Referer"],
"Server": ["ESF"],
"Date": ["Thu, 04 Sep 2025 14:07:57 GMT"],
"Content-Type": ["application/json; charset=UTF-8"],
"Alt-Svc": ["h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000"]
},
"body": {
"type": "JSON",
"json": {
"sheets": [
{
"properties": {
"sheetId": 0,
"title": "Sheet1",
"index": 0,
"sheetType": "GRID",
"gridProperties": {
"rowCount": 2001,
"columnCount": 26
}
}
},
{
"properties": {
"sheetId": 1911651598,
"title": "Sheet2",
"index": 1,
"sheetType": "GRID",
"gridProperties": {
"rowCount": 1000,
"columnCount": 26
}
}
}
]
},
"rawBytes": "ewogICJzaGVldHMiOiBbCiAgICB7CiAgICAgICJwcm9wZXJ0aWVzIjogewogICAgICAgICJzaGVldElkIjogMCwKICAgICAgICAidGl0bGUiOiAiU2hlZXQxIiwKICAgICAgICAiaW5kZXgiOiAwLAogICAgICAgICJzaGVldFR5cGUiOiAiR1JJRCIsCiAgICAgICAgImdyaWRQcm9wZXJ0aWVzIjogewogICAgICAgICAgInJvd0NvdW50IjogMjAwMSwKICAgICAgICAgICJjb2x1bW5Db3VudCI6IDI2CiAgICAgICAgfQogICAgICB9CiAgICB9LAogICAgewogICAgICAicHJvcGVydGllcyI6IHsKICAgICAgICAic2hlZXRJZCI6IDE5MTE2NTE1OTgsCiAgICAgICAgInRpdGxlIjogIlNoZWV0MiIsCiAgICAgICAgImluZGV4IjogMSwKICAgICAgICAic2hlZXRUeXBlIjogIkdSSUQiLAogICAgICAgICJncmlkUHJvcGVydGllcyI6IHsKICAgICAgICAgICJyb3dDb3VudCI6IDEwMDAsCiAgICAgICAgICAiY29sdW1uQ291bnQiOiAyNgogICAgICAgIH0KICAgICAgfQogICAgfQogIF0KfQo="
}
},
"id": "1756994893547-sheets.googleapis.com-GET-_v4_spreadsheets_1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs-185c9dd7.json",
"priority": 0,
"timeToLive": {
"unlimited": true
},
"times": {
"unlimited": true
}
}

View File

@@ -0,0 +1,66 @@
{
"httpRequest": {
"method": "GET",
"path": "/v4/spreadsheets/1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs/values/'Sheet2'",
"queryStringParameters": {
"valueRenderOption": ["UNFORMATTED_VALUE"],
"dateTimeRenderOption": ["FORMATTED_STRING"]
}
},
"httpResponse": {
"statusCode": 200,
"reasonPhrase": "OK",
"headers": {
"x-l2-request-path": ["l2-managed-6"],
"X-XSS-Protection": ["0"],
"X-Frame-Options": ["SAMEORIGIN"],
"X-Content-Type-Options": ["nosniff"],
"Vary": ["Origin", "X-Origin", "Referer"],
"Server": ["ESF"],
"Date": ["Thu, 04 Sep 2025 14:07:59 GMT"],
"Content-Type": ["application/json; charset=UTF-8"],
"Alt-Svc": ["h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000"]
},
"body": {
"type": "JSON",
"json": {
"range": "Sheet2!A1:Z1000",
"majorDimension": "ROWS",
"values": [
[
"name",
"email",
"actual",
"op",
"output-row_number",
"output-itemIndex",
"output-runIndex",
"data",
"random-output"
],
[
"test",
"test",
10,
"output",
2,
0,
0,
"output-0.17321991314554896",
0.47763178373020865
],
["hello", "wolrd", 104, "", 3, 0, 0, "output-0.14637030644980253", 0.13015058088525477]
]
},
"rawBytes": "ewogICJyYW5nZSI6ICJTaGVldDIhQTE6WjEwMDAiLAogICJtYWpvckRpbWVuc2lvbiI6ICJST1dTIiwKICAidmFsdWVzIjogWwogICAgWwogICAgICAibmFtZSIsCiAgICAgICJlbWFpbCIsCiAgICAgICJhY3R1YWwiLAogICAgICAib3AiLAogICAgICAib3V0cHV0LXJvd19udW1iZXIiLAogICAgICAib3V0cHV0LWl0ZW1JbmRleCIsCiAgICAgICJvdXRwdXQtcnVuSW5kZXgiLAogICAgICAiZGF0YSIsCiAgICAgICJyYW5kb20tb3V0cHV0IgogICAgXSwKICAgIFsKICAgICAgInRlc3QiLAogICAgICAidGVzdCIsCiAgICAgIDEwLAogICAgICAib3V0cHV0IiwKICAgICAgMiwKICAgICAgMCwKICAgICAgMCwKICAgICAgIm91dHB1dC0wLjE3MzIxOTkxMzE0NTU0ODk2IiwKICAgICAgMC40Nzc2MzE3ODM3MzAyMDg2NQogICAgXSwKICAgIFsKICAgICAgImhlbGxvIiwKICAgICAgIndvbHJkIiwKICAgICAgMTA0LAogICAgICAiIiwKICAgICAgMywKICAgICAgMCwKICAgICAgMCwKICAgICAgIm91dHB1dC0wLjE0NjM3MDMwNjQ0OTgwMjUzIiwKICAgICAgMC4xMzAxNTA1ODA4ODUyNTQ3NwogICAgXQogIF0KfQo="
}
},
"id": "1756994893548-sheets.googleapis.com-GET-_v4_spreadsheets_1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs_values__Sheet2_-7746917a.json",
"priority": 0,
"timeToLive": {
"unlimited": true
},
"times": {
"unlimited": true
}
}

View File

@@ -0,0 +1,55 @@
{
"httpRequest": {
"method": "GET",
"path": "/v4/spreadsheets/1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs/values/Sheet2!2:1000",
"queryStringParameters": {
"valueRenderOption": ["UNFORMATTED_VALUE"],
"dateTimeRenderOption": ["FORMATTED_STRING"]
}
},
"httpResponse": {
"statusCode": 200,
"reasonPhrase": "OK",
"headers": {
"x-l2-request-path": ["l2-managed-6"],
"X-XSS-Protection": ["0"],
"X-Frame-Options": ["SAMEORIGIN"],
"X-Content-Type-Options": ["nosniff"],
"Vary": ["Origin", "X-Origin", "Referer"],
"Server": ["ESF"],
"Date": ["Thu, 04 Sep 2025 14:08:00 GMT"],
"Content-Type": ["application/json; charset=UTF-8"],
"Alt-Svc": ["h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000"]
},
"body": {
"type": "JSON",
"json": {
"range": "Sheet2!A2:Z1000",
"majorDimension": "ROWS",
"values": [
[
"test",
"test",
10,
"output",
2,
0,
0,
"output-0.17321991314554896",
0.47763178373020865
],
["hello", "wolrd", 104, "", 3, 0, 0, "output-0.14637030644980253", 0.13015058088525477]
]
},
"rawBytes": "ewogICJyYW5nZSI6ICJTaGVldDIhQTI6WjEwMDAiLAogICJtYWpvckRpbWVuc2lvbiI6ICJST1dTIiwKICAidmFsdWVzIjogWwogICAgWwogICAgICAidGVzdCIsCiAgICAgICJ0ZXN0IiwKICAgICAgMTAsCiAgICAgICJvdXRwdXQiLAogICAgICAyLAogICAgICAwLAogICAgICAwLAogICAgICAib3V0cHV0LTAuMTczMjE5OTEzMTQ1NTQ4OTYiLAogICAgICAwLjQ3NzYzMTc4MzczMDIwODY1CiAgICBdLAogICAgWwogICAgICAiaGVsbG8iLAogICAgICAid29scmQiLAogICAgICAxMDQsCiAgICAgICIiLAogICAgICAzLAogICAgICAwLAogICAgICAwLAogICAgICAib3V0cHV0LTAuMTQ2MzcwMzA2NDQ5ODAyNTMiLAogICAgICAwLjEzMDE1MDU4MDg4NTI1NDc3CiAgICBdCiAgXQp9Cg=="
}
},
"id": "1756994893549-sheets.googleapis.com-GET-_v4_spreadsheets_1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs_values_Sheet2_2_1000-e7fa67bd.json",
"priority": 0,
"timeToLive": {
"unlimited": true
},
"times": {
"unlimited": true
}
}

View File

@@ -0,0 +1,63 @@
{
"httpRequest": {
"method": "POST",
"path": "/v4/spreadsheets/1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs/values:batchUpdate",
"body": {
"contentType": "application/json",
"type": "JSON",
"json": {
"data": [
{
"range": "Sheet2!C2",
"values": [[11]]
}
],
"valueInputOption": "RAW"
},
"rawBytes": "eyJkYXRhIjpbeyJyYW5nZSI6IlNoZWV0MiFDMiIsInZhbHVlcyI6W1sxMV1dfV0sInZhbHVlSW5wdXRPcHRpb24iOiJSQVcifQ=="
}
},
"httpResponse": {
"statusCode": 200,
"reasonPhrase": "OK",
"headers": {
"x-l2-request-path": ["l2-managed-6"],
"X-XSS-Protection": ["0"],
"X-Frame-Options": ["SAMEORIGIN"],
"X-Content-Type-Options": ["nosniff"],
"Vary": ["Origin", "X-Origin", "Referer"],
"Server": ["ESF"],
"Date": ["Thu, 04 Sep 2025 14:08:04 GMT"],
"Content-Type": ["application/json; charset=UTF-8"],
"Alt-Svc": ["h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000"]
},
"body": {
"type": "JSON",
"json": {
"spreadsheetId": "1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs",
"totalUpdatedRows": 1,
"totalUpdatedColumns": 1,
"totalUpdatedCells": 1,
"totalUpdatedSheets": 1,
"responses": [
{
"spreadsheetId": "1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs",
"updatedRange": "Sheet2!C2",
"updatedRows": 1,
"updatedColumns": 1,
"updatedCells": 1
}
]
},
"rawBytes": "ewogICJzcHJlYWRzaGVldElkIjogIjF6SnU4ekx0RmMzcmJaUEFXcUtCaDdUSW91NTlTdEpyN0hMbjBuVXkwYnFzIiwKICAidG90YWxVcGRhdGVkUm93cyI6IDEsCiAgInRvdGFsVXBkYXRlZENvbHVtbnMiOiAxLAogICJ0b3RhbFVwZGF0ZWRDZWxscyI6IDEsCiAgInRvdGFsVXBkYXRlZFNoZWV0cyI6IDEsCiAgInJlc3BvbnNlcyI6IFsKICAgIHsKICAgICAgInNwcmVhZHNoZWV0SWQiOiAiMXpKdTh6THRGYzNyYlpQQVdxS0JoN1RJb3U1OVN0SnI3SExuMG5VeTBicXMiLAogICAgICAidXBkYXRlZFJhbmdlIjogIlNoZWV0MiFDMiIsCiAgICAgICJ1cGRhdGVkUm93cyI6IDEsCiAgICAgICJ1cGRhdGVkQ29sdW1ucyI6IDEsCiAgICAgICJ1cGRhdGVkQ2VsbHMiOiAxCiAgICB9CiAgXQp9Cg=="
}
},
"id": "1756994893550-sheets.googleapis.com-POST-_v4_spreadsheets_1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs_values_batchUpdate-19f43fca.json",
"priority": 0,
"timeToLive": {
"unlimited": true
},
"times": {
"unlimited": true
}
}

View File

@@ -0,0 +1,61 @@
{
"httpRequest": {
"method": "PUT",
"path": "/v4/spreadsheets/1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs/values/Sheet2!1:1",
"body": {
"contentType": "application/json",
"type": "JSON",
"json": {
"range": "Sheet2!1:1",
"values": [
[
"name",
"email",
"actual",
"op",
"output-row_number",
"output-itemIndex",
"output-runIndex",
"data",
"random-output"
]
]
},
"rawBytes": "eyJyYW5nZSI6IlNoZWV0MiExOjEiLCJ2YWx1ZXMiOltbIm5hbWUiLCJlbWFpbCIsImFjdHVhbCIsIm9wIiwib3V0cHV0LXJvd19udW1iZXIiLCJvdXRwdXQtaXRlbUluZGV4Iiwib3V0cHV0LXJ1bkluZGV4IiwiZGF0YSIsInJhbmRvbS1vdXRwdXQiXV19"
}
},
"httpResponse": {
"statusCode": 200,
"reasonPhrase": "OK",
"headers": {
"x-l2-request-path": ["l2-managed-6"],
"X-XSS-Protection": ["0"],
"X-Frame-Options": ["SAMEORIGIN"],
"X-Content-Type-Options": ["nosniff"],
"Vary": ["Origin", "X-Origin", "Referer"],
"Server": ["ESF"],
"Date": ["Thu, 04 Sep 2025 14:08:02 GMT"],
"Content-Type": ["application/json; charset=UTF-8"],
"Alt-Svc": ["h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000"]
},
"body": {
"type": "JSON",
"json": {
"spreadsheetId": "1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs",
"updatedRange": "Sheet2!A1:I1",
"updatedRows": 1,
"updatedColumns": 9,
"updatedCells": 9
},
"rawBytes": "ewogICJzcHJlYWRzaGVldElkIjogIjF6SnU4ekx0RmMzcmJaUEFXcUtCaDdUSW91NTlTdEpyN0hMbjBuVXkwYnFzIiwKICAidXBkYXRlZFJhbmdlIjogIlNoZWV0MiFBMTpJMSIsCiAgInVwZGF0ZWRSb3dzIjogMSwKICAidXBkYXRlZENvbHVtbnMiOiA5LAogICJ1cGRhdGVkQ2VsbHMiOiA5Cn0K"
}
},
"id": "1756994893550-sheets.googleapis.com-PUT-_v4_spreadsheets_1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs_values_Sheet2_1_1-c0a137d1.json",
"priority": 0,
"timeToLive": {
"unlimited": true
},
"times": {
"unlimited": true
}
}

View File

@@ -0,0 +1,44 @@
{
"httpRequest": {
"method": "GET",
"path": "/v4/spreadsheets/1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs/values/Sheet2!3:1000",
"queryStringParameters": {
"valueRenderOption": ["UNFORMATTED_VALUE"],
"dateTimeRenderOption": ["FORMATTED_STRING"]
}
},
"httpResponse": {
"statusCode": 200,
"reasonPhrase": "OK",
"headers": {
"x-l2-request-path": ["l2-managed-6"],
"X-XSS-Protection": ["0"],
"X-Frame-Options": ["SAMEORIGIN"],
"X-Content-Type-Options": ["nosniff"],
"Vary": ["Origin", "X-Origin", "Referer"],
"Server": ["ESF"],
"Date": ["Thu, 04 Sep 2025 14:08:07 GMT"],
"Content-Type": ["application/json; charset=UTF-8"],
"Alt-Svc": ["h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000"]
},
"body": {
"type": "JSON",
"json": {
"range": "Sheet2!A3:Z1000",
"majorDimension": "ROWS",
"values": [
["hello", "wolrd", 104, "", 3, 0, 0, "output-0.14637030644980253", 0.13015058088525477]
]
},
"rawBytes": "ewogICJyYW5nZSI6ICJTaGVldDIhQTM6WjEwMDAiLAogICJtYWpvckRpbWVuc2lvbiI6ICJST1dTIiwKICAidmFsdWVzIjogWwogICAgWwogICAgICAiaGVsbG8iLAogICAgICAid29scmQiLAogICAgICAxMDQsCiAgICAgICIiLAogICAgICAzLAogICAgICAwLAogICAgICAwLAogICAgICAib3V0cHV0LTAuMTQ2MzcwMzA2NDQ5ODAyNTMiLAogICAgICAwLjEzMDE1MDU4MDg4NTI1NDc3CiAgICBdCiAgXQp9Cg=="
}
},
"id": "1756994893551-sheets.googleapis.com-GET-_v4_spreadsheets_1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs_values_Sheet2_3_1000-1bdb2093.json",
"priority": 0,
"timeToLive": {
"unlimited": true
},
"times": {
"unlimited": true
}
}

View File

@@ -0,0 +1,63 @@
{
"httpRequest": {
"method": "POST",
"path": "/v4/spreadsheets/1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs/values:batchUpdate",
"body": {
"contentType": "application/json",
"type": "JSON",
"json": {
"data": [
{
"range": "Sheet2!C3",
"values": [[105]]
}
],
"valueInputOption": "RAW"
},
"rawBytes": "eyJkYXRhIjpbeyJyYW5nZSI6IlNoZWV0MiFDMyIsInZhbHVlcyI6W1sxMDVdXX1dLCJ2YWx1ZUlucHV0T3B0aW9uIjoiUkFXIn0="
}
},
"httpResponse": {
"statusCode": 200,
"reasonPhrase": "OK",
"headers": {
"x-l2-request-path": ["l2-managed-6"],
"X-XSS-Protection": ["0"],
"X-Frame-Options": ["SAMEORIGIN"],
"X-Content-Type-Options": ["nosniff"],
"Vary": ["Origin", "X-Origin", "Referer"],
"Server": ["ESF"],
"Date": ["Thu, 04 Sep 2025 14:08:11 GMT"],
"Content-Type": ["application/json; charset=UTF-8"],
"Alt-Svc": ["h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000"]
},
"body": {
"type": "JSON",
"json": {
"spreadsheetId": "1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs",
"totalUpdatedRows": 1,
"totalUpdatedColumns": 1,
"totalUpdatedCells": 1,
"totalUpdatedSheets": 1,
"responses": [
{
"spreadsheetId": "1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs",
"updatedRange": "Sheet2!C3",
"updatedRows": 1,
"updatedColumns": 1,
"updatedCells": 1
}
]
},
"rawBytes": "ewogICJzcHJlYWRzaGVldElkIjogIjF6SnU4ekx0RmMzcmJaUEFXcUtCaDdUSW91NTlTdEpyN0hMbjBuVXkwYnFzIiwKICAidG90YWxVcGRhdGVkUm93cyI6IDEsCiAgInRvdGFsVXBkYXRlZENvbHVtbnMiOiAxLAogICJ0b3RhbFVwZGF0ZWRDZWxscyI6IDEsCiAgInRvdGFsVXBkYXRlZFNoZWV0cyI6IDEsCiAgInJlc3BvbnNlcyI6IFsKICAgIHsKICAgICAgInNwcmVhZHNoZWV0SWQiOiAiMXpKdTh6THRGYzNyYlpQQVdxS0JoN1RJb3U1OVN0SnI3SExuMG5VeTBicXMiLAogICAgICAidXBkYXRlZFJhbmdlIjogIlNoZWV0MiFDMyIsCiAgICAgICJ1cGRhdGVkUm93cyI6IDEsCiAgICAgICJ1cGRhdGVkQ29sdW1ucyI6IDEsCiAgICAgICJ1cGRhdGVkQ2VsbHMiOiAxCiAgICB9CiAgXQp9Cg=="
}
},
"id": "1756994893552-sheets.googleapis.com-POST-_v4_spreadsheets_1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs_values_batchUpdate-65f181a6.json",
"priority": 0,
"timeToLive": {
"unlimited": true
},
"times": {
"unlimited": true
}
}

View File

@@ -182,7 +182,6 @@ export const test = base.extend<TestFixtures, WorkerFixtures>({
const serverUrl = `http://${proxyServerContainer?.getHost()}:${proxyServerContainer?.getFirstMappedPort()}`; const serverUrl = `http://${proxyServerContainer?.getHost()}:${proxyServerContainer?.getFirstMappedPort()}`;
const proxyServer = new ProxyServer(serverUrl); const proxyServer = new ProxyServer(serverUrl);
await proxyServer.loadExpectations();
await use(proxyServer); await use(proxyServer);
}, },

View File

@@ -4,11 +4,22 @@
import crypto from 'crypto'; import crypto from 'crypto';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import type { Expectation, HttpRequest } from 'mockserver-client'; import type { Expectation, RequestDefinition } from 'mockserver-client';
import { mockServerClient as proxyServerClient } from 'mockserver-client'; import { mockServerClient as proxyServerClient } from 'mockserver-client';
import type { MockServerClient, RequestResponse } from 'mockserver-client/mockServerClient'; import type { HttpRequest, HttpResponse } from 'mockserver-client/mockServer';
import type {
MockServerClient,
PathOrRequestDefinition,
RequestResponse,
} from 'mockserver-client/mockServerClient';
import { join } from 'path'; import { join } from 'path';
export type RequestMade = {
httpRequest?: HttpRequest;
httpResponse?: HttpResponse;
timestamp?: string;
};
export interface ProxyServerRequest { export interface ProxyServerRequest {
method: string; method: string;
path: string; path: string;
@@ -60,17 +71,18 @@ export class ProxyServer {
} }
/** /**
* Load all expectations from the expectations directory and mock them * Load all expectations from the specified subfolder and mock them
*/ */
async loadExpectations(): Promise<void> { async loadExpectations(folderName: string): Promise<void> {
try { try {
const files = await fs.readdir(this.expectationsDir); const targetDir = join(this.expectationsDir, folderName);
const files = await fs.readdir(targetDir);
const jsonFiles = files.filter((file) => file.endsWith('.json')); const jsonFiles = files.filter((file) => file.endsWith('.json'));
const expectations: Expectation[] = []; const expectations: Expectation[] = [];
for (const file of jsonFiles) { for (const file of jsonFiles) {
try { try {
const filePath = join(this.expectationsDir, file); const filePath = join(targetDir, file);
const fileContent = await fs.readFile(filePath, 'utf8'); const fileContent = await fs.readFile(filePath, 'utf8');
const expectation = JSON.parse(fileContent); const expectation = JSON.parse(fileContent);
expectations.push(expectation); expectations.push(expectation);
@@ -108,11 +120,12 @@ export class ProxyServer {
/** /**
* Verify that a request was received by ProxyServer * Verify that a request was received by ProxyServer
*/ */
async verifyRequest(request: ProxyServerRequest, numberOfRequests: number): Promise<boolean> { async verifyRequest(request: RequestDefinition, numberOfRequests: number): Promise<boolean> {
try { try {
await this.client.verify(request, numberOfRequests); await this.client.verify(request, numberOfRequests, numberOfRequests);
return true; return true;
} catch (error) { } catch (error) {
console.log('error', error);
return false; return false;
} }
} }
@@ -120,16 +133,16 @@ export class ProxyServer {
/** /**
* Clear all expectations and logs from ProxyServer * Clear all expectations and logs from ProxyServer
*/ */
async clearProxyServer(): Promise<void> { async clearAllExpectations(): Promise<void> {
try { try {
await this.client.clear(null, 'ALL'); await this.client.clear('', 'ALL');
} catch (error) { } catch (error) {
throw new Error(`Failed to clear ProxyServer: ${JSON.stringify(error)}`); throw new Error(`Failed to clear ProxyServer: ${JSON.stringify(error)}`);
} }
} }
/** /**
* Create a simple GET request expectation with JSON response * Create a request expectation with JSON response
*/ */
async createGetExpectation( async createGetExpectation(
path: string, path: string,
@@ -161,40 +174,50 @@ export class ProxyServer {
} }
/** /**
* Verify a GET request was made to ProxyServer * Verify a request was made to ProxyServer
*/ */
async wasGetRequestMade( async wasRequestMade(request: RequestDefinition, numberOfRequests = 1): Promise<boolean> {
path: string, return await this.verifyRequest(request, numberOfRequests);
queryParams?: Record<string, string>, }
numberOfRequests = 1,
): Promise<boolean> {
const queryStringParameters = queryParams
? Object.entries(queryParams).reduce<Record<string, string[]>>((acc, [key, value]) => {
acc[key] = [value];
return acc;
}, {})
: undefined;
return await this.verifyRequest( async getAllRequestsMade(): Promise<RequestMade[]> {
{ // @ts-expect-error mockserver types seem to be messed up
method: 'GET', return await this.client.retrieveRecordedRequestsAndResponses('');
path,
...(queryStringParameters && { queryStringParameters }),
},
numberOfRequests,
);
} }
/** /**
* Retrieve recorded expectations and write to files * Retrieve recorded expectations and write to files
*
* @param folderName - Target folder name for saving expectation files
* @param options - Optional configuration
* @param options.pathOrRequestDefinition - Filter expectations by path or request definition
* @param options.host - Filter expectations by host name (partial match)
* @param options.dedupe - Remove duplicate expectations based on request
* @param options.raw - Save full original requests (true) or cleaned requests (false, default)
* - raw: false (default) - Saves only essential fields: method, path, queryStringParameters (GET), body (POST/PUT)
* - raw: true - Saves complete original request including all headers and metadata
*/ */
async recordExpectations(request?: HttpRequest): Promise<void> { async recordExpectations(
folderName: string,
options?: {
pathOrRequestDefinition?: PathOrRequestDefinition;
host?: string;
dedupe?: boolean;
raw?: boolean;
},
): Promise<void> {
try { try {
// Retrieve recorded expectations from the mock server // Retrieve recorded expectations from the mock server
const recordedExpectations = await this.client.retrieveRecordedExpectations(request); const recordedExpectations = await this.client.retrieveRecordedExpectations(
options?.pathOrRequestDefinition,
);
// Ensure expectations directory exists // Create target directory path
await fs.mkdir(this.expectationsDir, { recursive: true }); const targetDir = join(this.expectationsDir, folderName);
// Ensure target directory exists
await fs.mkdir(targetDir, { recursive: true });
const seenRequests = new Set<string>();
for (const expectation of recordedExpectations) { for (const expectation of recordedExpectations) {
if ( if (
@@ -208,25 +231,77 @@ export class ProxyServer {
continue; continue;
} }
// Generate unique filename based on request details // Extract host for filename and filtering
const requestData = { const headers = expectation.httpRequest.headers ?? {};
method: expectation.httpRequest?.method, const hostHeader = 'Host' in headers ? headers?.Host : undefined;
path: expectation.httpRequest?.path, const hostName = Array.isArray(hostHeader) ? hostHeader[0] : (hostHeader ?? 'unknown-host');
queryStringParameters: expectation.httpRequest?.queryStringParameters,
headers: expectation.httpRequest?.headers, if (options?.host && typeof hostName === 'string' && !hostName.includes(options.host)) {
continue;
}
const method = expectation.httpRequest.method;
let requestForProcessing: Record<string, unknown> | HttpRequest;
if (options?.raw) {
// Use raw request without cleaning
requestForProcessing = expectation.httpRequest;
} else {
// Clean up the request data
const cleanedRequest: Record<string, unknown> = {
method: expectation.httpRequest.method,
path: expectation.httpRequest.path,
};
// Include different fields based on method
if (method === 'GET') {
// For GET requests, include queryStringParameters if present
if (expectation.httpRequest.queryStringParameters) {
cleanedRequest.queryStringParameters = expectation.httpRequest.queryStringParameters;
}
} else if (method === 'POST' || method === 'PUT') {
// For POST/PUT requests, include body if present
if (expectation.httpRequest.body) {
cleanedRequest.body = expectation.httpRequest.body;
}
}
requestForProcessing = cleanedRequest;
}
// Dedupe expectations if requested
if (options?.dedupe) {
const dedupeKey = JSON.stringify(requestForProcessing);
if (seenRequests.has(dedupeKey)) {
continue;
}
seenRequests.add(dedupeKey);
}
// Create expectation (cleaned or raw)
const processedExpectation: Expectation = {
...expectation,
httpRequest: requestForProcessing,
times: {
unlimited: true,
},
}; };
// Generate unique filename based on request details
const hash = crypto const hash = crypto
.createHash('sha256') .createHash('sha256')
.update(JSON.stringify(requestData)) .update(JSON.stringify(requestForProcessing))
.digest('hex') .digest('hex')
.substring(0, 8); .substring(0, 8);
const filename = `${expectation.httpRequest?.method?.toString()}-${expectation.httpRequest?.path?.replace(/[^a-zA-Z0-9]/g, '_')}-${hash}.json`; const filename = `${Date.now()}-${hostName}-${method}-${expectation.httpRequest.path.replace(/[^a-zA-Z0-9]/g, '_')}-${hash}.json`;
const filePath = join(this.expectationsDir, filename); processedExpectation.id = filename;
const filePath = join(targetDir, filename);
// Write expectation to JSON file // Write expectation to JSON file
await fs.writeFile(filePath, JSON.stringify(expectation, null, 2)); await fs.writeFile(filePath, JSON.stringify(processedExpectation, null, 2));
} }
} catch (error) { } catch (error) {
throw new Error(`Failed to record expectations: ${JSON.stringify(error)}`); throw new Error(`Failed to record expectations: ${JSON.stringify(error)}`);

View File

@@ -4,6 +4,10 @@ import { test, expect } from '../../fixtures/base';
// @capability:proxy tag ensures that test suite is only run when proxy is available // @capability:proxy tag ensures that test suite is only run when proxy is available
test.describe('Proxy server @capability:proxy', () => { test.describe('Proxy server @capability:proxy', () => {
test.beforeEach(async ({ proxyServer }) => {
await proxyServer.clearAllExpectations();
});
test('should verify ProxyServer container is running', async ({ proxyServer }) => { test('should verify ProxyServer container is running', async ({ proxyServer }) => {
const mockResponse = await proxyServer.createGetExpectation('/health', { const mockResponse = await proxyServer.createGetExpectation('/health', {
status: 'healthy', status: 'healthy',
@@ -12,7 +16,7 @@ test.describe('Proxy server @capability:proxy', () => {
assert(typeof mockResponse !== 'string'); assert(typeof mockResponse !== 'string');
expect(mockResponse.statusCode).toBe(201); expect(mockResponse.statusCode).toBe(201);
expect(await proxyServer.wasGetRequestMade('/health')).toBe(false); expect(await proxyServer.wasRequestMade({ method: 'GET', path: '/health' })).toBe(false);
// Verify the mock endpoint works // Verify the mock endpoint works
const healthResponse = await fetch(`${proxyServer.url}/health`); const healthResponse = await fetch(`${proxyServer.url}/health`);
@@ -20,7 +24,7 @@ test.describe('Proxy server @capability:proxy', () => {
const healthData = await healthResponse.json(); const healthData = await healthResponse.json();
expect(healthData.status).toBe('healthy'); expect(healthData.status).toBe('healthy');
expect(await proxyServer.wasGetRequestMade('/health')).toBe(true); expect(await proxyServer.wasRequestMade({ method: 'GET', path: '/health' })).toBe(true);
}); });
test('should run a simple workflow calling http endpoint', async ({ n8n, proxyServer }) => { test('should run a simple workflow calling http endpoint', async ({ n8n, proxyServer }) => {
@@ -41,18 +45,22 @@ test.describe('Proxy server @capability:proxy', () => {
// Verify the request was handled by mockserver // Verify the request was handled by mockserver
expect( expect(
await proxyServer.wasGetRequestMade('/data', { await proxyServer.wasRequestMade({
test: '1', method: 'GET',
path: '/data',
queryStringParameters: { test: ['1'] },
}), }),
).toBe(true); ).toBe(true);
}); });
test('should use stored expectations respond to api request', async ({ proxyServer }) => { test('should use stored expectations respond to api request', async ({ proxyServer }) => {
await proxyServer.loadExpectations('proxy-server');
const response = await fetch(`${proxyServer.url}/mock-endpoint`); const response = await fetch(`${proxyServer.url}/mock-endpoint`);
expect(response.ok).toBe(true); expect(response.ok).toBe(true);
const data = await response.json(); const data = await response.json();
expect(data.title).toBe('delectus aut autem'); expect(data.title).toBe('delectus aut autem');
expect(await proxyServer.wasGetRequestMade('/mock-endpoint')).toBe(true); expect(await proxyServer.wasRequestMade({ method: 'GET', path: '/mock-endpoint' })).toBe(true);
}); });
test('should run a simple workflow proxying HTTPS request', async ({ n8n }) => { test('should run a simple workflow proxying HTTPS request', async ({ n8n }) => {

View File

@@ -0,0 +1,112 @@
import { expect, test } from '../../fixtures/base';
test.describe('Evaluations @capability:proxy', () => {
test.beforeEach(async ({ n8n, proxyServer }) => {
await proxyServer.clearAllExpectations();
await n8n.goHome();
await n8n.workflows.clickAddWorkflowButton();
});
test('should load evaluations workflow and execute twice', async ({ n8n, api, proxyServer }) => {
await proxyServer.loadExpectations('evaluations');
await api.credentialApi.createCredentialFromDefinition({
name: 'Test Google Sheets',
type: 'googleApi',
data: {
email: 'email@quickstart-1234.iam.gserviceaccount.com',
// mock private key
privateKey: `-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDx1//AaoSkyHYl
npqS3+uaePYhJXKD/T1h6zGThAUooN7ZzWK46nNcU1vghQMTlPMHfUTbl4xzZxEL
OYjyTPOKpwJvhmy44MU+zTQYJuUaU4dQuOCnnC61CL91Xy+8GJd7PvdUeVRWENWu
zzO825Fxeiy2qnbrOJfhYh+f9znwWM2R8/V6LIp1HSWNBU0h/NCesmVhGwTP2H/P
wGgFPzl9+effW8TgmAukVuZoG+z8pOiqJnZLgTOO++PLyM6UJe560UnAbv0yP4y5
lZ370XwOQ6gVIiB0+8Z2A3tJp6ackfoMfDYbuU+CAhFPqkdvXgbrYciUCr6fzINo
ImK6CcSDAgMBAAECggEAF0+XokdiI7QC11tzUMbuocQZDVbbs+c7/G08KRjnmmPv
NxU599L5baPHTlvj0QZhao5jjbsM2a7MkMVp8tkB/JJehLtzTVq1CHmlFNLi8Geu
ulQnq2A9jEuckMatBjdkmoeWNXlAbM9QmXn1ZbXQThzVpIHH1qJs2Veo7rVYy1bD
+hnzadyeXsHOC518wNAaF3b1UShybI3dlrHbXqqRmkOZP272IKfmvZ2KOcnFC+MT
cWLUGWBTq2YK+UJv09OXHEBnonrm18m2Sku+/PhFwjOiifIK/1MWILss60IB7dFm
7Fe7NAtYQMPZyDEqY5Xo+K4FwWYzfxfHPiJf7k0DqQKBgQD5Rz+HCZC8V5c1oK8/
1hGthyh5JdXxW7C8D1WVuo7W2OHrOJSDXjGhsxMjnKYdq/1YybJl9XpQSvZeumto
YazNiJqAexIlpmEHLW5gDtX3xpM0dujuJudTHYfveugtR8i/EZpWpFKv45/6Rm33
Yt2PaMjLuO7yW0buEjSQInHtHwKBgQD4XW44YujgF+xvMmx8+QyyNI2UNI1ZmnsU
VZLmDAn5+WDz5YtBXN9JGIXIk5279S7xzu9xyq7Ih6uedxE/hmzaHSZ1gl9Xasci
n86FGaGPm6RtEeZ8c68oqha7kddLoBwTPBoZq5NaCCaTh2TQkMPg+Ws3erM0pkyC
fqw1hzkYHQKBgQC2Iv3i3/VV+DXupCqIXRRrkx7abe/FO3aF4jppfXdSugNQR/YT
imZ/PIXWdmXVtk4VasIjx1oIgs1C57kE+qE1SAODrujSg5/Pi71jCFQEh54VLnEB
WYGZ9DDXpRkxxIqEOQtpFQWpqIrCZmWA5Ub3uttEJyrIADNyTfEEA3b0hwKBgHrn
STbQA2t5iz/PlQ4W9GhvRyxzAQu5PXTnj+UVSg6QkKDBE7NJsRjr8LA8FE9B2nRA
sg7+fJWxRYUKaNelvtIEoNZ/qIyKw3Zn3HvTHjcBj1GGDSfC24fk+5Dgb8j1t07x
a/0OAcIIzIYu9v2a1cPLyXnP10STksL0ymVGwEMlAoGBAK2dtYZllhooN/C4ssFW
nmfqICLWEc/UZSxmxau1rOz71GJiiHgXFmQgiZtpf3Qp3wKKtoFkf+sJ6zP2VX35
2tJcTO9lKm6kNa3eaveE/NJrkH5a0IpxrvDT1TvmnapaNEKuGZJAX5BNaggDrfEJ
m82JpEptTfAxFHtd8+Sb0U2G
-----END PRIVATE KEY-----`,
},
});
// Import the evaluations workflow
await n8n.canvas.importWorkflow('evaluations_loop.json', 'Evaluations');
// Open each node to ensure credentials are set
await n8n.canvas.openNode('When fetching a dataset row');
await n8n.page.keyboard.press('Escape');
// Open each node to ensure credentials are set
await n8n.canvas.openNode('Set outputs');
await n8n.page.keyboard.press('Escape');
// Execute workflow from canvas - first execution
await n8n.canvas.clickExecuteWorkflowButton();
// wait for first run to finish
await n8n.notifications.waitForNotificationAndClose('Successful', { timeout: 10000 });
// wait for second run to finish
await n8n.notifications.waitForNotificationAndClose('Successful', { timeout: 10000 });
// 💡 To update recordings, remove stored expectations, set real credentials above and rerecord here.
// await proxyServer.recordExpectations('evaluations', { host: 'google', dedupe: true });
const batchUpdateRequests = (await proxyServer.getAllRequestsMade()).filter((request) => {
const path = request.httpRequest?.path;
const method = request.httpRequest?.method;
return method === 'POST' && typeof path === 'string' && path.endsWith('/values:batchUpdate');
});
/**
* Original Table in Google Sheets
* The loop should execute twice over both rows here
* Incrementing each value by 1 (expression in Set Output node)
*
* name email actual
test test 10
hello wolrd 104
*/
// Set output node was called twice in a loop, updating Google sheets output value
expect(batchUpdateRequests.length).toEqual(2);
expect((batchUpdateRequests[0]?.httpRequest?.body as { json: object })?.json).toEqual({
data: [
{
range: 'Sheet2!C2',
values: [[11]],
},
],
valueInputOption: 'RAW',
});
expect((batchUpdateRequests[1]?.httpRequest?.body as { json: object })?.json).toEqual({
data: [
{
range: 'Sheet2!C3',
values: [[105]],
},
],
valueInputOption: 'RAW',
});
});
});

View File

@@ -0,0 +1,160 @@
{
"nodes": [
{
"parameters": {
"documentId": {
"__rl": true,
"value": "1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs",
"mode": "list",
"cachedResultName": "Evaluation test - mutasem",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs/edit?usp=drivesdk"
},
"sheetName": {
"__rl": true,
"value": 1911651598,
"mode": "list",
"cachedResultName": "Sheet2",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs/edit#gid=1911651598"
}
},
"type": "n8n-nodes-base.evaluationTrigger",
"typeVersion": 4.6,
"position": [0, 0],
"id": "2353d300-628d-4e9f-86ad-89b3b78bf02f",
"name": "When fetching a dataset row",
"credentials": {
"googleSheetsOAuth2Api": {
"id": "DQuAchCa7lPMNsOG",
"name": "Google Sheets account"
}
}
},
{
"parameters": {
"documentId": {
"__rl": true,
"value": "1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs",
"mode": "list",
"cachedResultName": "Evaluation test - mutasem",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs/edit?usp=drivesdk"
},
"sheetName": {
"__rl": true,
"value": 1911651598,
"mode": "list",
"cachedResultName": "Sheet2",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1zJu8zLtFc3rbZPAWqKBh7TIou59StJr7HLn0nUy0bqs/edit#gid=1911651598"
},
"outputs": {
"values": [
{
"outputName": "actual",
"outputValue": "={{ parseInt($json.actual) + 1 }}"
}
]
}
},
"type": "n8n-nodes-base.evaluation",
"typeVersion": 4.7,
"position": [640, 0],
"id": "47bf60d4-1c60-4dcc-a9d7-af0c02c84db3",
"name": "Set outputs",
"credentials": {
"googleSheetsOAuth2Api": {
"id": "DQuAchCa7lPMNsOG",
"name": "Google Sheets account"
}
}
},
{
"parameters": {
"amount": 1
},
"type": "n8n-nodes-base.wait",
"typeVersion": 1.1,
"position": [208, 0],
"id": "2b5f3437-19bc-4d2e-ac60-52bfbb0fec1b",
"name": "Wait",
"webhookId": "26826666-e8e8-492e-9619-49e604fc90ee"
},
{
"parameters": {
"operation": "setInputs",
"inputs": {
"values": [
{
"inputName": "input",
"inputValue": "test"
}
]
}
},
"type": "n8n-nodes-base.evaluation",
"typeVersion": 4.7,
"position": [864, 0],
"id": "e027893e-4150-42dc-8b1e-8c24f5060cd1",
"name": "set inptus"
},
{
"parameters": {
"operation": "checkIfEvaluating"
},
"type": "n8n-nodes-base.evaluation",
"typeVersion": 4.7,
"position": [416, 0],
"id": "7cc3b2fb-985b-437d-a0c7-0375f3717ee1",
"name": "Evaluation"
}
],
"connections": {
"When fetching a dataset row": {
"main": [
[
{
"node": "Wait",
"type": "main",
"index": 0
}
]
]
},
"Set outputs": {
"main": [
[
{
"node": "set inptus",
"type": "main",
"index": 0
}
]
]
},
"Wait": {
"main": [
[
{
"node": "Evaluation",
"type": "main",
"index": 0
}
]
]
},
"Evaluation": {
"main": [
[
{
"node": "Set outputs",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {},
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "f0e9801eba0feea6a9ddf9beeabe34b0843eae42a1dbc62eaadd68e8f576be64"
}
}