This commit is contained in:
gongwenxin
2025-08-28 16:20:24 +08:00
parent 61e533cddf
commit 73e887ebf4
1539 changed files with 360926 additions and 0 deletions
@@ -0,0 +1,618 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var should = require("should");
var sinon = require("sinon");
const stoppable = require('stoppable');
var http = require('http');
var express = require('express');
var app = express();
var WebSocket = require('ws');
var NR_TEST_UTILS = require("nr-test-utils");
var comms = NR_TEST_UTILS.require("@node-red/editor-api/lib/editor/comms");
var Users = NR_TEST_UTILS.require("@node-red/editor-api/lib/auth/users");
var Tokens = NR_TEST_UTILS.require("@node-red/editor-api/lib/auth/tokens");
var Strategies = NR_TEST_UTILS.require("@node-red/editor-api/lib/auth/strategies");
var address = '127.0.0.1';
var listenPort = 0; // use ephemeral port
describe("api/editor/comms", function() {
var connections = [];
var mockComms = {
addConnection: function(opts) {
connections.push(opts.client);
return Promise.resolve()
},
removeConnection: function(opts) {
for (var i=0;i<connections.length;i++) {
if (connections[i] === opts.client) {
connections.splice(i,1);
break;
}
}
return Promise.resolve()
},
subscribe: function() { return Promise.resolve()},
unsubscribe: function() { return Promise.resolve(); }
}
describe("with default keepalive", function() {
var server;
var url;
var port;
before(function(done) {
sinon.stub(Users,"default").callsFake(function() { return Promise.resolve(null);});
server = stoppable(http.createServer(function(req,res){app(req,res)}));
comms.init(server, {}, {comms: mockComms});
server.listen(listenPort, address);
server.on('listening', function() {
port = server.address().port;
url = 'http://' + address + ':' + port + '/comms';
comms.start();
done();
});
});
after(function(done) {
Users.default.restore();
comms.stop();
server.stop(done);
});
it('accepts connection', function(done) {
var ws = new WebSocket(url);
connections.length.should.eql(0);
ws.on('open', function() {
try {
connections.length.should.eql(1);
ws.close();
done();
} catch(err) {
done(err);
}
});
});
it('publishes message after subscription', function(done) {
var ws = new WebSocket(url);
ws.on('open', function() {
ws.send('{"subscribe":"topic1"}');
connections.length.should.eql(1);
connections[0].send('topic1', 'foo');
});
ws.on('message', function(msg) {
msg.should.equal('[{"topic":"topic1","data":"foo"}]');
ws.close();
done();
});
});
it('malformed messages are ignored',function(done) {
var ws = new WebSocket(url);
ws.on('open', function() {
ws.send('not json');
ws.send('[]');
ws.send('{"subscribe":"topic3"}');
connections[0].send('topic3', 'correct');
});
ws.on('message', function(msg) {
msg.should.equal('[{"topic":"topic3","data":"correct"}]');
ws.close();
done();
});
});
});
describe("disabled editor", function() {
var server;
var url;
var port;
before(function(done) {
sinon.stub(Users,"default").callsFake(function() { return Promise.resolve(null);});
server = stoppable(http.createServer(function(req,res){app(req,res)}));
comms.init(server, {disableEditor:true}, {comms: mockComms});
server.listen(listenPort, address);
server.on('listening', function() {
port = server.address().port;
url = 'http://' + address + ':' + port + '/comms';
comms.start();
done();
});
});
after(function(done) {
Users.default.restore();
comms.stop();
server.stop(done);
});
it('rejects websocket connections',function(done) {
connections.length.should.eql(0);
var ws = new WebSocket(url);
ws.on('open', function() {
done(new Error("Socket connection unexpectedly accepted"));
ws.close();
});
ws.on('error', function() {
connections.length.should.eql(0);
done();
});
});
});
describe("non-default httpAdminRoot set: /adminPath", function() {
var server;
var url;
var port;
before(function(done) {
sinon.stub(Users,"default").callsFake(function() { return Promise.resolve(null);});
server = stoppable(http.createServer(function(req,res){app(req,res)}));
comms.init(server, {httpAdminRoot:"/adminPath"}, {comms: mockComms});
server.listen(listenPort, address);
server.on('listening', function() {
port = server.address().port;
url = 'http://' + address + ':' + port + '/adminPath/comms';
comms.start();
done();
});
});
after(function(done) {
Users.default.restore();
comms.stop();
server.stop(done);
});
it('accepts connections',function(done) {
connections.length.should.eql(0);
var ws = new WebSocket(url);
ws.on('open', function() {
connections.length.should.eql(1);
ws.close();
done();
});
ws.on('error', function() {
done(new Error("Socket connection failed"));
});
});
});
describe("non-default httpAdminRoot set: /adminPath/", function() {
var server;
var url;
var port;
before(function(done) {
sinon.stub(Users,"default").callsFake(function() { return Promise.resolve(null);});
server = stoppable(http.createServer(function(req,res){app(req,res)}));
comms.init(server, {httpAdminRoot:"/adminPath/"}, {comms: mockComms});
server.listen(listenPort, address);
server.on('listening', function() {
port = server.address().port;
url = 'http://' + address + ':' + port + '/adminPath/comms';
comms.start();
done();
});
});
after(function(done) {
Users.default.restore();
comms.stop();
server.stop(done);
});
it('accepts connections',function(done) {
connections.length.should.eql(0);
var ws = new WebSocket(url);
ws.on('open', function() {
connections.length.should.eql(1);
ws.close();
done();
});
ws.on('error', function() {
done(new Error("Socket connection failed"));
});
});
});
describe("non-default httpAdminRoot set: adminPath", function() {
var server;
var url;
var port;
before(function(done) {
sinon.stub(Users,"default").callsFake(function() { return Promise.resolve(null);});
server = stoppable(http.createServer(function(req,res){app(req,res)}));
comms.init(server, {httpAdminRoot:"adminPath"}, {comms: mockComms});
server.listen(listenPort, address);
server.on('listening', function() {
port = server.address().port;
url = 'http://' + address + ':' + port + '/adminPath/comms';
comms.start();
done();
});
});
after(function(done) {
Users.default.restore();
comms.stop();
server.stop(done);
});
it('accepts connections',function(done) {
connections.length.should.eql(0);
var ws = new WebSocket(url);
ws.on('open', function() {
connections.length.should.eql(1);
ws.close();
done();
});
ws.on('error', function() {
done(new Error("Socket connection failed"));
});
});
});
describe("keep alives", function() {
var server;
var url;
var port;
before(function(done) {
sinon.stub(Users,"default").callsFake(function() { return Promise.resolve(null);});
server = stoppable(http.createServer(function(req,res){app(req,res)}));
comms.init(server, {webSocketKeepAliveTime: 100}, {comms: mockComms});
server.listen(listenPort, address);
server.on('listening', function() {
port = server.address().port;
url = 'http://' + address + ':' + port + '/comms';
comms.start();
done();
});
});
after(function(done) {
Users.default.restore();
comms.stop();
server.stop(done);
});
it('are sent', function(done) {
var ws = new WebSocket(url);
var count = 0;
ws.on('message', function(data) {
var msg = JSON.parse(data)[0];
msg.should.have.property('topic','hb');
msg.should.have.property('data').be.a.Number();
count++;
if (count == 3) {
ws.close();
done();
}
});
});
it('are not sent if other messages are sent', function(done) {
var ws = new WebSocket(url);
var count = 0;
var interval;
ws.on('open', function() {
ws.send('{"subscribe":"foo"}');
interval = setInterval(function() {
connections[0].send('foo', 'bar');
}, 50);
});
ws.on('message', function(data) {
var msg = JSON.parse(data)[0];
// It is possible a heartbeat message may arrive - so ignore them
if (msg.topic != "hb") {
msg.should.have.property('topic', 'foo');
msg.should.have.property('data', 'bar');
count++;
if (count == 5) {
clearInterval(interval);
ws.close();
done();
}
}
});
});
});
describe('authentication required, no anonymous',function() {
var server;
var url;
var port;
var getDefaultUser;
var getUser;
var getToken;
var getUserToken;
var getUserTokenHeader;
var authenticateUserToken;
var onSessionExpiry;
var onSessionExpiryCallback;
before(function(done) {
getDefaultUser = sinon.stub(Users,"default").callsFake(function() { return Promise.resolve(null);});
getUser = sinon.stub(Users,"get").callsFake(function(username) {
if (username == "fred") {
return Promise.resolve({permissions:"read"});
} else {
return Promise.resolve(null);
}
});
getUserToken = sinon.stub(Users,"tokens").callsFake(function(token) {
if (token == "abcde") {
return Promise.resolve({user:"wilma", permissions:"*"})
} else {
return Promise.resolve(null);
}
});
getToken = sinon.stub(Tokens,"get").callsFake(function(token) {
if (token == "1234") {
return Promise.resolve({user:"fred",scope:["*"]});
} else if (token == "5678") {
return Promise.resolve({user:"barney",scope:["*"]});
} else {
return Promise.resolve(null);
}
});
getUserTokenHeader = sinon.stub(Users,"tokenHeader").callsFake(function() {
return "custom-header"
})
authenticateUserToken = sinon.stub(Strategies, "authenticateUserToken").callsFake(async function(req) {
var token = req.headers['custom-header'];
if (token === "knock-knock") {
return {user:"fred",scope:["*"]}
}
throw new Error("Invalid user");
})
onSessionExpiry = sinon.stub(Tokens,"onSessionExpiry").callsFake(function(cb) {
onSessionExpiryCallback = cb;
});
server = stoppable(http.createServer(function(req,res){app(req,res)}));
comms.init(server, {adminAuth:{}}, {comms: mockComms});
server.listen(listenPort, address);
server.on('listening', function() {
port = server.address().port;
url = 'http://' + address + ':' + port + '/comms';
comms.start();
done();
});
});
after(function(done) {
getDefaultUser.restore();
getUser.restore();
getToken.restore();
getUserToken.restore();
getUserTokenHeader.restore();
authenticateUserToken.restore();
onSessionExpiry.restore();
comms.stop();
server.stop(done);
});
it('prevents connections that do not authenticate',function(done) {
var ws = new WebSocket(url);
var count = 0;
var interval;
ws.on('open', function() {
ws.send('{"subscribe":"foo"}');
});
ws.on('close', function() {
done();
});
});
it('allows connections that do authenticate',function(done) {
var ws = new WebSocket(url);
var received = 0;
ws.on('open', function() {
ws.send('{"auth":"1234"}');
});
ws.on('message', function(msg) {
received++;
if (received == 1) {
msg.should.equal('{"auth":"ok"}');
ws.send('{"subscribe":"foo"}');
connections[0].send('foo', 'correct');
} else {
msg.should.equal('[{"topic":"foo","data":"correct"}]');
ws.close();
}
});
ws.on('close', function() {
try {
received.should.equal(2);
done();
} catch(err) {
done(err);
}
});
});
it('allows connections that do authenticate - header-provided-token',function(done) {
var ws = new WebSocket(url,{
headers: { "custom-header": "knock-knock" }
});
var received = 0;
ws.on('open', function() {
ws.send('{"subscribe":"foo"}');
connections.should.have.length(1);
connections[0].send('foo', 'correct');
});
ws.on('message', function(msg) {
received++;
if (received == 1) {
msg.should.equal('[{"topic":"foo","data":"correct"}]');
ws.close();
}
});
ws.on('close', function() {
try {
received.should.equal(1);
done();
} catch(err) {
done(err);
}
});
});
it('allows connections that do authenticate - user-provided-token',function(done) {
var ws = new WebSocket(url);
var received = 0;
ws.on('open', function() {
ws.send('{"auth":"abcde"}');
});
ws.on('message', function(msg) {
received++;
if (received == 1) {
msg.should.equal('{"auth":"ok"}');
ws.send('{"subscribe":"foo"}');
connections[0].send('foo', 'correct');
} else {
msg.should.equal('[{"topic":"foo","data":"correct"}]');
ws.close();
}
});
ws.on('close', function() {
try {
received.should.equal(2);
done();
} catch(err) {
done(err);
}
});
});
it('rejects connections for non-existant token',function(done) {
var ws = new WebSocket(url);
var received = 0;
ws.on('open', function() {
ws.send('{"auth":"2345"}');
});
ws.on('close', function() {
done();
});
});
it('rejects connections for invalid token',function(done) {
var ws = new WebSocket(url);
var received = 0;
ws.on('open', function() {
ws.send('{"auth":"5678"}');
});
ws.on('close', function() {
done();
});
});
it('rejects connections for invalid token - header-provided-token',function(done) {
var ws = new WebSocket(url,{
headers: { "custom-header": "bad token" }
});
var received = 0;
ws.on('open', function() {
ws.send('{"subscribe":"foo"}');
});
ws.on('error', function() {
done();
})
});
it("expires websocket sessions", function(done) {
var ws = new WebSocket(url);
var received = 0;
ws.on('open', function() {
ws.send('{"auth":"1234"}');
});
ws.on('message', function(msg) {
received++;
if (received == 3) {
msg.should.equal('{"auth":"fail"}');
} else if (received == 1) {
msg.should.equal('{"auth":"ok"}');
ws.send('{"subscribe":"foo"}');
connections[0].send('foo', 'correct');
} else {
msg.should.equal('[{"topic":"foo","data":"correct"}]');
setTimeout(function() {
onSessionExpiryCallback({accessToken:"1234"})
},50);
}
});
ws.on('close', function() {
try {
received.should.equal(3);
done();
} catch(err) {
done(err);
}
});
})
});
describe('authentication required, anonymous enabled',function() {
var server;
var url;
var port;
var getDefaultUser;
before(function(done) {
getDefaultUser = sinon.stub(Users,"default").callsFake(function() { return Promise.resolve({permissions:"read"});});
server = stoppable(http.createServer(function(req,res){app(req,res)}));
comms.init(server, {adminAuth:{}}, {comms: mockComms});
server.listen(listenPort, address);
server.on('listening', function() {
port = server.address().port;
url = 'http://' + address + ':' + port + '/comms';
comms.start();
done();
});
});
after(function(done) {
getDefaultUser.restore();
comms.stop();
server.stop(done);
});
it('allows anonymous connections that do not authenticate',function(done) {
var ws = new WebSocket(url);
var count = 0;
var interval;
ws.on('open', function() {
ws.send('{"subscribe":"foo"}');
setTimeout(function() {
connections[0].send('foo', 'correct');
},200);
});
ws.on('message', function(msg) {
msg.should.equal('[{"topic":"foo","data":"correct"}]');
count++;
ws.close();
});
ws.on('close', function() {
try {
count.should.equal(1);
done();
} catch(err) {
done(err);
}
});
});
});
});
@@ -0,0 +1,94 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var should = require("should");
var request = require('supertest');
var express = require('express');
var sinon = require('sinon');
var NR_TEST_UTILS = require("nr-test-utils");
var credentials = NR_TEST_UTILS.require("@node-red/editor-api/lib/editor/credentials");
describe('api/editor/credentials', function() {
var app;
before(function() {
app = express();
app.get('/credentials/:type/:id',credentials.get);
credentials.init({
flows: {
getNodeCredentials: function(opts) {
if (opts.type === "known-type" && opts.id === "n1") {
return Promise.resolve({
user1:"abc",
has_password1: true
});
} else {
var err = new Error("message");
err.code = "test_code";
var p = Promise.reject(err);
p.catch(()=>{});
return p;
}
}
}
});
});
it('returns stored credentials',function(done) {
request(app)
.get("/credentials/known-type/n1")
.expect("Content-Type",/json/)
.expect(200)
.end(function(err,res) {
if (err) {
done(err);
} else {
try {
res.body.should.have.a.property("user1","abc");
res.body.should.not.have.a.property("password1");
res.body.should.have.a.property("has_password1",true);
done();
} catch(e) {
done(e);
}
}
})
});
it('returns any error',function(done) {
request(app)
.get("/credentials/unknown-type/n2")
.expect("Content-Type",/json/)
.expect(400)
.end(function(err,res) {
if (err) {
done(err);
} else {
try {
res.body.should.have.property('code');
res.body.code.should.be.equal("test_code");
res.body.should.have.property('message');
res.body.message.should.be.equal('message');
done();
} catch(e) {
done(e);
}
}
})
});
});
@@ -0,0 +1,134 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var should = require("should");
var sinon = require("sinon");
var request = require("supertest");
var express = require("express");
var NR_TEST_UTILS = require("nr-test-utils");
var editorApi = NR_TEST_UTILS.require("@node-red/editor-api/lib/editor");
var comms = NR_TEST_UTILS.require("@node-red/editor-api/lib/editor/comms");
var info = NR_TEST_UTILS.require("@node-red/editor-api/lib/editor/settings");
var auth = NR_TEST_UTILS.require("@node-red/editor-api/lib/auth");
var log = NR_TEST_UTILS.require("@node-red/util").log;
describe("api/editor/index", function() {
var app;
describe("disabled the editor", function() {
beforeEach(function() {
sinon.stub(comms,'init').callsFake(function(){});
sinon.stub(info,'init').callsFake(function(){});
});
afterEach(function() {
comms.init.restore();
info.init.restore();
});
it("disables the editor", function() {
var editorApp = editorApi.init({},{disableEditor:true},{});
should.not.exist(editorApp);
comms.init.called.should.be.false();
info.init.called.should.be.false();
});
});
describe("enables the editor", function() {
var mockList = [
'library','theme','locales','credentials','comms',"settings"
]
var isStarted = true;
var errors = [];
var session_data = {};
before(function() {
sinon.stub(auth,'needsPermission').callsFake(function(permission) {
return function(req,res,next) { next(); }
});
mockList.forEach(function(m) {
sinon.stub(NR_TEST_UTILS.require("@node-red/editor-api/lib/editor/"+m),"init").callsFake(function(){});
});
sinon.stub(NR_TEST_UTILS.require("@node-red/editor-api/lib/editor/theme"),"app").callsFake(function(){ return express()});
sinon.stub(NR_TEST_UTILS.require("@node-red/editor-api/lib/editor/settings"),"sshkeys").callsFake(function(){ return express()});
});
after(function() {
mockList.forEach(function(m) {
NR_TEST_UTILS.require("@node-red/editor-api/lib/editor/"+m).init.restore();
})
NR_TEST_UTILS.require("@node-red/editor-api/lib/editor/theme").app.restore();
NR_TEST_UTILS.require("@node-red/editor-api/lib/editor/settings").sshkeys.restore();
auth.needsPermission.restore();
log.error.restore();
});
before(function() {
sinon.stub(log,"error").callsFake(function(err) { errors.push(err)})
app = editorApi.init({},{httpNodeRoot:true, httpAdminRoot: true,disableEditor:false,exportNodeSettings:function(){}},{
isStarted: () => Promise.resolve(isStarted)
});
});
it('serves the editor', function(done) {
request(app)
.get("/")
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
// Index page should probably mention Node-RED somewhere
res.text.indexOf("Node-RED").should.not.eql(-1);
done();
});
});
it('serves icons', function(done) {
request(app)
.get("/red/images/icons/arrow-in.svg")
.expect(200)
.expect("Content-Type", /image\/svg\+xml/)
.end(function(err,res) {
done(err);
});
});
it('handles page not there', function(done) {
request(app)
.get("/foo")
.expect(404,done)
});
it('warns if runtime not started', function(done) {
isStarted = false;
request(app)
.get("/")
.expect(503)
.end(function(err,res) {
if (err) {
return done(err);
}
res.text.should.eql("Not started");
errors.should.have.lengthOf(1);
errors[0].should.eql("Node-RED runtime not started");
done();
});
});
// it.skip('GET /settings', function(done) {
// request(app).get("/settings").expect(200).end(function(err,res) {
// if (err) {
// return done(err);
// }
// // permissionChecks.should.have.property('settings.read',1);
// done();
// })
// });
});
});
@@ -0,0 +1,263 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var should = require("should");
var sinon = require("sinon");
var request = require('supertest');
var express = require('express');
var bodyParser = require('body-parser');
var NR_TEST_UTILS = require("nr-test-utils");
var library = NR_TEST_UTILS.require("@node-red/editor-api/lib/editor/library");
var app;
describe("api/editor/library", function() {
before(function() {
app = express();
app.use(bodyParser.json());
app.get(/library\/([^\/]+)\/([^\/]+)(?:$|\/(.*))/,library.getEntry);
app.post(/library\/([^\/]+)\/([^\/]+)\/(.*)/,library.saveEntry);
});
after(function() {
});
it('returns an individual entry - flow type', function(done) {
var opts;
library.init({
library: {
getEntry: function(_opts) {
opts = _opts;
return Promise.resolve('{"a":1,"b":2}');
}
}
});
request(app)
.get('/library/local/flows/abc')
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property('a',1);
res.body.should.have.property('b',2);
opts.should.have.property('library','local');
opts.should.have.property('type','flows');
opts.should.have.property('path','abc');
done();
});
})
it('returns a directory listing - flow type', function(done) {
var opts;
library.init({
library: {
getEntry: function(_opts) {
opts = _opts;
return Promise.resolve({"a":1,"b":2});
}
}
});
request(app)
.get('/library/local/flows/abc/def')
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property('a',1);
res.body.should.have.property('b',2);
opts.should.have.property('library','local');
opts.should.have.property('type','flows');
opts.should.have.property('path','abc/def');
done();
});
})
it('returns an individual entry - non-flow type', function(done) {
var opts;
library.init({
library: {
getEntry: function(_opts) {
opts = _opts;
return Promise.resolve('{"a":1,"b":2}');
}
}
});
request(app)
.get('/library/local/non-flow/abc')
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
opts.should.have.property('library','local');
opts.should.have.property('type','non-flow');
opts.should.have.property('path','abc');
res.text.should.eql('{"a":1,"b":2}');
done();
});
})
it('returns a directory listing - non-flow type', function(done) {
var opts;
library.init({
library: {
getEntry: function(_opts) {
opts = _opts;
return Promise.resolve({"a":1,"b":2});
}
}
});
request(app)
.get('/library/local/non-flow/abc/def')
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property('a',1);
res.body.should.have.property('b',2);
opts.should.have.property('library','local');
opts.should.have.property('type','non-flow');
opts.should.have.property('path','abc/def');
done();
});
})
it('returns an error on individual get', function(done) {
var opts;
library.init({
library: {
getEntry: function(_opts) {
opts = _opts;
var err = new Error("message");
err.code = "random_error";
err.status = 400;
var p = Promise.reject(err);
p.catch(()=>{});
return p;
}
}
});
request(app)
.get('/library/local/flows/123')
.expect(400)
.end(function(err,res) {
if (err) {
return done(err);
}
opts.should.have.property('library','local');
opts.should.have.property('type','flows');
opts.should.have.property('path','123');
res.body.should.have.property('code');
res.body.code.should.be.equal("random_error");
res.body.should.have.property('message');
res.body.message.should.be.equal("message");
done();
});
});
it('saves an individual entry - flow type', function(done) {
var opts;
library.init({
library: {
saveEntry: function(_opts) {
opts = _opts;
return Promise.resolve();
}
}
});
request(app)
.post('/library/local/flows/abc/def')
.expect(204)
.send({a:1,b:2,c:3})
.end(function(err,res) {
if (err) {
return done(err);
}
opts.should.have.property('library','local');
opts.should.have.property('type','flows');
opts.should.have.property('path','abc/def');
opts.should.have.property('meta',{});
opts.should.have.property('body',JSON.stringify({a:1,b:2,c:3}));
done();
});
})
it('saves an individual entry - non-flow type', function(done) {
var opts;
library.init({
library: {
saveEntry: function(_opts) {
opts = _opts;
return Promise.resolve();
}
}
});
request(app)
.post('/library/local/non-flow/abc/def')
.expect(204)
.send({a:1,b:2,text:"123"})
.end(function(err,res) {
if (err) {
return done(err);
}
opts.should.have.property('library','local');
opts.should.have.property('type','non-flow');
opts.should.have.property('path','abc/def');
opts.should.have.property('meta',{a:1,b:2});
opts.should.have.property('body',"123");
done();
});
})
it('returns an error on individual save', function(done) {
var opts;
library.init({
library: {
saveEntry: function(_opts) {
opts = _opts;
var err = new Error("message");
err.code = "random_error";
err.status = 400;
var p = Promise.reject(err);
p.catch(()=>{});
return p;
}
}
});
request(app)
.post('/library/local/non-flow/abc/def')
.send({a:1,b:2,text:"123"})
.expect(400)
.end(function(err,res) {
if (err) {
return done(err);
}
opts.should.have.property('type','non-flow');
opts.should.have.property('library','local');
opts.should.have.property('path','abc/def');
res.body.should.have.property('code');
res.body.code.should.be.equal("random_error");
res.body.should.have.property('message');
res.body.message.should.be.equal("message");
done();
});
});
});
@@ -0,0 +1,165 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var should = require("should");
var request = require('supertest');
var express = require('express');
var sinon = require('sinon');
var NR_TEST_UTILS = require("nr-test-utils");
var locales = NR_TEST_UTILS.require("@node-red/editor-api/lib/editor/locales");
var i18n = NR_TEST_UTILS.require("@node-red/util").i18n;
describe("api/editor/locales", function() {
beforeEach(function() {
})
afterEach(function() {
})
describe('get named resource catalog',function() {
var app;
before(function() {
// locales.init({
// i18n: {
// i: {
// language: function() { return 'en-US'},
// changeLanguage: function(lang,callback) {
// if (callback) {
// callback();
// }
// },
// getResourceBundle: function(lang, namespace) {
// return {namespace:namespace, lang:lang};
// }
// },
// }
// });
locales.init({});
// bit of a mess of internal workings
sinon.stub(i18n.i,'changeLanguage').callsFake(function(lang,callback) { if (callback) {callback();}});
if (i18n.i.getResourceBundle) {
sinon.stub(i18n.i,'getResourceBundle').callsFake(function(lang, namespace) {return {namespace:namespace, lang:lang};});
} else {
// If i18n.init has not been called, then getResourceBundle isn't
// defined - so hardcode a stub
i18n.i.getResourceBundle = function(lang, namespace) {return {namespace:namespace, lang:lang};};
i18n.i.getResourceBundle.restore = function() { delete i18n.i.getResourceBundle };
}
app = express();
app.get(/locales\/(.+)\/?$/,locales.get);
});
after(function() {
i18n.i.changeLanguage.restore();
i18n.i.getResourceBundle.restore();
})
it('returns with default language', function(done) {
request(app)
.get("/locales/message-catalog")
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property('namespace','message-catalog');
done();
});
});
it('returns with selected language', function(done) {
request(app)
.get("/locales/message-catalog?lng=fr-FR")
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property('namespace','message-catalog');
res.body.should.have.property('lang','fr-FR');
done();
});
});
it('returns for locale defined only with primary tag ', function(done) {
var orig = i18n.i.getResourceBundle;
i18n.i.getResourceBundle = function (lang, ns) {
if (lang === "ja-JP") {
return undefined;
}
return orig(lang, ns);
};
request(app)
// returns `ja` instead of `ja-JP`
.get("/locales/message-catalog?lng=ja-JP")
.expect(200)
.end(function(err,res) {
i18n.i.getResourceBundle = orig;
if (err) {
return done(err);
}
res.body.should.have.property('namespace','message-catalog');
res.body.should.have.property('lang','ja');
done();
});
});
});
// describe('get all node resource catalogs',function() {
// var app;
// before(function() {
// // bit of a mess of internal workings
// sinon.stub(i18n,'catalog').callsFake(function(namespace, lang) {
// return {
// "node-red": "should not return",
// "test-module-a-id": "test-module-a-catalog",
// "test-module-b-id": "test-module-b-catalog",
// "test-module-c-id": "test-module-c-catalog"
// }[namespace]
// });
// locales.init({
// nodes: {
// getNodeList: function(opts) {
// return Promise.resolve([
// {module:"node-red",id:"node-red-id"},
// {module:"test-module-a",id:"test-module-a-id"},
// {module:"test-module-b",id:"test-module-b-id"}
// ]);
// }
// }
// });
// app = express();
// app.get("/locales/nodes",locales.getAllNodes);
// });
// after(function() {
// i18n.catalog.restore();
// })
// it('returns with the node catalogs', function(done) {
// request(app)
// .get("/locales/nodes")
// .expect(200)
// .end(function(err,res) {
// if (err) {
// return done(err);
// }
// res.body.should.eql({
// 'test-module-a-id': 'test-module-a-catalog',
// 'test-module-b-id': 'test-module-b-catalog'
// });
// done();
// });
// });
// });
});
@@ -0,0 +1,21 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var NR_TEST_UTILS = require("nr-test-utils");
describe("api/editor/projects", function() {
it.skip("NEEDS TESTS WRITING",function() {});
});
@@ -0,0 +1,97 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var should = require("should");
var request = require('supertest');
var express = require('express');
var bodyParser = require("body-parser");
var sinon = require('sinon');
var app;
var NR_TEST_UTILS = require("nr-test-utils");
var info = NR_TEST_UTILS.require("@node-red/editor-api/lib/editor/settings");
var theme = NR_TEST_UTILS.require("@node-red/editor-api/lib/editor/theme");
describe("api/editor/settings", function() {
before(function() {
sinon.stub(theme,"settings").callsFake(function() { return { existing: 123, test: 456 };});
app = express();
app.use(bodyParser.json());
app.get("/settings/user",function(req,res,next) {req.user = "fred"; next()}, info.userSettings);
app.post("/settings/user",function(req,res,next) {req.user = "fred"; next()},info.updateUserSettings);
});
after(function() {
theme.settings.restore();
});
it('returns the user settings', function(done) {
info.init({}, {
settings: {
getUserSettings: function(opts) {
if (opts.user !== "fred") {
return Promise.reject(new Error("Unknown user"));
}
return Promise.resolve({
c:3,
d:4
})
}
}
});
request(app)
.get("/settings/user")
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.eql({c:3,d:4});
done();
});
});
it('updates the user settings', function(done) {
var update;
info.init({}, {
settings: {
updateUserSettings: function(opts) {
if (opts.user !== "fred") {
return Promise.reject(new Error("Unknown user"));
}
update = opts.settings;
return Promise.resolve()
}
}
});
request(app)
.post("/settings/user")
.send({
e:4,
f:5
})
.expect(204)
.end(function(err,res) {
if (err) {
return done(err);
}
update.should.eql({e:4,f:5});
done();
});
});
});
@@ -0,0 +1,333 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var should = require("should");
var sinon = require("sinon");
var request = require("supertest");
var express = require("express");
var NR_TEST_UTILS = require("nr-test-utils");
var sshkeys = NR_TEST_UTILS.require("@node-red/editor-api/lib/editor/sshkeys");
var bodyParser = require("body-parser");
describe("api/editor/sshkeys", function() {
var app;
var mockRuntime = {
settings: {
getUserKeys: function() {},
getUserKey: function() {},
generateUserKey: function() {},
removeUserKey: function() {}
}
}
before(function() {
sshkeys.init({}, mockRuntime);
app = express();
app.use(bodyParser.json());
app.use("/settings/user/keys", sshkeys.app());
});
beforeEach(function() {
sinon.stub(mockRuntime.settings, "getUserKeys");
sinon.stub(mockRuntime.settings, "getUserKey");
sinon.stub(mockRuntime.settings, "generateUserKey");
sinon.stub(mockRuntime.settings, "removeUserKey");
})
afterEach(function() {
mockRuntime.settings.getUserKeys.restore();
mockRuntime.settings.getUserKey.restore();
mockRuntime.settings.generateUserKey.restore();
mockRuntime.settings.removeUserKey.restore();
})
it('GET /settings/user/keys --- return empty list', function(done) {
mockRuntime.settings.getUserKeys.returns(Promise.resolve([]));
request(app)
.get("/settings/user/keys")
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property('keys');
res.body.keys.should.be.empty();
done();
});
});
it('GET /settings/user/keys --- return normal list', function(done) {
var fileList = [
'test_key01',
'test_key02'
];
var retList = fileList.map(function(elem) {
return {
name: elem
};
});
mockRuntime.settings.getUserKeys.returns(Promise.resolve(retList));
request(app)
.get("/settings/user/keys")
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property('keys');
for (var item of retList) {
res.body.keys.should.containEql(item);
}
done();
});
});
it('GET /settings/user/keys --- return Error', function(done) {
var errInstance = new Error("Messages here.....");
errInstance.code = "test_code";
var p = Promise.reject(errInstance);
p.catch(()=>{});
mockRuntime.settings.getUserKeys.returns(p);
request(app)
.get("/settings/user/keys")
.expect(400)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property('code');
res.body.code.should.be.equal(errInstance.code);
res.body.should.have.property('message');
res.body.message.should.be.equal(errInstance.message);
done();
});
});
it('GET /settings/user/keys/<key_file_name> --- return 404', function(done) {
var errInstance = new Error("Not Found.");
errInstance.code = "not_found";
errInstance.status = 404;
var p = Promise.reject(errInstance);
p.catch(()=>{});
mockRuntime.settings.getUserKey.returns(p);
request(app)
.get("/settings/user/keys/NOT_REAL")
.expect(404)
.end(function(err,res) {
if (err) {
return done(err);
}
done();
});
});
it('GET /settings/user/keys --- return Unexpected Error', function(done) {
var errInstance = new Error();
var p = Promise.reject(errInstance);
p.catch(()=>{});
mockRuntime.settings.getUserKeys.returns(p)
request(app)
.get("/settings/user/keys")
.expect(400)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property('code');
res.body.code.should.be.equal("unexpected_error");
res.body.should.have.property('message');
res.body.message.should.be.equal(errInstance.toString());
done();
});
});
it('GET /settings/user/keys/<key_file_name> --- return content', function(done) {
var key_file_name = "test_key";
var fileContent = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQD3a+sgtgzSbbliWxmOq5p6+H/mE+0gjWfLWrkIVmHENd1mifV4uCmIHAR2NfuadUYMQ3+bQ90kpmmEKTMYPsyentsKpHQZxTzG7wOCAIpJnbPTHDMxEJhVTaAwEjbVyMSIzTTPfnhoavWIBu0+uMgKDDlBm+RjlgkFlyhXyCN6UwFrIUUMH6Gw+eQHLiooKIl8ce7uDxIlt+9b7hFCU+sQ3kvuse239DZluu6+8buMWqJvrEHgzS9adRFKku8nSPAEPYn85vDi7OgVAcLQufknNgs47KHBAx9h04LeSrFJ/P5J1b//ItRpMOIme+O9d1BR46puzhvUaCHLdvO9czj+OmW+dIm+QIk6lZIOOMnppG72kZxtLfeKT16ur+2FbwAdL9ItBp4BI/YTlBPoa5mLMxpuWfmX1qHntvtGc9wEwS1P7YFfmF3XiK5apxalzrn0Qlr5UmDNbVIqJb1OlbC0w03Z0oktti1xT+R2DGOLWM4lBbpXDHV1BhQ7oYOvbUD8Cnof55lTP0WHHsOHlQc/BGDti1XA9aBX/OzVyzBUYEf0pkimsD0RYo6aqt7QwehJYdlz9x1NBguBffT0s4NhNb9IWr+ASnFPvNl2sw4XH/8U0J0q8ZkMpKkbLM1Zdp1Fv00GF0f5UNRokai6uM3w/ccantJ3WvZ6GtctqytWrw== \n";
mockRuntime.settings.getUserKey.returns(Promise.resolve(fileContent));
request(app)
.get("/settings/user/keys/" + key_file_name)
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
mockRuntime.settings.getUserKey.called.should.be.true();
mockRuntime.settings.getUserKey.firstCall.args[0].should.eql({ user: undefined, id: 'test_key' });
res.body.should.be.deepEqual({ publickey: fileContent });
done();
});
});
it('GET /settings/user/keys/<key_file_name> --- return Error', function(done) {
var key_file_name = "test_key";
var errInstance = new Error("Messages.....");
errInstance.code = "test_code";
var p = Promise.reject(errInstance);
p.catch(()=>{});
mockRuntime.settings.getUserKey.returns(p);
request(app)
.get("/settings/user/keys/" + key_file_name)
.expect(400)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property('code');
res.body.code.should.be.equal(errInstance.code);
res.body.should.have.property('message');
res.body.message.should.be.equal(errInstance.message);
done();
});
});
it('GET /settings/user/keys/<key_file_name> --- return Unexpected Error', function(done) {
var key_file_name = "test_key";
var errInstance = new Error("Messages.....");
var p = Promise.reject(errInstance);
p.catch(()=>{});
mockRuntime.settings.getUserKey.returns(p);
request(app)
.get("/settings/user/keys/" + key_file_name)
.expect(400)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property('code');
res.body.code.should.be.equal("unexpected_error");
res.body.should.have.property('message');
res.body.message.should.be.equal("Messages.....");
done();
});
});
it('POST /settings/user/keys --- success', function(done) {
var key_file_name = "test_key";
mockRuntime.settings.generateUserKey.returns(Promise.resolve(key_file_name));
request(app)
.post("/settings/user/keys")
.send({ name: key_file_name })
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
done();
});
});
it('POST /settings/user/keys --- return Error', function(done) {
var key_file_name = "test_key";
var errInstance = new Error("Messages.....");
errInstance.code = "test_code";
var p = Promise.reject(errInstance);
p.catch(()=>{});
mockRuntime.settings.generateUserKey.returns(p);
request(app)
.post("/settings/user/keys")
.send({ name: key_file_name })
.expect(400)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property('code');
res.body.code.should.be.equal("test_code");
res.body.should.have.property('message');
res.body.message.should.be.equal(errInstance.message);
done();
});
});
it('POST /settings/user/keys --- return Unexpected error', function(done) {
var key_file_name = "test_key";
var errInstance = new Error("Messages.....");
var p = Promise.reject(errInstance);
p.catch(()=>{});
mockRuntime.settings.generateUserKey.returns(p);
request(app)
.post("/settings/user/keys")
.send({ name: key_file_name })
.expect(400)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property('code');
res.body.code.should.be.equal("unexpected_error");
res.body.should.have.property('message');
res.body.message.should.be.equal("Messages.....");
done();
});
});
it('DELETE /settings/user/keys/<key_file_name> --- success', function(done) {
var key_file_name = "test_key";
mockRuntime.settings.removeUserKey.returns(Promise.resolve(true));
request(app)
.delete("/settings/user/keys/" + key_file_name)
.expect(204)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.be.deepEqual({});
done();
});
});
it('DELETE /settings/user/keys/<key_file_name> --- return Error', function(done) {
var key_file_name = "test_key";
var errInstance = new Error("Messages.....");
errInstance.code = "test_code";
var p = Promise.reject(errInstance);
p.catch(()=>{});
mockRuntime.settings.removeUserKey.returns(p);
request(app)
.delete("/settings/user/keys/" + key_file_name)
.expect(400)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property('code');
res.body.code.should.be.equal("test_code");
res.body.should.have.property('message');
res.body.message.should.be.equal(errInstance.message);
done();
});
});
it('DELETE /settings/user/keys/<key_file_name> --- return Unexpected Error', function(done) {
var key_file_name = "test_key";
var errInstance = new Error("Messages.....");
var p = Promise.reject(errInstance);
p.catch(()=>{});
mockRuntime.settings.removeUserKey.returns(p);
request(app)
.delete("/settings/user/keys/" + key_file_name)
.expect(400)
.end(function(err,res) {
if (err) {
return done(err);
}
res.body.should.have.property('code');
res.body.code.should.be.equal("unexpected_error");
res.body.should.have.property('message');
res.body.message.should.be.equal('Messages.....');
done();
});
});
});
@@ -0,0 +1,275 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var should = require("should");
var request = require("supertest");
var express = require('express');
var sinon = require('sinon');
var fs = require("fs");
var app = express();
var NR_TEST_UTILS = require("nr-test-utils");
var theme = NR_TEST_UTILS.require("@node-red/editor-api/lib/editor/theme");
describe("api/editor/theme", function () {
beforeEach(function () {
sinon.stub(fs, "statSync").callsFake(function () { return true; });
});
afterEach(function () {
theme.init({settings: {}});
fs.statSync.restore();
});
it("applies the default theme", async function () {
var result = theme.init({});
should.not.exist(result);
var context = await theme.context();
context.should.have.a.property("page");
context.page.should.have.a.property("title", "Node-RED");
context.page.should.have.a.property("favicon", "favicon.ico");
context.page.should.have.a.property("tabicon");
context.page.tabicon.should.have.a.property("icon", "red/images/node-red-icon-black.svg");
context.page.tabicon.should.have.a.property("colour", "#8f0000");
context.should.have.a.property("header");
context.header.should.have.a.property("title", "Node-RED");
context.header.should.have.a.property("image", "red/images/node-red.svg");
context.should.have.a.property("asset");
context.asset.should.have.a.property("red", "red/red.min.js");
context.asset.should.have.a.property("main", "red/main.min.js");
context.asset.should.have.a.property("vendorMonaco", "vendor/monaco/monaco-bootstrap.js");
should.not.exist(theme.settings());
});
it("uses non-minified js files when in dev mode", async function () {
const previousEnv = process.env.NODE_ENV;
try {
process.env.NODE_ENV = 'development'
theme.init({});
var context = await theme.context();
context.asset.should.have.a.property("red", "red/red.js");
context.asset.should.have.a.property("main", "red/main.js");
} finally {
process.env.NODE_ENV = previousEnv;
}
});
it("Does not add monaco bootstrap when ace selected", async function () {
theme.init({
editorTheme: {
codeEditor: {
lib: 'ace'
}
}
});
var context = await theme.context();
context.asset.should.have.a.property("vendorMonaco", "");
});
it("picks up custom theme", async function () {
theme.init({
editorTheme: {
page: {
title: "Test Page Title",
favicon: "/absolute/path/to/theme/favicon",
tabicon: {
icon: "/absolute/path/to/theme/tabicon",
colour: "#8f008f"
},
css: [
"/absolute/path/to/custom/css/file.css"
],
scripts: "/absolute/path/to/script.js"
},
header: {
title: "Test Header Title",
url: "https://nodered.org",
image: "/absolute/path/to/header/image" // or null to remove image
},
deployButton: {
type: "simple",
label: "Save",
icon: "/absolute/path/to/deploy/button/image" // or null to remove image
},
menu: { // Hide unwanted menu items by id. see editor/js/main.js:loadEditor for complete list
"menu-item-import-library": false,
"menu-item-export-library": false,
"menu-item-keyboard-shortcuts": false,
"menu-item-help": {
label: "Alternative Help Link Text",
url: "http://example.com"
}
},
userMenu: false, // Hide the user-menu even if adminAuth is enabled
login: {
image: "/absolute/path/to/login/page/big/image" // a 256x256 image
},
palette: {
editable: true,
catalogues: ['https://catalogue.nodered.org/catalogue.json'],
theme: [{ category: ".*", type: ".*", color: "#f0f" }]
},
projects: {
enabled: false
}
}
});
theme.app();
var context = await theme.context();
context.should.have.a.property("page");
context.page.should.have.a.property("title", "Test Page Title");
context.page.should.have.a.property("favicon", "theme/favicon/favicon");
context.page.should.have.a.property("tabicon")
context.page.tabicon.should.have.a.property("icon", "theme/tabicon/tabicon");
context.page.tabicon.should.have.a.property("colour", "#8f008f")
context.should.have.a.property("header");
context.header.should.have.a.property("title", "Test Header Title");
context.header.should.have.a.property("url", "https://nodered.org");
context.header.should.have.a.property("image", "theme/header/image");
context.page.should.have.a.property("css");
context.page.css.should.have.lengthOf(1);
context.page.css[0].should.eql('theme/css/file.css');
context.page.should.have.a.property("scripts");
context.page.scripts.should.have.lengthOf(1);
context.page.scripts[0].should.eql('theme/scripts/script.js');
context.should.have.a.property("login");
context.login.should.have.a.property("image", "theme/login/image");
var settings = theme.settings();
settings.should.have.a.property("deployButton");
settings.deployButton.should.have.a.property("type", "simple");
settings.deployButton.should.have.a.property("label", "Save");
settings.deployButton.should.have.a.property("icon", "theme/deploy/image");
settings.should.have.a.property("userMenu");
settings.userMenu.should.be.eql(false);
settings.should.have.a.property("menu");
settings.menu.should.have.a.property("menu-item-import-library", false);
settings.menu.should.have.a.property("menu-item-export-library", false);
settings.menu.should.have.a.property("menu-item-keyboard-shortcuts", false);
settings.menu.should.have.a.property("menu-item-help", { label: "Alternative Help Link Text", url: "http://example.com" });
settings.should.have.a.property("palette");
settings.palette.should.have.a.property("editable", true);
settings.palette.should.have.a.property("catalogues", ['https://catalogue.nodered.org/catalogue.json']);
settings.palette.should.have.a.property("theme", [{ category: ".*", type: ".*", color: "#f0f" }]);
settings.should.have.a.property("projects");
settings.projects.should.have.a.property("enabled", false);
});
it("picks up backwards compatible tabicon setting", async function () {
theme.init({
editorTheme: {
page: {
tabicon: "/absolute/path/to/theme/tabicon",
}
}
});
theme.app();
var context = await theme.context();
context.should.have.a.property("page");
context.page.should.have.a.property("tabicon");
context.page.tabicon.should.have.a.property("icon", "theme/tabicon/tabicon");
// The colour property should remain as default in this case as the
// legacy format for defining tabicon doesn't allow specifying a colour
context.page.tabicon.should.have.a.property("colour", "#8f0000");
});
it("test explicit userMenu set to true in theme setting", function () {
theme.init({
editorTheme: {
userMenu: true,
}
});
theme.app();
var settings = theme.settings();
settings.should.have.a.property("userMenu");
settings.userMenu.should.be.eql(true);
});
it("includes list of plugin themes", function(done) {
theme.init({},{
plugins: { getPluginsByType: _ => [{id:"theme-plugin"}] }
});
const app = theme.app();
request(app)
.get("/")
.end(function(err,res) {
if (err) {
return done(err);
}
try {
const response = JSON.parse(res.text);
response.should.have.property("themes");
response.themes.should.eql(["theme-plugin"])
done();
} catch(err) {
done(err);
}
});
});
it("includes theme plugin settings", async function () {
theme.init({
editorTheme: {
theme: 'test-theme'
}
},{
plugins: { getPlugin: t => {
return ({'test-theme':{
path: '/abosolute/path/to/plugin',
css: [
"path/to/custom/css/file1.css",
"/invalid/path/to/file2.css",
"../another/invalid/path/file3.css"
],
scripts: [
"path/to/custom/js/file1.js",
"/invalid/path/to/file2.js",
"../another/invalid/path/file3.js"
]
}})[t.id];
} }
});
theme.app();
var context = await theme.context();
context.should.have.a.property("page");
context.page.should.have.a.property("css");
context.page.css.should.have.lengthOf(1);
context.page.css[0].should.eql('theme/css/file1.css');
context.page.should.have.a.property("scripts");
context.page.scripts.should.have.lengthOf(1);
context.page.scripts[0].should.eql('theme/scripts/file1.js');
});
});
@@ -0,0 +1,210 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var should = require("should");
var request = require("supertest");
var express = require("express");
var fs = require("fs");
var path = require("path");
var NR_TEST_UTILS = require("nr-test-utils");
var ui = NR_TEST_UTILS.require("@node-red/editor-api/lib/editor/ui");
describe("api/editor/ui", function() {
var app;
before(function() {
ui.init({}, {
nodes: {
getIcon: function(opts) {
return new Promise(function(resolve,reject) {
if (opts.icon === "icon.png") {
fs.readFile(NR_TEST_UTILS.resolve("@node-red/editor-client/src/images/icons/arrow-in.svg"), function(err,data) {
resolve(data);
})
} else {
resolve(null);
}
});
},
getModuleResource: async function(opts) {
if (opts.module !== "test-module" || opts.path !== "a/path/text.txt") {
return null;
} else {
return "Some text data";
}
}
}
});
});
describe("slash handler", function() {
before(function() {
app = express();
app.get("/foo",ui.ensureSlash,function(req,res) { res.sendStatus(200);});
});
it('redirects if the path does not end in a slash',function(done) {
request(app)
.get('/foo')
.expect(301,done);
});
it('redirects if the path, with query string, does not end in a slash',function(done) {
request(app)
.get('/foo?abc=def')
.expect(301)
.end(function(err,res) {
if (err) {
return done(err);
}
res.header['location'].should.equal("/foo/?abc=def");
done();
});
});
it('does not redirect if the path ends in a slash',function(done) {
request(app)
.get('/foo/')
.expect(200,done);
});
});
describe("icon handler", function() {
before(function() {
app = express();
app.get("/icons/:module/:icon",ui.icon);
});
function binaryParser(res, callback) {
res.setEncoding('binary');
res.data = '';
res.on('data', function (chunk) {
res.data += chunk;
});
res.on('end', function () {
callback(null, Buffer.from(res.data, 'binary'));
});
}
function compareBuffers(b1,b2) {
b1.length.should.equal(b2.length);
for (var i=0;i<b1.length;i++) {
b1[i].should.equal(b2[i]);
}
}
it('returns the requested icon', function(done) {
var defaultIcon = fs.readFileSync(NR_TEST_UTILS.resolve("@node-red/editor-client/src/images/icons/arrow-in.svg"));
request(app)
.get("/icons/module/icon.png")
.expect("Content-Type", /image\/png/)
.expect(200)
.parse(binaryParser)
.end(function(err,res) {
if (err){
return done(err);
}
Buffer.isBuffer(res.body).should.be.true();
compareBuffers(res.body,defaultIcon);
done();
});
});
it('returns the default icon for invalid paths', function(done) {
var defaultIcon = fs.readFileSync(NR_TEST_UTILS.resolve("@node-red/editor-client/src/images/icons/arrow-in.svg"));
request(app)
.get("/icons/module/unreal.png")
.expect("Content-Type", /image\/svg/)
.expect(200)
.parse(binaryParser)
.end(function(err,res) {
if (err){
return done(err);
}
Buffer.isBuffer(res.body).should.be.true();
compareBuffers(res.body,defaultIcon);
done();
});
});
});
describe("module resource handler", function() {
before(function() {
app = express();
app.get(/^\/resources\/((?:@[^\/]+\/)?[^\/]+)\/(.+)$/,ui.moduleResource);
});
it('returns the requested resource', function(done) {
request(app)
.get("/resources/test-module/a/path/text.txt")
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
res.text.should.eql('Some text data');
done();
});
});
it('404s invalid paths', function(done) {
request(app)
.get("/resources/test-module/../a/path/text.txt")
.expect(404)
.end(function(err,res) {
if (err) {
return done(err);
}
done();
});
});
});
describe("editor ui handler", function() {
before(function() {
app = express();
app.use("/",ui.editor);
});
it('serves the editor', function(done) {
request(app)
.get("/")
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
// Index page should probably mention Node-RED somewhere
res.text.indexOf("Node-RED").should.not.eql(-1);
done();
});
});
});
describe("editor ui resource handler", function() {
before(function() {
app = express();
app.use("/",ui.editorResources);
});
it('serves the editor resources', function(done) {
request(app)
.get("/favicon.ico")
.expect(200)
.end(function(err,res) {
if (err) {
return done(err);
}
done();
});
});
});
});