Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 0.2.3

- Add a credential test to the BatchData API credential (n8n Creator Portal review: "missing credential test"). It sends `GET /api/v2/property/search-sessions?take=1` with the Bearer token from `authenticate`: a valid token returns 200 and a bad token returns 401. The call is read-only and does not bill property records. 401, 403 and 429 get clear messages.
- Remove the node-level `testedBy` credential test methods, so the credential's own test is the single check for every node and the HTTP Request node.


## 0.2.2

- Pass `npx @n8n/scan-community-package`: move input-error throws out of `catch` blocks, prefer `httpRequest` in the credential test, and rename the Custom API Call operation to "Make" so the official scanner (which ignores eslint-disable comments) is clean.
Expand Down
41 changes: 40 additions & 1 deletion credentials/BatchDataApi.credentials.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import type { IAuthenticateGeneric, Icon, ICredentialType, INodeProperties } from 'n8n-workflow';
import type {
IAuthenticateGeneric,
Icon,
ICredentialTestRequest,
ICredentialType,
INodeProperties,
} from 'n8n-workflow';

export class BatchDataApi implements ICredentialType {
name = 'batchDataApi';
Expand Down Expand Up @@ -33,4 +39,37 @@ export class BatchDataApi implements ICredentialType {
},
},
};

// Lists search sessions (read-only, does not bill property records): a valid token gets
// 200, a bad token gets 401. The Bearer header from `authenticate` is applied to this request.
test: ICredentialTestRequest = {
request: {
baseURL: 'https://api.batchdata.com/api',
url: '/v2/property/search-sessions',
method: 'GET',
headers: { Accept: 'application/json' },
qs: { take: 1 },
},
rules: [
{
type: 'responseCode',
properties: { value: 401, message: 'The API token was rejected (401). Check the token.' },
},
{
type: 'responseCode',
properties: {
value: 403,
message:
'The API token was refused (403). The credential test lists search sessions, so the token needs the property search sessions permission. Questions: support@batchservice.com',
},
},
{
type: 'responseCode',
properties: {
value: 429,
message: 'Rate limit reached (429). Try the test again shortly.',
},
},
],
};
}
3 changes: 0 additions & 3 deletions nodes/BatchData/BatchData.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import {
RESOURCE_VALUE,
} from './catalogProperties';
import { API_HOST } from './constants';
import { credentialTestMethods } from './credentialMethods';
import {
addressFields,
addressOperations,
Expand Down Expand Up @@ -107,7 +106,6 @@ export class BatchData implements INodeType {
{
name: 'batchDataApi',
required: true,
testedBy: 'batchDataApiTest',
},
],
properties: [
Expand Down Expand Up @@ -153,7 +151,6 @@ export class BatchData implements INodeType {
return await listForDropdown(this, '/api/v2/property-subscription', 'subscriptions');
},
},
credentialTest: credentialTestMethods,
};

async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
Expand Down
73 changes: 0 additions & 73 deletions nodes/BatchData/credentialMethods.ts

This file was deleted.

29 changes: 0 additions & 29 deletions nodes/BatchData/credentialTest.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';
import { collectInputWith } from '../BatchData/catalogExecute';
import { buildRequest, getPath, setPath, toBoolean } from '../BatchData/catalogEngine';
import { catalog, opProperties } from '../BatchData/catalogProperties';
import { credentialTestMethods } from '../BatchData/credentialMethods';
import { batchDataRequest, toNodeApiError } from '../BatchData/GenericFunctions';
import { eventRecord, rememberIds, subscriptionIdFrom } from '../BatchData/triggerSupport';

Expand All @@ -30,7 +29,7 @@ export class BatchDataMonitorTrigger implements INodeType {
defaults: { name: 'BatchData Monitor Trigger' },
inputs: [],
outputs: [NodeConnectionTypes.Main],
credentials: [{ name: 'batchDataApi', required: true, testedBy: 'batchDataApiTest' }],
credentials: [{ name: 'batchDataApi', required: true }],
webhooks: [
{
name: 'default',
Expand All @@ -50,8 +49,6 @@ export class BatchDataMonitorTrigger implements INodeType {
],
};

methods = { credentialTest: credentialTestMethods };

webhookMethods = {
default: {
async checkExists(this: IHookFunctions): Promise<boolean> {
Expand Down
5 changes: 1 addition & 4 deletions nodes/BatchDataTrigger/BatchDataTrigger.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';
import { collectInputWith } from '../BatchData/catalogExecute';
import { buildRequest, extractResult } from '../BatchData/catalogEngine';
import { catalog, opProperties } from '../BatchData/catalogProperties';
import { credentialTestMethods } from '../BatchData/credentialMethods';
import { batchDataRequest, toNodeApiError } from '../BatchData/GenericFunctions';
import { pollRecords, recordId, rememberIds } from '../BatchData/triggerSupport';

Expand All @@ -30,7 +29,7 @@ export class BatchDataTrigger implements INodeType {
polling: true,
inputs: [],
outputs: [NodeConnectionTypes.Main],
credentials: [{ name: 'batchDataApi', required: true, testedBy: 'batchDataApiTest' }],
credentials: [{ name: 'batchDataApi', required: true }],
properties: [
{
displayName: 'Event',
Expand All @@ -48,8 +47,6 @@ export class BatchDataTrigger implements INodeType {
],
};

methods = { credentialTest: credentialTestMethods };

async poll(this: IPollFunctions): Promise<INodeExecutionData[][] | null> {
const event = this.getNodeParameter('event') as string;
const op = pollingOps.find((entry) => entry.key === event);
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "n8n-nodes-batchdata",
"version": "0.2.2",
"version": "0.2.3",
"description": "n8n community nodes for the BatchData API: property search and lookup, skip trace, reverse skip trace, permits, investors, address and phone checks, search sessions, property monitors, polling and instant triggers",
"license": "MIT",
"homepage": "https://github.com/batchdataco/n8n-nodes-batchdata#readme",
Expand Down
4 changes: 2 additions & 2 deletions test/catalog.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -409,9 +409,9 @@ test('row-level Your Reference ID reaches the request in batch modules', async (
}
});

test('package metadata points at this GitHub repo and is a 0.2.2 release', () => {
test('package metadata points at this GitHub repo and is a 0.2.3 release', () => {
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
assert.equal(pkg.version, '0.2.2');
assert.equal(pkg.version, '0.2.3');
assert.equal(pkg.repository.url, 'git+https://github.com/batchdataco/n8n-nodes-batchdata.git');
assert.match(pkg.homepage, /github\.com\/batchdataco\/n8n-nodes-batchdata/);
assert.ok(pkg.keywords.includes('n8n-community-node-package'));
Expand Down
65 changes: 28 additions & 37 deletions test/unit.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,39 @@ import { test } from 'node:test';

import { BatchData } from '../dist/nodes/BatchData/BatchData.node.js';
import { BatchDataApi } from '../dist/credentials/BatchDataApi.credentials.js';
import { evaluateCredentialProbe } from '../dist/nodes/BatchData/credentialTest.js';
import { QUICKLISTS } from '../dist/nodes/BatchData/constants.js';

test('credential probe: 400 and 2xx are valid; 401/403 are not', () => {
assert.equal(evaluateCredentialProbe(400).status, 'OK');
assert.equal(evaluateCredentialProbe(200).status, 'OK');
assert.equal(evaluateCredentialProbe(401).status, 'Error');
assert.equal(evaluateCredentialProbe(403).status, 'Error');
assert.equal(evaluateCredentialProbe(429).status, 'Error');
assert.equal(evaluateCredentialProbe(500).status, 'Error');
assert.equal(evaluateCredentialProbe(0).status, 'Error');
test('credential defines a free test request: GET search sessions, one result', () => {
const { test: credTest } = new BatchDataApi();
assert.ok(credTest, 'credential has a test');
const { request } = credTest;
assert.equal(request.method, 'GET');
assert.equal(
`${request.baseURL}${request.url}`,
'https://api.batchdata.com/api/v2/property/search-sessions',
);
assert.deepEqual(request.qs, { take: 1 });
assert.equal(request.body, undefined);
// Auth comes from `authenticate`, not a hard-coded header on the test request.
assert.equal(request.headers?.Authorization, undefined);
const codes = credTest.rules
.filter((r) => r.type === 'responseCode')
.map((r) => r.properties.value);
assert.deepEqual(codes.sort(), [401, 403, 429]);
});

test('credential test method posts an empty batch with the bearer token', async () => {
const node = new BatchData();
const seen = [];
const make = (statusCode, helper = 'httpRequest') => ({
helpers: {
[helper]: async (options) => {
seen.push(options);
return { statusCode };
},
},
});
const credential = { data: { apiToken: ' secret-token ' } };
const ok = await node.methods.credentialTest.batchDataApiTest.call(make(400), credential);
assert.equal(ok.status, 'OK');
assert.equal(seen[0].url, 'https://api.batchdata.com/api/v1/address/verify');
assert.deepEqual(seen[0].body, { requests: [] });
assert.equal(seen[0].headers.Authorization, 'Bearer secret-token');
const bad = await node.methods.credentialTest.batchDataApiTest.call(make(401), credential);
assert.equal(bad.status, 'Error');
const empty = await node.methods.credentialTest.batchDataApiTest.call(make(200), { data: {} });
assert.equal(empty.status, 'Error');
seen.length = 0;
const legacy = await node.methods.credentialTest.batchDataApiTest.call(
make(400, 'request'),
credential,
test('nodes rely on the credential test, not a node-level testedBy method', async () => {
const { BatchDataTrigger } = await import(
'../dist/nodes/BatchDataTrigger/BatchDataTrigger.node.js'
);
const { BatchDataMonitorTrigger } = await import(
'../dist/nodes/BatchDataMonitorTrigger/BatchDataMonitorTrigger.node.js'
);
assert.equal(legacy.status, 'OK');
assert.equal(seen[0].uri, 'https://api.batchdata.com/api/v1/address/verify');
for (const node of [new BatchData(), new BatchDataTrigger(), new BatchDataMonitorTrigger()]) {
assert.equal(node.description.credentials[0].name, 'batchDataApi');
assert.equal(node.description.credentials[0].testedBy, undefined);
assert.equal(node.methods?.credentialTest, undefined);
}
});

test('credential injects a Bearer header and declares a password field', () => {
Expand Down Expand Up @@ -74,7 +66,6 @@ test('node description exposes the planned resources and no wallet or webhook fi
names.some((n) => n.includes('webhook') || n.includes('callback')),
false,
);
assert.equal(description.credentials[0].testedBy, 'batchDataApiTest');
});

test('quicklist constants have no duplicates', () => {
Expand Down
Loading