@seqera/node-red-seqera
Version:
Node-RED nodes for interacting with the Seqera Platform API
620 lines (553 loc) • 18 kB
JavaScript
/**
* Tests for nodes/datalink-list.js (seqera-datalink-list node)
*
* Tests the Data Link file listing functionality including:
* - Resolving Data Link by name
* - Listing files with filtering by prefix and pattern
* - Recursive depth support
* - Return type filtering (files, folders, all)
*/
const {
helper,
nock,
DEFAULT_BASE_URL,
DEFAULT_WORKSPACE_ID,
createConfigNode,
createCredentials,
createDataLinksResponse,
createDataBrowserResponse,
} = require("./helper");
const { expect } = require("chai");
const datalinkListNode = require("../nodes/datalink-list");
const configNode = require("../nodes/config");
describe("seqera-datalink-list Node", function () {
beforeEach(function (done) {
helper.startServer(done);
});
afterEach(function (done) {
helper.unload();
helper.stopServer(done);
nock.cleanAll();
});
describe("node loading", function () {
it("should be loaded with correct type", function (done) {
const flow = [
createConfigNode(),
{
id: "datalink1",
type: "seqera-datalink-list",
name: "Test Datalink List",
seqera: "config-node-1",
dataLinkName: "my-datalink",
dataLinkNameType: "str",
wires: [[]],
},
];
helper.load([configNode, datalinkListNode], flow, createCredentials(), function () {
const n1 = helper.getNode("datalink1");
try {
expect(n1).to.exist;
expect(n1.type).to.equal("seqera-datalink-list");
expect(n1.name).to.equal("Test Datalink List");
done();
} catch (err) {
done(err);
}
});
});
it("should store configuration properties", function (done) {
const flow = [
createConfigNode(),
{
id: "datalink1",
type: "seqera-datalink-list",
name: "Test Datalink List",
seqera: "config-node-1",
dataLinkName: "my-datalink",
dataLinkNameType: "str",
depth: "2",
depthType: "num",
maxResults: "50",
maxResultsType: "num",
returnType: "all",
wires: [[]],
},
];
helper.load([configNode, datalinkListNode], flow, createCredentials(), function () {
const n1 = helper.getNode("datalink1");
try {
expect(n1.dataLinkNameProp).to.equal("my-datalink");
expect(n1.depthProp).to.equal("2");
expect(n1.maxResultsProp).to.equal("50");
expect(n1.returnType).to.equal("all");
done();
} catch (err) {
done(err);
}
});
});
it("should default returnType to files", function (done) {
const flow = [
createConfigNode(),
{
id: "datalink1",
type: "seqera-datalink-list",
name: "Test Datalink List",
seqera: "config-node-1",
wires: [[]],
},
];
helper.load([configNode, datalinkListNode], flow, createCredentials(), function () {
const n1 = helper.getNode("datalink1");
try {
expect(n1.returnType).to.equal("files");
done();
} catch (err) {
done(err);
}
});
});
});
describe("listing files", function () {
it("should resolve datalink and list files", function (done) {
const flow = [
createConfigNode(),
{
id: "datalink1",
type: "seqera-datalink-list",
name: "Test Datalink List",
seqera: "config-node-1",
dataLinkName: "my-datalink",
dataLinkNameType: "str",
maxResults: "100",
maxResultsType: "num",
depth: "0",
depthType: "num",
returnType: "all",
wires: [["helper1"]],
},
{ id: "helper1", type: "helper" },
];
// Mock data-links search
nock(DEFAULT_BASE_URL)
.get("/data-links/")
.query(true)
.reply(
200,
createDataLinksResponse([
{
id: "dl-123",
name: "my-datalink",
type: "S3",
provider: "aws",
resourceRef: "s3://bucket/data",
credentials: [{ id: "cred-1" }],
},
]),
);
// Mock browse files
nock(DEFAULT_BASE_URL)
.get(/\/data-links\/dl-123\/browse/)
.query(true)
.reply(
200,
createDataBrowserResponse([
{ name: "file1.txt", type: "FILE", size: 1024 },
{ name: "file2.csv", type: "FILE", size: 2048 },
]),
);
helper.load([configNode, datalinkListNode], flow, createCredentials(), function () {
const datalinkNode = helper.getNode("datalink1");
const helperNode = helper.getNode("helper1");
helperNode.on("input", function (msg) {
try {
expect(msg.payload.files).to.be.an("array");
expect(msg.payload.files).to.have.length(2);
expect(msg.files).to.be.an("array");
expect(msg.files).to.have.length(2);
expect(msg.files[0]).to.include("s3://bucket/data");
expect(msg.payload.resourceRef).to.equal("s3://bucket/data");
expect(msg.payload.provider).to.equal("aws");
done();
} catch (err) {
done(err);
}
});
datalinkNode.receive({ payload: {} });
});
});
it("should filter by prefix", function (done) {
const flow = [
createConfigNode(),
{
id: "datalink1",
type: "seqera-datalink-list",
name: "Test Datalink List",
seqera: "config-node-1",
dataLinkName: "my-datalink",
dataLinkNameType: "str",
prefix: "data_",
prefixType: "str",
returnType: "all",
wires: [["helper1"]],
},
{ id: "helper1", type: "helper" },
];
nock(DEFAULT_BASE_URL)
.get("/data-links/")
.query(true)
.reply(
200,
createDataLinksResponse([{ id: "dl-123", name: "my-datalink", resourceRef: "s3://bucket", credentials: [] }]),
);
// Mock browse with search parameter
nock(DEFAULT_BASE_URL)
.get(/\/data-links\/dl-123\/browse/)
.query((q) => q.search === "data_")
.reply(200, createDataBrowserResponse([{ name: "data_file1.txt", type: "FILE" }]));
helper.load([configNode, datalinkListNode], flow, createCredentials(), function () {
const datalinkNode = helper.getNode("datalink1");
const helperNode = helper.getNode("helper1");
helperNode.on("input", function (msg) {
try {
expect(msg.payload.files).to.have.length(1);
expect(msg.payload.files[0].name).to.equal("data_file1.txt");
done();
} catch (err) {
done(err);
}
});
datalinkNode.receive({ payload: {} });
});
});
it("should filter by regex pattern", function (done) {
const flow = [
createConfigNode(),
{
id: "datalink1",
type: "seqera-datalink-list",
name: "Test Datalink List",
seqera: "config-node-1",
dataLinkName: "my-datalink",
dataLinkNameType: "str",
pattern: "\\.csv$",
patternType: "str",
returnType: "all",
wires: [["helper1"]],
},
{ id: "helper1", type: "helper" },
];
nock(DEFAULT_BASE_URL)
.get("/data-links/")
.query(true)
.reply(
200,
createDataLinksResponse([{ id: "dl-123", name: "my-datalink", resourceRef: "s3://bucket", credentials: [] }]),
);
nock(DEFAULT_BASE_URL)
.get(/\/data-links\/dl-123\/browse/)
.query(true)
.reply(
200,
createDataBrowserResponse([
{ name: "file1.txt", type: "FILE" },
{ name: "file2.csv", type: "FILE" },
{ name: "file3.csv", type: "FILE" },
]),
);
helper.load([configNode, datalinkListNode], flow, createCredentials(), function () {
const datalinkNode = helper.getNode("datalink1");
const helperNode = helper.getNode("helper1");
helperNode.on("input", function (msg) {
try {
// Should only return .csv files
expect(msg.payload.files).to.have.length(2);
expect(msg.payload.files[0].name).to.match(/\.csv$/);
expect(msg.payload.files[1].name).to.match(/\.csv$/);
done();
} catch (err) {
done(err);
}
});
datalinkNode.receive({ payload: {} });
});
});
it("should filter return type to files only", function (done) {
const flow = [
createConfigNode(),
{
id: "datalink1",
type: "seqera-datalink-list",
name: "Test Datalink List",
seqera: "config-node-1",
dataLinkName: "my-datalink",
dataLinkNameType: "str",
returnType: "files",
wires: [["helper1"]],
},
{ id: "helper1", type: "helper" },
];
nock(DEFAULT_BASE_URL)
.get("/data-links/")
.query(true)
.reply(
200,
createDataLinksResponse([{ id: "dl-123", name: "my-datalink", resourceRef: "s3://bucket", credentials: [] }]),
);
nock(DEFAULT_BASE_URL)
.get(/\/data-links\/dl-123\/browse/)
.query(true)
.reply(
200,
createDataBrowserResponse([
{ name: "file1.txt", type: "FILE" },
{ name: "folder1", type: "FOLDER" },
{ name: "file2.csv", type: "FILE" },
]),
);
helper.load([configNode, datalinkListNode], flow, createCredentials(), function () {
const datalinkNode = helper.getNode("datalink1");
const helperNode = helper.getNode("helper1");
helperNode.on("input", function (msg) {
try {
// Should only return files, not folders
expect(msg.payload.files).to.have.length(2);
msg.payload.files.forEach((f) => {
expect(f.type).to.equal("FILE");
});
done();
} catch (err) {
done(err);
}
});
datalinkNode.receive({ payload: {} });
});
});
it("should filter return type to folders only", function (done) {
const flow = [
createConfigNode(),
{
id: "datalink1",
type: "seqera-datalink-list",
name: "Test Datalink List",
seqera: "config-node-1",
dataLinkName: "my-datalink",
dataLinkNameType: "str",
returnType: "folders",
wires: [["helper1"]],
},
{ id: "helper1", type: "helper" },
];
nock(DEFAULT_BASE_URL)
.get("/data-links/")
.query(true)
.reply(
200,
createDataLinksResponse([{ id: "dl-123", name: "my-datalink", resourceRef: "s3://bucket", credentials: [] }]),
);
nock(DEFAULT_BASE_URL)
.get(/\/data-links\/dl-123\/browse/)
.query(true)
.reply(
200,
createDataBrowserResponse([
{ name: "file1.txt", type: "FILE" },
{ name: "folder1", type: "FOLDER" },
{ name: "folder2", type: "FOLDER" },
]),
);
helper.load([configNode, datalinkListNode], flow, createCredentials(), function () {
const datalinkNode = helper.getNode("datalink1");
const helperNode = helper.getNode("helper1");
helperNode.on("input", function (msg) {
try {
// Should only return folders
expect(msg.payload.files).to.have.length(2);
msg.payload.files.forEach((f) => {
expect(f.type).to.equal("FOLDER");
});
done();
} catch (err) {
done(err);
}
});
datalinkNode.receive({ payload: {} });
});
});
});
describe("message passthrough", function () {
it("should preserve custom message properties", function (done) {
const flow = [
createConfigNode(),
{
id: "datalink1",
type: "seqera-datalink-list",
name: "Test Datalink List",
seqera: "config-node-1",
dataLinkName: "my-datalink",
dataLinkNameType: "str",
returnType: "all",
wires: [["helper1"]],
},
{ id: "helper1", type: "helper" },
];
nock(DEFAULT_BASE_URL)
.get("/data-links/")
.query(true)
.reply(
200,
createDataLinksResponse([{ id: "dl-123", name: "my-datalink", resourceRef: "s3://bucket", credentials: [] }]),
);
nock(DEFAULT_BASE_URL)
.get(/\/data-links\/dl-123\/browse/)
.query(true)
.reply(200, createDataBrowserResponse([]));
helper.load([configNode, datalinkListNode], flow, createCredentials(), function () {
const datalinkNode = helper.getNode("datalink1");
const helperNode = helper.getNode("helper1");
helperNode.on("input", function (msg) {
try {
expect(msg._context).to.equal("test-context");
expect(msg.correlationId).to.equal("corr-789");
done();
} catch (err) {
done(err);
}
});
datalinkNode.receive({
payload: {},
_context: "test-context",
correlationId: "corr-789",
});
});
});
});
describe("error handling", function () {
it("should report error when dataLinkName is not provided", function (done) {
const flow = [
createConfigNode(),
{
id: "datalink1",
type: "seqera-datalink-list",
name: "Test Datalink List",
seqera: "config-node-1",
dataLinkName: "",
dataLinkNameType: "str",
wires: [[]],
},
];
helper.load([configNode, datalinkListNode], flow, createCredentials(), function () {
const datalinkNode = helper.getNode("datalink1");
datalinkNode.on("call:error", function (call) {
try {
expect(call.firstArg).to.include("dataLinkName not provided");
done();
} catch (err) {
done(err);
}
});
datalinkNode.receive({ payload: {} });
});
});
it("should report error when datalink not found", function (done) {
const flow = [
createConfigNode(),
{
id: "datalink1",
type: "seqera-datalink-list",
name: "Test Datalink List",
seqera: "config-node-1",
dataLinkName: "nonexistent-datalink",
dataLinkNameType: "str",
wires: [[]],
},
];
nock(DEFAULT_BASE_URL).get("/data-links/").query(true).reply(200, { dataLinks: [] });
helper.load([configNode, datalinkListNode], flow, createCredentials(), function () {
const datalinkNode = helper.getNode("datalink1");
datalinkNode.on("call:error", function (call) {
try {
expect(call.firstArg).to.include("Could not find Data Link");
done();
} catch (err) {
done(err);
}
});
datalinkNode.receive({ payload: {} });
});
});
it("should report error when multiple datalinks match", function (done) {
const flow = [
createConfigNode(),
{
id: "datalink1",
type: "seqera-datalink-list",
name: "Test Datalink List",
seqera: "config-node-1",
dataLinkName: "ambiguous",
dataLinkNameType: "str",
wires: [[]],
},
];
nock(DEFAULT_BASE_URL)
.get("/data-links/")
.query(true)
.reply(
200,
createDataLinksResponse([
{ id: "dl-1", name: "ambiguous-1", resourceRef: "s3://a" },
{ id: "dl-2", name: "ambiguous-2", resourceRef: "s3://b" },
]),
);
helper.load([configNode, datalinkListNode], flow, createCredentials(), function () {
const datalinkNode = helper.getNode("datalink1");
datalinkNode.on("call:error", function (call) {
try {
expect(call.firstArg).to.include("more than one Data Link");
done();
} catch (err) {
done(err);
}
});
datalinkNode.receive({ payload: {} });
});
});
it("should handle API error during browse", function (done) {
const flow = [
createConfigNode(),
{
id: "datalink1",
type: "seqera-datalink-list",
name: "Test Datalink List",
seqera: "config-node-1",
dataLinkName: "my-datalink",
dataLinkNameType: "str",
wires: [[]],
},
];
nock(DEFAULT_BASE_URL)
.get("/data-links/")
.query(true)
.reply(
200,
createDataLinksResponse([{ id: "dl-123", name: "my-datalink", resourceRef: "s3://bucket", credentials: [] }]),
);
nock(DEFAULT_BASE_URL)
.get(/\/data-links\/dl-123\/browse/)
.query(true)
.reply(500, { error: "Server error" });
helper.load([configNode, datalinkListNode], flow, createCredentials(), function () {
const datalinkNode = helper.getNode("datalink1");
datalinkNode.on("call:error", function (call) {
try {
expect(call.firstArg).to.include("Seqera datalink list failed");
done();
} catch (err) {
done(err);
}
});
datalinkNode.receive({ payload: {} });
});
});
});
});