diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c1230348..916a47ebe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,54 @@ +#### 0.18.5: Maintenance Release + +Projects + + - Add clone project to welcome screen + - Handle cloning a project without package.json + - Keep remote branch state in sync between editor and runtime + +New Features + + - Add type checks to switch node options (#1714) + - add output property select to HTML parse node (#1701) + - Add Prevent Following Redirect to HTTP Request node (#615) (#1684) + - Add debug and trace functions to function node (#1654) + - Enable user defined icon for subflow + - Add MQTT disconnect message and rework broker node UI (#1719) + - Japanese message catalogue updates (#1723) + - Show node load errors in the Palette Manager view + +Editor Fixes + + - Highlight subflow node when log msg comes from inside Fixes #1698 + - Ensure node wires array is not longer than outputs value Fixes #1678 + - Allow importing an unknown config node to be undone Fixes #1681 + - Ensure keyboard shortcuts get saved in runtime settings Fixes #1696 + - Don't mark a subflow changed when actually modified nothing (#1665) + +Node Fixes + + - bind to correct port when doing udp broadcast/multicast (#1686) + - Provide full error stack in Function node log message (#1700) + - Fix http request doc type Fixes #1690 + - Make debug slightly larger to pass WCAG AA rating + - Make core nodes labels more consistent, to close #1673 + - Allow template node to be updated more than once Fixes #1671 + - Fix the problem that output labels of switch node sometimes disappear (#1664) + - Chinese translations for core nodes (#1607) + +Runtime Fixes + + - Handle and display for invalid flow credentials when project is disabled #1689 (#1694) + - node-red-pi: fix behavior with old bash version (#1713) + - Fix ENOENT error on first start when no user dir (#1711) + - Handle null error object in Flow.handleError Fixes #1721 + - update settings comments to describe how to setup for ipv6 (#1675) + - Remove credential props after diffing flow to prevent future false positives Fixes #1359 + - Log error if settings unavailable when saving user settings Fixes #1645 + - Keep backup of .config.json + - Add warning if using \_credentialSecret from .config.json + - Filter req.user in /settings to prevent potentially leaking info + #### 0.18.4: Maintenance Release Projects diff --git a/bin/node-red-pi b/bin/node-red-pi index 828e5cca3..0e9de3c49 100755 --- a/bin/node-red-pi +++ b/bin/node-red-pi @@ -31,7 +31,7 @@ done # Find the real location of this script CURRENT_PATH=`pwd` SCRIPT_PATH="${BASH_SOURCE[0]}"; -while([ -h "${SCRIPT_PATH}" ]); do +while [ -h "${SCRIPT_PATH}" ]; do cd "`dirname "${SCRIPT_PATH}"`" SCRIPT_PATH="$(readlink "`basename "${SCRIPT_PATH}"`")"; done diff --git a/editor/js/main.js b/editor/js/main.js index 06d408ca7..b01136386 100644 --- a/editor/js/main.js +++ b/editor/js/main.js @@ -13,9 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ - $(function() { - if ((window.location.hostname !== "localhost") && (window.location.hostname !== "127.0.0.1")) { - document.title = document.title+" : "+window.location.hostname; - } - RED.init(); - }); + +$(function() { + if ((window.location.hostname !== "localhost") && (window.location.hostname !== "127.0.0.1")) { + document.title = document.title+" : "+window.location.hostname; + } + RED.init(); +}); diff --git a/editor/js/red.js b/editor/js/red.js index 930cd2bdb..74ebb1187 100644 --- a/editor/js/red.js +++ b/editor/js/red.js @@ -214,6 +214,18 @@ var RED = (function() { } ] } + } else if (msg.error === "missing_package_file") { + if (RED.user.hasPermission("projects.write")) { + options.buttons = [ + { + text: "Create default package file", + click: function() { + persistentNotifications[notificationId].hideNotification(); + RED.projects.createDefaultPackageFile(); + } + } + ] + } } else if (msg.error === "project_empty") { if (RED.user.hasPermission("projects.write")) { options.buttons = [ diff --git a/editor/js/ui/palette-editor.js b/editor/js/ui/palette-editor.js index ad24ba826..0793566b2 100644 --- a/editor/js/ui/palette-editor.js +++ b/editor/js/ui/palette-editor.js @@ -208,6 +208,8 @@ RED.palette.editor = (function() { if (nodeEntry) { var activeTypeCount = 0; var typeCount = 0; + var errorCount = 0; + nodeEntry.errorList.empty(); nodeEntries[module].totalUseCount = 0; nodeEntries[module].setUseCount = {}; @@ -216,7 +218,10 @@ RED.palette.editor = (function() { var inUseCount = 0; var set = moduleInfo.sets[setName]; var setElements = nodeEntry.sets[setName]; - + if (set.err) { + errorCount++; + $("
').text("To get started you can create your first project or clone an existing project from a git repository.").appendTo(body); $('
').text("If you are not sure, you can skip this for now. You will still be able to create your first project from the 'Projects' menu at any time.").appendTo(body); + var row = $('
').appendTo(body); + var createAsEmpty = $('').appendTo(row); + var createAsClone = $('').appendTo(row); + + createAsEmpty.click(function(e) { + e.preventDefault(); + createProjectOptions = { + action: "create" + } + show('git-config'); + }) + + createAsClone.click(function(e) { + e.preventDefault(); + createProjectOptions = { + action: "clone" + } + show('git-config'); + }) + return container; }, buttons: [ @@ -90,13 +110,6 @@ RED.projects = (function() { createProjectOptions = {}; $( this ).dialog( "close" ); } - }, - { - text: "Create your first project", // TODO: nls - class: "primary", - click: function() { - show('git-config'); - } } ] }, @@ -170,7 +183,11 @@ RED.projects = (function() { currentGitSettings.user.name = gitUsernameInput.val(); currentGitSettings.user.email = gitEmailInput.val(); RED.settings.set('git', currentGitSettings); - show('project-details'); + if (createProjectOptions.action === "create") { + show('project-details'); + } else if (createProjectOptions.action === "clone") { + show('clone-project'); + } } } ] @@ -303,6 +320,366 @@ RED.projects = (function() { } }; })(), + 'clone-project': (function() { + var projectNameInput; + var projectSummaryInput; + var projectFlowFileInput; + var projectSecretInput; + var projectSecretSelect; + var copyProject; + var projectRepoInput; + var projectCloneSecret; + var emptyProjectCredentialInput; + var projectRepoUserInput; + var projectRepoPasswordInput; + var projectNameSublabel; + var projectRepoSSHKeySelect; + var projectRepoPassphrase; + var projectRepoRemoteName + var projectRepoBranch; + var selectedProject; + + return { + content: function(options) { + var container = $(''); + migrateProjectHeader.appendTo(container); + var body = $('').appendTo(container); + $('').text("Clone a project").appendTo(body); + $('
').text("If you already have a git repository containing a project, you can clone it to get started.").appendTo(body); + + var projectList = null; + var pendingFormValidation = false; + $.getJSON("projects", function(data) { + projectList = {}; + data.projects.forEach(function(p) { + projectList[p] = true; + if (pendingFormValidation) { + pendingFormValidation = false; + validateForm(); + } + }) + }); + + + var validateForm = function() { + var projectName = projectNameInput.val(); + var valid = true; + if (projectNameInputChanged) { + if (projectList === null) { + pendingFormValidation = true; + return; + } + projectNameStatus.empty(); + if (!/^[a-zA-Z0-9\-_]+$/.test(projectName) || projectList[projectName]) { + projectNameInput.addClass("input-error"); + $('').appendTo(projectNameStatus); + projectNameValid = false; + valid = false; + if (projectList[projectName]) { + projectNameSublabel.text("Project already exists"); + } else { + projectNameSublabel.text("Must contain only A-Z 0-9 _ -"); + } + } else { + projectNameInput.removeClass("input-error"); + $('').appendTo(projectNameStatus); + projectNameSublabel.text("Must contain only A-Z 0-9 _ -"); + projectNameValid = true; + } + projectNameLastChecked = projectName; + } + valid = projectNameValid; + + var repo = projectRepoInput.val(); + + // var validRepo = /^(?:file|git|ssh|https?|[\d\w\.\-_]+@[\w\.]+):(?:\/\/)?[\w\.@:\/~_-]+(?:\/?|\#[\d\w\.\-_]+?)$/.test(repo); + var validRepo = repo.length > 0 && !/\s/.test(repo); + if (/^https?:\/\/[^/]+@/i.test(repo)) { + $("#projects-dialog-screen-create-project-repo-label small").text("Do not include the username/password in the url"); + validRepo = false; + } + if (!validRepo) { + if (projectRepoChanged) { + projectRepoInput.addClass("input-error"); + } + valid = false; + } else { + projectRepoInput.removeClass("input-error"); + } + if (/^https?:\/\//.test(repo)) { + $(".projects-dialog-screen-create-row-creds").show(); + $(".projects-dialog-screen-create-row-sshkey").hide(); + } else if (/^(?:ssh|[\S]+?@[\S]+?):(?:\/\/)?/.test(repo)) { + $(".projects-dialog-screen-create-row-creds").hide(); + $(".projects-dialog-screen-create-row-sshkey").show(); + // if ( !getSelectedSSHKey(projectRepoSSHKeySelect) ) { + // valid = false; + // } + } else { + $(".projects-dialog-screen-create-row-creds").hide(); + $(".projects-dialog-screen-create-row-sshkey").hide(); + } + + $("#projects-dialog-clone-project").prop('disabled',!valid).toggleClass('disabled ui-button-disabled ui-state-disabled',!valid); + } + + var row; + + row = $('
').appendTo(body); + $('').appendTo(row); + + var subrow = $('').appendTo(row); + projectNameInput = $('').appendTo(subrow); + var projectNameStatus = $('').appendTo(subrow); + + var projectNameInputChanged = false; + var projectNameLastChecked = ""; + var projectNameValid; + var checkProjectName; + var autoInsertedName = ""; + + + projectNameInput.on("change keyup paste",function() { + projectNameInputChanged = (projectNameInput.val() !== projectNameLastChecked); + if (checkProjectName) { + clearTimeout(checkProjectName); + } else if (projectNameInputChanged) { + projectNameStatus.empty(); + $('').appendTo(projectNameStatus); + if (projectNameInput.val() === '') { + validateForm(); + return; + } + } + checkProjectName = setTimeout(function() { + validateForm(); + checkProjectName = null; + },300) + }); + projectNameSublabel = $('').appendTo(row).find("small"); + + row = $('').appendTo(body); + $('').appendTo(row); + projectRepoInput = $('').appendTo(row); + $('').appendTo(row); + var projectRepoChanged = false; + var lastProjectRepo = ""; + projectRepoInput.on("change keyup paste",function() { + projectRepoChanged = true; + var repo = $(this).val(); + if (lastProjectRepo !== repo) { + $("#projects-dialog-screen-create-project-repo-label small").text("https://, ssh:// or file://"); + } + lastProjectRepo = repo; + + var m = /\/([^/]+?)(?:\.git)?$/.exec(repo); + if (m) { + var projectName = projectNameInput.val(); + if (projectName === "" || projectName === autoInsertedName) { + autoInsertedName = m[1]; + projectNameInput.val(autoInsertedName); + projectNameInput.change(); + } + } + validateForm(); + }); + + var cloneAuthRows = $('').appendTo(body); + row = $('').hide().appendTo(cloneAuthRows); + $('This is a message that will be published on the configured topic whenever the connection is established.
+This is a message that will be published on the configured topic before the + connection is closed normally, either by re-deploying the node, or by shutting down.
This is a message that will be published by the broker in the event the node unexpectedly loses its connection.
@@ -300,14 +339,18 @@ compatmode: { value: true}, keepalive: {value:60,validate:RED.validators.number()}, cleansession: {value: true}, - willTopic: {value:""}, - willQos: {value:"0"}, - willRetain: {value:false}, - willPayload: {value:""}, birthTopic: {value:""}, birthQos: {value:"0"}, birthRetain: {value:false}, - birthPayload: {value:""} + birthPayload: {value:""}, + closeTopic: {value:""}, + closeQos: {value:"0"}, + closeRetain: {value:false}, + closePayload: {value:""}, + willTopic: {value:""}, + willQos: {value:"0"}, + willRetain: {value:false}, + willPayload: {value:""} }, credentials: { user: {type:"text"}, @@ -343,14 +386,39 @@ id: "mqtt-broker-tab-security", label: this._("mqtt.tabs-label.security") }); + tabs.addTab({ - id: "mqtt-broker-tab-birth", - label: this._("mqtt.tabs-label.birth") - }); - tabs.addTab({ - id: "mqtt-broker-tab-will", - label: this._("mqtt.tabs-label.will") + id: "mqtt-broker-tab-messages", + label: this._("mqtt.tabs-label.messages") }); + + function setUpSection(sectionId, isExpanded) { + var birthMessageSection = $(sectionId); + var paletteHeader = birthMessageSection.find('.palette-header'); + var twistie = paletteHeader.find('i'); + var sectionContent = birthMessageSection.find('.section-content'); + + function toggleSection(expanded) { + twistie.toggleClass('expanded', expanded); + sectionContent.toggle(expanded); + } + paletteHeader.click(function(e) { + e.preventDefault(); + var isExpanded = twistie.hasClass('expanded'); + toggleSection(!isExpanded); + }); + toggleSection(isExpanded); + } + + // show first section if none are set so the user gets the idea + var showBirthSection = this.birthTopic !== "" + || this.willTopic === "" + && this.birthTopic === "" + && this.closeTopic == ""; + setUpSection('#mqtt-broker-section-birth', showBirthSection); + setUpSection('#mqtt-broker-section-close', this.closeTopic !== ""); + setUpSection('#mqtt-broker-section-will', this.willTopic !== ""); + setTimeout(function() { tabs.resize(); },0); if (typeof this.cleansession === 'undefined') { this.cleansession = true; @@ -368,14 +436,18 @@ this.keepalive = 15; $("#node-config-input-keepalive").val(this.keepalive); } - if (typeof this.willQos === 'undefined') { - this.willQos = "0"; - $("#node-config-input-willQos").val("0"); - } if (typeof this.birthQos === 'undefined') { this.birthQos = "0"; $("#node-config-input-birthQos").val("0"); } + if (typeof this.closeQos === 'undefined') { + this.willQos = "0"; + $("#node-config-input-willQos").val("0"); + } + if (typeof this.willQos === 'undefined') { + this.willQos = "0"; + $("#node-config-input-willQos").val("0"); + } function updateTLSOptions() { if ($("#node-config-input-usetls").is(':checked')) { diff --git a/nodes/core/io/10-mqtt.js b/nodes/core/io/10-mqtt.js index c1a8318a6..509099aa4 100644 --- a/nodes/core/io/10-mqtt.js +++ b/nodes/core/io/10-mqtt.js @@ -60,6 +60,15 @@ module.exports = function(RED) { }; } + if (n.closeTopic) { + this.closeMessage = { + topic: n.closeTopic, + payload: n.closePayload || "", + qos: Number(n.closeQos||0), + retain: n.closeRetain=="true"|| n.closeRetain===true + }; + } + if (this.credentials) { this.username = this.credentials.user; this.password = this.credentials.password; @@ -314,6 +323,10 @@ module.exports = function(RED) { this.on('close', function(done) { this.closing = true; if (this.connected) { + // Send close message + if (node.closeMessage) { + node.publish(node.closeMessage); + } this.client.once('close', function() { done(); }); diff --git a/nodes/core/io/32-udp.html b/nodes/core/io/32-udp.html index 3711978c4..126f1da6f 100644 --- a/nodes/core/io/32-udp.html +++ b/nodes/core/io/32-udp.html @@ -29,7 +29,7 @@Flows stopped as the credentials could not be decrypted.
The flow credential file is encrypted, but the project's encryption key is missing or invalid.
", "credentials_load_failed_reset":"Credentials could not be decrypted
The flow credential file is encrypted, but the project's encryption key is missing or invalid.
The flow credential file will be reset on the next deployment. Any existing flow credentials will be cleared.
", "missing_flow_file": "Project flow file not found.
The project is not configured with a flow file.
", + "missing_package_file": "Project package file not found.
The project is missing a package.json file.
", "project_empty": "The project is empty.
Do you want to create a default set of project files?
Otherwise, you will have to manually add files to the project outside of the editor.
Project '__project__' not found.
", "git_merge_conflict": "Automatic merging of changes failed.
Fix the unmerged conflicts then commit the results.
" diff --git a/red/runtime-api/settings.js b/red/runtime-api/settings.js index 716fd7aaf..ac9092d5f 100644 --- a/red/runtime-api/settings.js +++ b/red/runtime-api/settings.js @@ -68,10 +68,17 @@ var api = module.exports = { try { var safeSettings = { httpNodeRoot: runtime.settings.httpNodeRoot||"/", - version: runtime.settings.version, - user: opts.user + version: runtime.settings.version + } + if (opts.user) { + safeSettings.user = {} + var props = ["anonymous","username","image","permissions"]; + props.forEach(prop => { + if (opts.user.hasOwnProperty(prop)) { + safeSettings.user[prop] = opts.user[prop]; + } + }) } - if (util.isArray(runtime.settings.paletteCategories)) { safeSettings.paletteCategories = runtime.settings.paletteCategories; } diff --git a/red/runtime/nodes/flows/Flow.js b/red/runtime/nodes/flows/Flow.js index 6f4e132df..f6a62cbfd 100644 --- a/red/runtime/nodes/flows/Flow.js +++ b/red/runtime/nodes/flows/Flow.js @@ -237,8 +237,8 @@ function Flow(global,flow) { this.handleError = function(node,logMessage,msg) { var count = 1; - if (msg && msg.hasOwnProperty("error")) { - if (msg.error.hasOwnProperty("source")) { + if (msg && msg.hasOwnProperty("error") && msg.error !== null) { + if (msg.error.hasOwnProperty("source") && msg.error.source !== null) { if (msg.error.source.id === node.id) { count = msg.error.source.count+1; if (count === 10) { diff --git a/red/runtime/storage/localfilesystem/projects/Project.js b/red/runtime/storage/localfilesystem/projects/Project.js index aa2693001..8ba85f768 100644 --- a/red/runtime/storage/localfilesystem/projects/Project.js +++ b/red/runtime/storage/localfilesystem/projects/Project.js @@ -53,15 +53,13 @@ function getGitUser(user) { } return null; } -function Project(name) { - this.name = name; - this.path = fspath.join(projectsDir,name); +function Project(path) { + this.path = path; + this.name = fspath.basename(path); this.paths = {}; this.files = {}; this.auth = {origin:{}}; - this.missingFiles = []; - this.credentialSecret = null; } Project.prototype.load = function () { @@ -70,7 +68,9 @@ Project.prototype.load = function () { // console.log(globalProjectSettings) var projectSettings = {}; if (globalProjectSettings) { - projectSettings = globalProjectSettings.projects[this.name]||{}; + if (globalProjectSettings.projects.hasOwnProperty(this.name)) { + projectSettings = globalProjectSettings.projects[this.name] || {}; + } } this.credentialSecret = projectSettings.credentialSecret; @@ -81,9 +81,7 @@ Project.prototype.load = function () { var promises = []; return checkProjectFiles(project).then(function(missingFiles) { - if (missingFiles.length > 0) { - project.missingFiles = missingFiles; - } + project.missingFiles = missingFiles; if (missingFiles.indexOf('package.json') === -1) { project.paths['package.json'] = fspath.join(project.path,"package.json"); promises.push(fs.readFile(project.paths['package.json'],"utf8").then(function(content) { @@ -135,9 +133,9 @@ Project.prototype.load = function () { Project.prototype.initialise = function(user,data) { var project = this; - if (!this.empty) { - throw new Error("Cannot initialise non-empty project"); - } + // if (!this.empty) { + // throw new Error("Cannot initialise non-empty project"); + // } var files = Object.keys(defaultFileSet); var promises = []; @@ -148,17 +146,25 @@ Project.prototype.initialise = function(user,data) { promises.push(settings.set('projects',projects)); } - project.files.flow = data.files.flow; - project.files.credentials = data.files.credentials; - var flowFilePath = fspath.join(project.path,project.files.flow); - var credsFilePath = getCredentialsFilename(flowFilePath); - promises.push(util.writeFile(flowFilePath,"[]")); - promises.push(util.writeFile(credsFilePath,"{}")); - files.push(project.files.flow); - files.push(project.files.credentials); + if (data.hasOwnProperty('files')) { + if (data.files.hasOwnProperty('flow') && data.files.hasOwnProperty('credentials')) { + project.files.flow = data.files.flow; + project.files.credentials = data.files.credentials; + var flowFilePath = fspath.join(project.path,project.files.flow); + var credsFilePath = getCredentialsFilename(flowFilePath); + promises.push(util.writeFile(flowFilePath,"[]")); + promises.push(util.writeFile(credsFilePath,"{}")); + files.push(project.files.flow); + files.push(project.files.credentials); + } + } for (var file in defaultFileSet) { if (defaultFileSet.hasOwnProperty(file)) { - promises.push(util.writeFile(fspath.join(project.path,file),defaultFileSet[file](project))); + var path = fspath.join(project.path,file); + if (!fs.existsSync(path)) { + promises.push(util.writeFile(path,defaultFileSet[file](project))); + } + } } @@ -740,7 +746,7 @@ Project.prototype.getCredentialsFileBackup = function() { return getBackupFilename(this.getCredentialsFile()); } -Project.prototype.toJSON = function () { +Project.prototype.export = function () { return { name: this.name, @@ -780,28 +786,18 @@ function getBackupFilename(filename) { return fspath.join(ffDir,"."+ffName+".backup"); } -function checkProjectExists(project) { - var projectPath = fspath.join(projectsDir,project); +function checkProjectExists(projectPath) { return fs.pathExists(projectPath).then(function(exists) { if (!exists) { - var e = new Error("Project not found: "+project); + var e = new Error("Project not found"); e.code = "project_not_found"; - e.project = project; + var name = fspath.basename(projectPath); + e.project = name; throw e; } }); } -function createProjectDirectory(project) { - var projectPath = fspath.join(projectsDir,project); - return fs.ensureDir(projectPath); -} - -function deleteProjectDirectory(project) { - var projectPath = fspath.join(projectsDir,project); - return fs.remove(projectPath); -} - function createDefaultProject(user, project) { var projectPath = fspath.join(projectsDir,project.name); // Create a basic skeleton of a project @@ -904,17 +900,23 @@ function createProject(user, metadata) { } else { username = user.username; } + if (!metadata.path) { + throw new Error("Project missing path property"); + } + if (!metadata.name) { + throw new Error("Project missing name property"); + } var project = metadata.name; + var projectPath = metadata.path; return new Promise(function(resolve,reject) { - var projectPath = fspath.join(projectsDir,project); fs.stat(projectPath, function(err,stat) { if (!err) { var e = new Error("NLS: Project already exists"); e.code = "project_exists"; return reject(e); } - createProjectDirectory(project).then(function() { + fs.ensureDir(projectPath).then(function() { var projects = settings.get('projects'); if (!projects) { projects = { @@ -951,7 +953,7 @@ function createProject(user, metadata) { return createDefaultProject(user, metadata); } }).then(function() { - resolve(getProject(project)) + resolve(loadProject(projectPath)) }).catch(function(err) { fs.remove(projectPath,function() { reject(err); @@ -961,50 +963,21 @@ function createProject(user, metadata) { }) } -function deleteProject(user, name) { - return checkProjectExists(name).then(function() { - if (currentProject && currentProject.name === name) { - var e = new Error("NLS: Can't delete the active project"); - e.code = "cannot_delete_active_project"; - throw e; - } - else { - return deleteProjectDirectory(name).then(function() { - var projects = settings.get('projects'); - delete projects.projects[name]; - return settings.set('projects', projects); - }); - } - }); -} - -var currentProject; - -function getProject(name) { - return checkProjectExists(name).then(function() { - if (currentProject && currentProject.name === name) { - return currentProject; - } - currentProject = new Project(name); - return currentProject.load(); - }); -} - -function listProjects() { - return fs.readdir(projectsDir).then(function(fns) { - var dirs = []; - fns.sort(function(A,B) { - return A.toLowerCase().localeCompare(B.toLowerCase()); - }).filter(function(fn) { - var fullPath = fspath.join(projectsDir,fn); - if (fn[0] != ".") { - var stats = fs.lstatSync(fullPath); - if (stats.isDirectory()) { - dirs.push(fn); - } - } +function deleteProject(user, projectPath) { + return checkProjectExists(projectPath).then(function() { + return fs.remove(projectPath).then(function() { + var name = fspath.basename(projectPath); + var projects = settings.get('projects'); + delete projects.projects[name]; + return settings.set('projects', projects); }); - return dirs; + }); +} + +function loadProject(projectPath) { + return checkProjectExists(projectPath).then(function() { + var project = new Project(projectPath); + return project.load(); }); } @@ -1018,9 +991,7 @@ function init(_settings, _runtime) { module.exports = { init: init, - get: getProject, + load: loadProject, create: createProject, - delete: deleteProject, - list: listProjects - + delete: deleteProject } diff --git a/red/runtime/storage/localfilesystem/projects/git/index.js b/red/runtime/storage/localfilesystem/projects/git/index.js index 717a1a235..196aece83 100644 --- a/red/runtime/storage/localfilesystem/projects/git/index.js +++ b/red/runtime/storage/localfilesystem/projects/git/index.js @@ -48,7 +48,9 @@ function runGitCommand(args,cwd,env) { var err = new Error(stderr); err.stdout = stdout; err.stderr = stderr; - if (/fatal: could not read/i.test(stderr)) { + if(/Connection refused/i.test(stderr)) { + err.code = "git_connection_failed"; + } else if (/fatal: could not read/i.test(stderr)) { // Username/Password err.code = "git_auth_failed"; } else if(/HTTP Basic: Access denied/i.test(stderr)) { @@ -58,8 +60,6 @@ function runGitCommand(args,cwd,env) { } else if(/Host key verification failed/i.test(stderr)) { // TODO: handle host key verification errors separately err.code = "git_auth_failed"; - } else if(/Connection refused/i.test(stderr)) { - err.code = "git_connection_failed"; } else if (/commit your changes or stash/i.test(stderr)) { err.code = "git_local_overwrite"; } else if (/CONFLICT/.test(err.stdout)) { diff --git a/red/runtime/storage/localfilesystem/projects/index.js b/red/runtime/storage/localfilesystem/projects/index.js index 7c24845ab..2ae44ac91 100644 --- a/red/runtime/storage/localfilesystem/projects/index.js +++ b/red/runtime/storage/localfilesystem/projects/index.js @@ -127,10 +127,20 @@ function init(_settings, _runtime) { activeProject = globalSettings.projects.activeProject; } if (settings.flowFile) { + // if flowFile is a known project name - use it if (globalSettings.projects.projects.hasOwnProperty(settings.flowFile)) { activeProject = settings.flowFile; globalSettings.projects.activeProject = settings.flowFile; saveSettings = true; + } else { + // if it resolves to a dir - use it... but: + // - where to get credsecret from? + // - what if the name clashes with a known project? + + // var stat = fs.statSync(settings.flowFile); + // if (stat && stat.isDirectory()) { + // activeProject = settings.flowFile; + // } } } if (!activeProject) { @@ -148,6 +158,24 @@ function init(_settings, _runtime) { return Promise.resolve(); } +function listProjects() { + return fs.readdir(projectsDir).then(function(fns) { + var dirs = []; + fns.sort(function(A,B) { + return A.toLowerCase().localeCompare(B.toLowerCase()); + }).filter(function(fn) { + var fullPath = fspath.join(projectsDir,fn); + if (fn[0] != ".") { + var stats = fs.lstatSync(fullPath); + if (stats.isDirectory()) { + dirs.push(fn); + } + } + }); + return dirs; + }); +} + function getUserGitSettings(user) { var userSettings = settings.getUserSettings(user)||{}; return userSettings.git; @@ -160,7 +188,11 @@ function getBackupFilename(filename) { } function loadProject(name) { - return Projects.get(name).then(function(project) { + var projectPath = name; + if (projectPath.indexOf(fspath.sep) === -1) { + projectPath = fspath.join(projectsDir,name); + } + return Projects.load(projectPath).then(function(project) { activeProject = project; flowsFullPath = project.getFlowFile(); flowsFileBackup = project.getFlowFileBackup(); @@ -170,26 +202,20 @@ function loadProject(name) { }) } -function listProjects(user) { - return Projects.list(); -} - function getProject(user, name) { checkActiveProject(name); //return when.resolve(activeProject.info); - var username; - if (!user) { - username = "_"; - } else { - username = user.username; - } - return Projects.get(name).then(function(project) { - return project.toJSON(); - }); + return Promise.resolve(activeProject.export()); } function deleteProject(user, name) { - return Projects.delete(user, name); + if (activeProject && activeProject.name === name) { + var e = new Error("NLS: Can't delete the active project"); + e.code = "cannot_delete_active_project"; + throw e; + } + var projectPath = fspath.join(projectsDir,name); + return Projects.delete(user, projectPath); } function checkActiveProject(project) { @@ -347,6 +373,7 @@ function createProject(user, metadata) { metadata.files.oldCredentials = credentialsFile; metadata.files.credentialSecret = currentEncryptionKey; } + metadata.path = fspath.join(projectsDir,metadata.name); return Projects.create(user, metadata).then(function(p) { return setActiveProject(user, p.name); }).then(function() { @@ -479,6 +506,12 @@ function getFlows() { error.code = "project_empty"; return when.reject(error); } + if (activeProject.missingFiles && activeProject.missingFiles.indexOf('package.json') !== -1) { + log.warn("Project missing package.json"); + error = new Error("Project missing package.json"); + error.code = "missing_package_file"; + return when.reject(error); + } if (!activeProject.getFlowFile()) { log.warn("Project has no flow file"); error = new Error("Project has no flow file"); diff --git a/red/runtime/storage/localfilesystem/util.js b/red/runtime/storage/localfilesystem/util.js index 7515f7bb1..8b353c81a 100644 --- a/red/runtime/storage/localfilesystem/util.js +++ b/red/runtime/storage/localfilesystem/util.js @@ -80,27 +80,26 @@ module.exports = { */ writeFile: function(path,content,backupPath) { if (backupPath) { - try { - fs.renameSync(path,backupPath); - } catch(err) { - } - } - return when.promise(function(resolve,reject) { - var stream = fs.createWriteStream(path); - stream.on('open',function(fd) { - stream.write(content,'utf8',function() { - fs.fsync(fd,function(err) { - if (err) { - log.warn(log._("storage.localfilesystem.fsync-fail",{path: path, message: err.toString()})); - } - stream.end(resolve); - }); - }); - }); - stream.on('error',function(err) { - reject(err); - }); - }); + if (fs.existsSync(path)) { + fs.renameSync(path,backupPath); + } + } + return when.promise(function(resolve,reject) { + var stream = fs.createWriteStream(path); + stream.on('open',function(fd) { + stream.write(content,'utf8',function() { + fs.fsync(fd,function(err) { + if (err) { + log.warn(log._("storage.localfilesystem.fsync-fail",{path: path, message: err.toString()})); + } + stream.end(resolve); + }); + }); + }); + stream.on('error',function(err) { + reject(err); + }); + }); }, readFile: readFile, diff --git a/settings.js b/settings.js index c3bd898ec..0438dc5ea 100644 --- a/settings.js +++ b/settings.js @@ -23,6 +23,7 @@ module.exports = { uiPort: process.env.PORT || 1880, // By default, the Node-RED UI accepts connections on all IPv4 interfaces. + // To listen on all IPv6 addresses, set uiHost to "::", // The following property can be used to listen on a specific interface. For // example, the following would only allow connections from the local machine. //uiHost: "127.0.0.1", diff --git a/test/nodes/core/analysis/72-sentiment_spec.js b/test/nodes/core/analysis/72-sentiment_spec.js index 32abd7c65..d2272f7b3 100644 --- a/test/nodes/core/analysis/72-sentiment_spec.js +++ b/test/nodes/core/analysis/72-sentiment_spec.js @@ -16,7 +16,7 @@ var should = require("should"); var sentimentNode = require("../../../../nodes/core/analysis/72-sentiment.js"); -var helper = require("../../helper.js"); +var helper = require("node-red-node-test-helper"); describe('sentiment Node', function() { diff --git a/test/nodes/core/core/20-inject_spec.js b/test/nodes/core/core/20-inject_spec.js index 572570ce6..f687626e4 100644 --- a/test/nodes/core/core/20-inject_spec.js +++ b/test/nodes/core/core/20-inject_spec.js @@ -16,7 +16,7 @@ var should = require("should"); var injectNode = require("../../../../nodes/core/core/20-inject.js"); -var helper = require("../../helper.js"); +var helper = require("node-red-node-test-helper"); describe('inject node', function() { diff --git a/test/nodes/core/core/25-catch_spec.js b/test/nodes/core/core/25-catch_spec.js index 51b326e2a..6caf72259 100644 --- a/test/nodes/core/core/25-catch_spec.js +++ b/test/nodes/core/core/25-catch_spec.js @@ -16,7 +16,7 @@ var should = require("should"); var catchNode = require("../../../../nodes/core/core/25-catch.js"); -var helper = require("../../helper.js"); +var helper = require("node-red-node-test-helper"); describe('catch Node', function() { diff --git a/test/nodes/core/core/25-status_spec.js b/test/nodes/core/core/25-status_spec.js index ef89fd9bb..0bf38edb2 100644 --- a/test/nodes/core/core/25-status_spec.js +++ b/test/nodes/core/core/25-status_spec.js @@ -16,7 +16,7 @@ var should = require("should"); var catchNode = require("../../../../nodes/core/core/25-status.js"); -var helper = require("../../helper.js"); +var helper = require("node-red-node-test-helper"); describe('status Node', function() { diff --git a/test/nodes/core/core/58-debug_spec.js b/test/nodes/core/core/58-debug_spec.js index 48fad03e9..7954bdaa3 100644 --- a/test/nodes/core/core/58-debug_spec.js +++ b/test/nodes/core/core/58-debug_spec.js @@ -16,7 +16,7 @@ var should = require("should"); var debugNode = require("../../../../nodes/core/core/58-debug.js"); -var helper = require("../../helper.js"); +var helper = require("node-red-node-test-helper"); var WebSocket = require('ws'); describe('debug node', function() { diff --git a/test/nodes/core/core/60-link_spec.js b/test/nodes/core/core/60-link_spec.js index fb844fdaf..ef275c4dc 100644 --- a/test/nodes/core/core/60-link_spec.js +++ b/test/nodes/core/core/60-link_spec.js @@ -16,7 +16,7 @@ var should = require("should"); var linkNode = require("../../../../nodes/core/core/60-link.js"); -var helper = require("../../helper.js"); +var helper = require("node-red-node-test-helper"); describe('link Node', function() { diff --git a/test/nodes/core/core/75-exec_spec.js b/test/nodes/core/core/75-exec_spec.js index 28f6eb00f..8a4ca332a 100644 --- a/test/nodes/core/core/75-exec_spec.js +++ b/test/nodes/core/core/75-exec_spec.js @@ -16,7 +16,7 @@ var should = require("should"); var sinon = require("sinon"); -var helper = require("../../helper.js"); +var helper = require("node-red-node-test-helper"); var execNode = require("../../../../nodes/core/core/75-exec.js"); var osType = require("os").type(); diff --git a/test/nodes/core/core/80-function_spec.js b/test/nodes/core/core/80-function_spec.js index 6d5457823..b96588b5b 100644 --- a/test/nodes/core/core/80-function_spec.js +++ b/test/nodes/core/core/80-function_spec.js @@ -16,7 +16,7 @@ var should = require("should"); var functionNode = require("../../../../nodes/core/core/80-function.js"); -var helper = require("../../helper.js"); +var helper = require("node-red-node-test-helper"); describe('function node', function() { @@ -242,7 +242,7 @@ describe('function node', function() { }); it('should handle and log script error', function(done) { - var flow = [{id:"n1",type:"function",wires:[["n2"]],func:"retunr"}]; + var flow = [{id:"n1",type:"function",wires:[["n2"]],func:"var a = 1;\nretunr"}]; helper.load(functionNode, flow, function() { var n1 = helper.getNode("n1"); n1.receive({payload:"foo",topic: "bar"}); @@ -256,7 +256,7 @@ describe('function node', function() { msg.should.have.property('level', helper.log().ERROR); msg.should.have.property('id', 'n1'); msg.should.have.property('type', 'function'); - msg.should.have.property('msg', 'ReferenceError: retunr is not defined (line 1, col 1)'); + msg.should.have.property('msg', 'ReferenceError: retunr is not defined (line 2, col 1)'); done(); } catch(err) { done(err); diff --git a/test/nodes/core/core/80-template_spec.js b/test/nodes/core/core/80-template_spec.js index b7e7cbbf8..9abab1f90 100644 --- a/test/nodes/core/core/80-template_spec.js +++ b/test/nodes/core/core/80-template_spec.js @@ -16,7 +16,7 @@ var should = require("should"); var templateNode = require("../../../../nodes/core/core/80-template.js"); -var helper = require("../../helper.js"); +var helper = require("node-red-node-test-helper"); describe('template node', function() { diff --git a/test/nodes/core/core/89-delay_spec.js b/test/nodes/core/core/89-delay_spec.js index 35ad19ede..1f6489484 100644 --- a/test/nodes/core/core/89-delay_spec.js +++ b/test/nodes/core/core/89-delay_spec.js @@ -17,7 +17,7 @@ var should = require("should"); var delayNode = require("../../../../nodes/core/core/89-delay.js"); -var helper = require("../../helper.js"); +var helper = require("node-red-node-test-helper"); var GRACE_PERCENTAGE=10; diff --git a/test/nodes/core/core/89-trigger_spec.js b/test/nodes/core/core/89-trigger_spec.js index 3d95234d5..fb1b1df9a 100644 --- a/test/nodes/core/core/89-trigger_spec.js +++ b/test/nodes/core/core/89-trigger_spec.js @@ -16,7 +16,7 @@ var should = require("should"); var sinon = require("sinon"); -var helper = require("../../helper.js"); +var helper = require("node-red-node-test-helper"); var triggerNode = require("../../../../nodes/core/core/89-trigger.js"); var RED = require("../../../../red/red.js"); @@ -419,7 +419,7 @@ describe('trigger node', function() { }); it('should be able to extend the delay (but with no 2nd output)', function(done) { - var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", extend:"true", op1type:"pay", op2type:"nul", op1:"false", op2:"true", duration:"50", wires:[["n2"]] }, + var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", extend:"true", op1type:"pay", op2type:"nul", op1:"false", op2:"true", duration:"100", wires:[["n2"]] }, {id:"n2", type:"helper"} ]; helper.load(triggerNode, flow, function() { var n1 = helper.getNode("n1"); @@ -434,7 +434,7 @@ describe('trigger node', function() { else { msg.should.have.a.property("payload", "World"); //console.log(Date.now() - ss); - (Date.now() - ss).should.be.greaterThan(70); + (Date.now() - ss).should.be.greaterThan(140); done(); } } @@ -447,7 +447,7 @@ describe('trigger node', function() { },20); setTimeout( function() { n1.emit("input", {payload:"World"}); - },80); + },150); }); }); diff --git a/test/nodes/core/core/90-comment_spec.js b/test/nodes/core/core/90-comment_spec.js index e9e582dd0..f5d2c5f3d 100644 --- a/test/nodes/core/core/90-comment_spec.js +++ b/test/nodes/core/core/90-comment_spec.js @@ -16,7 +16,7 @@ var should = require("should"); var commentNode = require("../../../../nodes/core/core/90-comment.js"); -var helper = require("../../helper.js"); +var helper = require("node-red-node-test-helper"); describe('comment Node', function() { diff --git a/test/nodes/core/core/98-unknown_spec.js b/test/nodes/core/core/98-unknown_spec.js index 7751c5a12..c544d1c08 100644 --- a/test/nodes/core/core/98-unknown_spec.js +++ b/test/nodes/core/core/98-unknown_spec.js @@ -16,7 +16,7 @@ var should = require("should"); var unknown = require("../../../../nodes/core/core/98-unknown.js"); -var helper = require("../../helper.js"); +var helper = require("node-red-node-test-helper"); describe('unknown Node', function() { diff --git a/test/nodes/core/io/21-httprequest_spec.js b/test/nodes/core/io/21-httprequest_spec.js index 28c166d39..43c42c8a4 100644 --- a/test/nodes/core/io/21-httprequest_spec.js +++ b/test/nodes/core/io/21-httprequest_spec.js @@ -16,23 +16,76 @@ var when = require("when"); var http = require("http"); +var https = require("https"); var should = require("should"); var express = require("express"); var bodyParser = require('body-parser'); var stoppable = require('stoppable'); -var helper = require("../../helper.js"); +var helper = require("node-red-node-test-helper"); var httpRequestNode = require("../../../../nodes/core/io/21-httprequest.js"); +var tlsNode = require("../../../../nodes/core/io/05-tls.js"); var hashSum = require("hash-sum"); +var httpProxy = require('http-proxy'); +var cookieParser = require('cookie-parser'); +var RED = require("../../../../red/red.js"); +var fs = require('fs-extra'); +var auth = require('basic-auth'); describe('HTTP Request Node', function() { var testApp; var testServer; var testPort = 9000; + var testSslServer; + var testSslPort = 9100; + var testProxyServer; + var testProxyPort = 9200; + + //save environment variables + var preEnvHttpProxyLowerCase; + var preEnvHttpProxyUpperCase; + var preEnvNoProxyLowerCase; + var preEnvNoProxyUpperCase; function startServer(done) { testPort += 1; testServer = stoppable(http.createServer(testApp)); testServer.listen(testPort,function(err) { + testSslPort += 1; + var sslOptions = { + key: fs.readFileSync('test/resources/ssl/server.key'), + cert: fs.readFileSync('test/resources/ssl/server.crt') + /* + Country Name (2 letter code) [AU]: + State or Province Name (full name) [Some-State]: + Locality Name (eg, city) []: + Organization Name (eg, company) [Internet Widgits Pty Ltd]: + Organizational Unit Name (eg, section) []: + Common Name (e.g. server FQDN or YOUR name) []:localhost + Email Address []: + + Please enter the following 'extra' attributes to be sent with your certificate request + A challenge password []: + An optional company name []: + */ + }; + testSslServer = stoppable(https.createServer(sslOptions,testApp)); + testSslServer.listen(testSslPort); + + testProxyPort += 1; + testProxyServer = stoppable(httpProxy.createProxyServer({target:'http://localhost:' + testPort})); + testProxyServer.on('proxyReq', function(proxyReq, req, res, options) { + proxyReq.setHeader('x-testproxy-header', 'foobar'); + }); + testProxyServer.on('proxyRes', function (proxyRes, req, res, options) { + if (req.url == getTestURL('/proxyAuthenticate')){ + var user = auth.parse(req.headers['proxy-authorization']); + if (!(user.name == "foouser" && user.pass == "barpassword")){ + proxyRes.headers['proxy-authenticate'] = 'BASIC realm="test"'; + proxyRes.statusCode = 407; + } + } + }); + testProxyServer.listen(testProxyPort); done(err); }); } @@ -41,20 +94,119 @@ describe('HTTP Request Node', function() { return "http://localhost:"+testPort+url; } + function getSslTestURL(url) { + return "https://localhost:"+testSslPort+url; + } + + function getSslTestURLWithoutProtocol(url) { + return "localhost:"+testSslPort+url; + } + + function saveProxySetting() { + preEnvHttpProxyLowerCase = process.env.http_proxy; + preEnvHttpProxyUpperCase = process.env.HTTP_PROXY; + preEnvNoProxyLowerCase = process.env.no_proxy; + preEnvNoProxyUpperCase = process.env.NO_PROXY; + delete process.env.http_proxy; + delete process.env.HTTP_PROXY; + delete process.env.no_proxy; + delete process.env.NO_PROXY; + } + + function restoreProxySetting() { + process.env.http_proxy = preEnvHttpProxyLowerCase; + process.env.HTTP_PROXY = preEnvHttpProxyUpperCase; + // On Windows, if environment variable of NO_PROXY that includes lower cases + // such as No_Proxy is replaced with NO_PROXY. + process.env.no_proxy = preEnvNoProxyLowerCase; + process.env.NO_PROXY = preEnvNoProxyUpperCase; + if (preEnvHttpProxyLowerCase == undefined){ + delete process.env.http_proxy; + } + if (preEnvHttpProxyUpperCase == undefined){ + delete process.env.HTTP_PROXY; + } + if (preEnvNoProxyLowerCase == undefined){ + delete process.env.no_proxy; + } + if (preEnvNoProxyUpperCase == undefined){ + delete process.env.NO_PROXY; + } + } + before(function(done) { testApp = express(); testApp.use(bodyParser.raw({type:"*/*"})); - testApp.get('/statusCode204', function(req,res) { res.status(204).end();}) + testApp.use(cookieParser()); + testApp.get('/statusCode204', function(req,res) { res.status(204).end();}); testApp.get('/text', function(req, res){ res.send('hello'); }); + testApp.get('/redirectToText', function(req, res){ res.status(302).set('Location', getTestURL('/text')).end(); }); testApp.get('/json-valid', function(req, res){ res.json({a:1}); }); testApp.get('/json-invalid', function(req, res){ res.set('Content-Type', 'application/json').send("{a:1"); }); + testApp.get('/headersInspect', function(req, res){ res.set('x-test-header', 'bar').send("a"); }); + testApp.get('/timeout', function(req, res){ + setTimeout(function() { + res.send('hello'); + }, 10000); + }); + testApp.get('/checkCookie', function(req, res){ + var value = req.cookies.data; + res.send(value); + }); + testApp.get('/setCookie', function(req, res){ + res.cookie('data','hello'); + res.send(""); + }); + testApp.get('/authenticate', function(req, res){ + var user = auth.parse(req.headers['authorization']); + var result = { + user: user.name, + pass: user.pass, + }; + res.json(result); + }); + testApp.get('/proxyAuthenticate', function(req, res){ + var user = auth.parse(req.headers['proxy-authorization']); + var result = { + user: user.name, + pass: user.pass, + headers: req.headers + }; + res.json(result); + }); testApp.post('/postInspect', function(req,res) { var result = { body: req.body.toString(), headers: req.headers - } + }; res.json(result); }); + testApp.put('/putInspect', function(req,res) { + var result = { + body: req.body.toString(), + headers: req.headers + }; + res.json(result); + }); + testApp.delete('/deleteInspect', function(req,res) { res.status(204).end();}); + testApp.head('/headInspect', function(req,res) { res.status(204).end();}); + testApp.patch('/patchInspect', function(req,res) { + var result = { + body: req.body.toString(), + headers: req.headers + }; + res.json(result); + }); + testApp.trace('/traceInspect', function(req,res) { + var result = { + body: req.body.toString(), + headers: req.headers + }; + res.json(result); + }); + testApp.options('/*', function(req,res) { + res.status(200).end(); + }); startServer(function(err) { if (err) { done(err); @@ -65,408 +217,1006 @@ describe('HTTP Request Node', function() { after(function(done) { testServer.stop(() => { - helper.stopServer(done); + testProxyServer.stop(() => { + testSslServer.stop(() => { + helper.stopServer(done); + }); + }); }); }); + afterEach(function() { helper.unload(); }); - it('get plain text content', function(done) { - var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"txt",url:getTestURL('/text')}, - {id:"n2", type:"helper"}]; - helper.load(httpRequestNode, flow, function() { - var n1 = helper.getNode("n1"); - var n2 = helper.getNode("n2"); - n2.on("input", function(msg) { - try { - msg.should.have.property('payload','hello'); - msg.should.have.property('statusCode',200); - msg.should.have.property('headers'); - msg.headers.should.have.property('content-length',''+('hello'.length)); - msg.headers.should.have.property('content-type').which.startWith('text/html'); - done(); - } catch(err) { - done(err); - } + describe('request', function() { + it('should get plain text content', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"txt",url:getTestURL('/text')}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property('payload','hello'); + msg.should.have.property('statusCode',200); + msg.should.have.property('headers'); + msg.headers.should.have.property('content-length',''+('hello'.length)); + msg.headers.should.have.property('content-type').which.startWith('text/html'); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"foo"}); + }); + }); + + it('should get JSON content', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"obj",url:getTestURL('/json-valid')}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property('payload',{a:1}); + msg.should.have.property('statusCode',200); + msg.should.have.property('headers'); + msg.headers.should.have.property('content-type').which.startWith('application/json'); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"foo"}); + }); + }); + + it('should send the payload as the body of a POST as application/json', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"POST",ret:"obj",url:getTestURL('/postInspect')}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property('payload'); + msg.payload.body.should.eql('{"foo":"abcde"}'); + msg.payload.headers.should.have.property('content-type').which.startWith('application/json'); + msg.should.have.property('statusCode',200); + msg.should.have.property('headers'); + msg.headers.should.have.property('content-type').which.startWith('application/json'); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:{foo:"abcde"}}); + }); + }); + + it('should send a payload of 0 as the body of a POST as text/plain', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"POST",ret:"obj",url:getTestURL('/postInspect')}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property('payload'); + msg.payload.body.should.eql('0'); + msg.payload.headers.should.have.property('content-length','1'); + msg.payload.headers.should.have.property('content-type').which.startWith('text/plain'); + msg.should.have.property('statusCode',200); + msg.should.have.property('headers'); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:0, headers: { 'content-type': 'text/plain'}}); + }); + }); + + it('should send an Object payload as the body of a POST', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"POST",ret:"obj",url:getTestURL('/postInspect')}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property('payload'); + msg.payload.body.should.eql('{"foo":"abcde"}'); + msg.payload.headers.should.have.property('content-type').which.startWith('text/plain'); + msg.should.have.property('statusCode',200); + msg.should.have.property('headers'); + msg.headers.should.have.property('content-type').which.startWith('application/json'); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:{foo:"abcde"}, headers: { 'content-type': 'text/plain'}}); + }); + }); + + it('should send a Buffer as the body of a POST', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"POST",ret:"obj",url:getTestURL('/postInspect')}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property('payload'); + msg.payload.body.should.eql('hello'); + msg.payload.headers.should.have.property('content-type').which.startWith('text/plain'); + msg.should.have.property('statusCode',200); + msg.should.have.property('headers'); + msg.headers.should.have.property('content-type').which.startWith('application/json'); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:new Buffer('hello'), headers: { 'content-type': 'text/plain'}}); + }); + }); + + it('should send form-based request', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"POST",ret:"obj",url:getTestURL('/postInspect')}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property('statusCode',200); + msg.payload.body.should.equal("foo=1%202%203&bar="); + msg.payload.should.have.property('headers'); + msg.payload.headers.should.have.property('content-type','application/x-www-form-urlencoded'); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:{foo:'1 2 3', bar:''}, headers: { 'content-type': 'application/x-www-form-urlencoded'}}); + }); + }); + + it('should send PUT request', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"PUT",ret:"obj",url:getTestURL('/putInspect')}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property('payload'); + msg.payload.body.should.eql('foo'); + msg.payload.headers.should.have.property('content-type').which.startWith('text/plain'); + msg.should.have.property('statusCode',200); + msg.should.have.property('headers'); + msg.headers.should.have.property('content-type').which.startWith('application/json'); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"foo", headers: { 'content-type': 'text/plain'}}); + }); + }); + + it('should send DELETE request', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"DELETE",ret:"obj",url:getTestURL('/deleteInspect')}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property('payload',''); + msg.should.have.property('statusCode',204); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:{foo:"abcde"}}); + }); + }); + + it('should send HEAD request', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"use",ret:"txt",url:getTestURL('/headInspect')}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property('payload',''); + msg.should.have.property('statusCode',204); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"foo", method:"head"}); + }); + }); + + it('should send PATCH request', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"PATCH",ret:"obj",url:getTestURL('/patchInspect')}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property('payload'); + msg.payload.body.should.eql('foo'); + msg.should.have.property('statusCode',200); + msg.should.have.property('headers'); + msg.headers.should.have.property('etag'); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"foo", headers: { 'content-type': 'text/plain'}}); + }); + }); + + it('should send OPTIONS request', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"use",ret:"obj",url:getTestURL('/*')}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property('statusCode',200); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"foo", method:"options"}); + }); + }); + + it('should send TRACE request', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"use",ret:"obj",url:getTestURL('/traceInspect')}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property('payload'); + msg.payload.body.should.eql('foo'); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"foo", method:"trace", headers: { 'content-type': 'text/plain'}}); + }); + }); + + it('should get Buffer content', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"bin",url:getTestURL('/text')}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property('payload'); + Buffer.isBuffer(msg.payload).should.be.true(); + msg.should.have.property('statusCode',200); + msg.should.have.property('headers'); + msg.headers.should.have.property('content-type'); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"foo"}); + }); + }); + + it('should return plain text when JSON fails to parse', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"obj",url:getTestURL('/json-invalid')}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property('payload',"{a:1"); + msg.should.have.property('statusCode',200); + msg.should.have.property('headers'); + msg.headers.should.have.property('content-type').which.startWith('application/json'); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"foo"}); + }); + }); + + it('should return the status code', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"txt",url:getTestURL('/statusCode204')}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property('payload',''); + msg.should.have.property('statusCode',204); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"foo"}); + }); + }); + + it('should use msg.url', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"obj",url:getTestURL('/text')}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property('payload','hello'); + msg.should.have.property('statusCode',200); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"foo", url:"/foo"}); + }); + }); + + it('should output an error when URL is not provided', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"obj",url:""}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + var inError = false; + n2.on("input", function(msg) { + inError = true; + }); + n1.receive({payload:"foo"}); + setTimeout(function() { + if (inError) { + done(new Error("no url allowed though")); + } else { + done(); + } + },20); + }); + }); + + it('should allow the message to provide the url', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"txt"}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property('payload','hello'); + msg.should.have.property('statusCode',200); + msg.should.have.property('headers'); + msg.headers.should.have.property('content-length',''+('hello'.length)); + msg.headers.should.have.property('content-type').which.startWith('text/html'); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"foo",url:getTestURL('/text')}); + }); + }); + + it('should allow the url to contain mustache placeholders', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"txt",url:getTestURL('/te{{placeholder}}')}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property('payload','hello'); + msg.should.have.property('statusCode',200); + msg.should.have.property('headers'); + msg.headers.should.have.property('content-length',''+('hello'.length)); + msg.headers.should.have.property('content-type').which.startWith('text/html'); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"foo",placeholder:"xt"}); + }); + }); + + it('should allow the url to be missing the http:// prefix', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"txt",url:getTestURL('/text').substring("http://".length)}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property('payload','hello'); + msg.should.have.property('statusCode',200); + msg.should.have.property('headers'); + msg.headers.should.have.property('content-length',''+('hello'.length)); + msg.headers.should.have.property('content-type').which.startWith('text/html'); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"foo"}); + }); + }); + + it('should reject non http:// schemes - node config', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"txt",url:"ftp://foo"}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + var inError = false; + n2.on("input", function(msg) { + inError = true; + }); + n1.receive({payload:"foo"}); + setTimeout(function() { + if (inError) { + done(new Error("non http(s):// scheme allowed through")); + } else { + done(); + } + },20); + }); + }); + + it('should reject non http:// schemes - msg.url', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"txt"}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + var inError = false; + n2.on("input", function(msg) { + inError = true; + }); + n1.receive({payload:"foo",url:"ftp://foo"}); + setTimeout(function() { + if (inError) { + done(new Error("non http(s):// scheme allowed through")); + } else { + done(); + } + },20); + }); + }); + + it('should use msg.method', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"obj",url:getTestURL('/text')}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property('payload','hello'); + msg.should.have.property('statusCode',200); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"foo", method:"POST"}); + }); + }); + + it('should allow the message to provide the method', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"use",ret:"txt",url:getTestURL('/text')}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property('payload','hello'); + msg.should.have.property('statusCode',200); + msg.should.have.property('headers'); + msg.headers.should.have.property('content-length',''+('hello'.length)); + msg.headers.should.have.property('content-type').which.startWith('text/html'); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"foo",method:"get"}); + }); + }); + + it('should receive msg.responseUrl', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"txt",url:getTestURL('/text')}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property('statusCode',200); + msg.should.have.property('responseUrl', getTestURL('/text')); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"foo"}); + }); + }); + + it('should receive msg.responseUrl when redirected', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"txt",url:getTestURL('/redirectToText')}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property('payload','hello'); + msg.should.have.property('statusCode',200); + msg.should.have.property('responseUrl', getTestURL('/text')); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"foo"}); + }); + }); + + it('shuold output an error when request timeout occurred', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"obj",url:getTestURL('/timeout')}, + {id:"n2", type:"helper"}]; + var timeout = RED.settings.httpRequestTimeout; + RED.settings.httpRequestTimeout = 50; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property('statusCode','ECONNRESET'); + done(); + } catch(err) { + done(err); + } finally { + RED.settings.httpRequestTimeout = timeout; + } + }); + n1.receive({payload:"foo"}); }); - n1.receive({payload:"foo"}); }); }); - it('get JSON content', function(done) { - var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"obj",url:getTestURL('/json-valid')}, - {id:"n2", type:"helper"}]; - helper.load(httpRequestNode, flow, function() { - var n1 = helper.getNode("n1"); - var n2 = helper.getNode("n2"); - n2.on("input", function(msg) { - try { - msg.should.have.property('payload',{a:1}); - msg.should.have.property('statusCode',200); - msg.should.have.property('headers'); - msg.headers.should.have.property('content-type').which.startWith('application/json'); - done(); - } catch(err) { - done(err); - } + describe('HTTP header', function() { + it('should receive cookie', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"obj",url:getTestURL('/setCookie')}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.responseCookies.should.have.property('data'); + msg.responseCookies.data.should.have.property('value','hello'); + msg.should.have.property('statusCode',200); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"foo"}); + }); + }); + + it('should send cookie with string', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"obj",url:getTestURL('/checkCookie')}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property('payload','abc'); + msg.should.have.property('statusCode',200); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"foo", cookies:{data:'abc'}}); + }); + }); + + it('should send cookie with obejct data', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"obj",url:getTestURL('/checkCookie')}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property('payload','abc'); + msg.should.have.property('statusCode',200); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"foo", cookies:{data:{value:'abc'}}}); + }); + }); + + it('should send cookie by msg.headers', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"obj",url:getTestURL('/checkCookie')}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property('payload','abc'); + msg.should.have.property('statusCode',200); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"foo", cookies:{boo:'123'}, headers:{'cookie':'data=abc'}}); + }); + }); + + it('should convert all HTTP headers into lower case', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"POST",ret:"obj",url:getTestURL('/postInspect')}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property('statusCode',200); + msg.payload.should.have.property('headers'); + msg.payload.headers.should.have.property('content-type').which.startWith('text/plain'); + msg.payload.headers.should.have.property('content-length', "3"); + msg.payload.headers.should.have.property('if-modified-since','Sun, 01 Jun 2000 00:00:00 GMT'); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"foo", headers: { 'Content-Type':'text/plain', 'Content-Length': "3", 'If-Modified-Since':'Sun, 01 Jun 2000 00:00:00 GMT'}}); + }); + }); + + it('should receive HTTP header', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"txt",url:getTestURL('/headersInspect')}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property('statusCode',200); + msg.headers.should.have.property('x-test-header','bar'); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"foo"}); + }); + }); + + it('should ignore unmodified x-node-red-request-node header', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"POST",ret:"obj",url:getTestURL('/postInspect')}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.payload.headers.should.have.property('content-type').which.startWith('application/json'); + msg.payload.headers.should.not.have.property('x-node-red-request-node'); + done(); + } catch(err) { + done(err); + } + }); + // Pass in a headers property with an unmodified x-node-red-request-node hash + // This should cause the node to ignore the headers + n1.receive({payload:{foo:"bar"}, headers: { 'content-type': 'text/plain', "x-node-red-request-node":"67690139"}}); + }); + }); + + it('should use modified msg.headers property', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"POST",ret:"obj",url:getTestURL('/postInspect')}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.payload.headers.should.have.property('content-type').which.startWith('text/plain'); + msg.payload.headers.should.not.have.property('x-node-red-request-node'); + done(); + } catch(err) { + done(err); + } + }); + // Pass in a headers property with a x-node-red-request-node hash that doesn't match the contents + // This should cause the node to use the headers + n1.receive({payload:{foo:"bar"}, headers: { 'content-type': 'text/plain', "x-node-red-request-node":"INVALID_SUM"}}); }); - n1.receive({payload:"foo"}); }); }); - it('get Buffer content', function(done) { - var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"bin",url:getTestURL('/text')}, - {id:"n2", type:"helper"}]; - helper.load(httpRequestNode, flow, function() { - var n1 = helper.getNode("n1"); - var n2 = helper.getNode("n2"); - n2.on("input", function(msg) { - try { - msg.should.have.property('payload'); - Buffer.isBuffer(msg.payload).should.be.true(); - msg.should.have.property('statusCode',200); - msg.should.have.property('headers'); - msg.headers.should.have.property('content-type'); - done(); - } catch(err) { - done(err); - } + describe('protocol', function() { + it('should use msg.rejectUnauthorized', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"txt",url:getSslTestURL('/text')}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n2 = helper.getNode("n2"); + var n1 = helper.getNode("n1"); + n2.on("input", function(msg) { + try { + msg.should.have.property('payload','hello'); + msg.should.have.property('statusCode',200); + msg.should.have.property('headers'); + msg.headers.should.have.property('content-length',''+('hello'.length)); + msg.headers.should.have.property('content-type').which.startWith('text/html'); + msg.should.have.property('responseUrl').which.startWith('https://'); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"foo", rejectUnauthorized: false}); + }); + }); + + it('should use tls-config', function(done) { + var flow = [ + {id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"txt",url:getSslTestURLWithoutProtocol('/text'),tls:"n3"}, + {id:"n2", type:"helper"}, + {id:"n3", type:"tls-config", cert:"test/resources/ssl/server.crt", key:"test/resources/ssl/server.key", ca:"", verifyservercert:false}]; + var testNodes = [httpRequestNode, tlsNode]; + helper.load(testNodes, flow, function() { + var n3 = helper.getNode("n3"); + var n2 = helper.getNode("n2"); + var n1 = helper.getNode("n1"); + n2.on("input", function(msg) { + try { + msg.should.have.property('payload','hello'); + msg.should.have.property('statusCode',200); + msg.should.have.property('headers'); + msg.headers.should.have.property('content-length',''+('hello'.length)); + msg.headers.should.have.property('content-type').which.startWith('text/html'); + msg.should.have.property('responseUrl').which.startWith('https://'); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"foo"}); + }); + }); + + it('should use http_proxy', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"POST",ret:"obj",url:getTestURL('/postInspect')}, + {id:"n2", type:"helper"}]; + saveProxySetting(); + process.env.http_proxy = "http://localhost:" + testProxyPort; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + restoreProxySetting(); + try { + msg.should.have.property('statusCode',200); + msg.payload.should.have.property('headers'); + msg.payload.headers.should.have.property('x-testproxy-header','foobar'); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"foo"}); + }); + }); + + it('should use http_proxy when environment variable is invalid', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"POST",ret:"obj",url:getTestURL('/postInspect')}, + {id:"n2", type:"helper"}]; + saveProxySetting(); + process.env.http_proxy = "invalidvalue"; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + restoreProxySetting(); + try { + msg.should.have.property('statusCode',200); + msg.payload.should.have.property('headers'); + msg.payload.headers.should.not.have.property('x-testproxy-header','foobar'); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"foo"}); + }); + }); + + it('should use HTTP_PROXY', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"POST",ret:"obj",url:getTestURL('/postInspect')}, + {id:"n2", type:"helper"}]; + saveProxySetting(); + process.env.HTTP_PROXY = "http://localhost:" + testProxyPort; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + restoreProxySetting(); + try { + msg.should.have.property('statusCode',200); + msg.payload.should.have.property('headers'); + msg.payload.headers.should.have.property('x-testproxy-header','foobar'); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"foo"}); + }); + }); + + it('should use no_proxy', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"POST",ret:"obj",url:getTestURL('/postInspect')}, + {id:"n2", type:"helper"}]; + saveProxySetting(); + process.env.http_proxy = "http://localhost:" + testProxyPort; + process.env.no_proxy = "foo,localhost"; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + restoreProxySetting(); + try { + msg.should.have.property('statusCode',200); + msg.payload.headers.should.not.have.property('x-testproxy-header','foobar'); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"foo"}); + }); + }); + + it('should use NO_PROXY', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"POST",ret:"obj",url:getTestURL('/postInspect')}, + {id:"n2", type:"helper"}]; + saveProxySetting(); + process.env.HTTP_PROXY = "http://localhost:" + testProxyPort; + process.env.NO_PROXY = "foo,localhost"; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + restoreProxySetting(); + try { + msg.should.have.property('statusCode',200); + msg.payload.headers.should.not.have.property('x-testproxy-header','foobar'); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"foo"}); }); - n1.receive({payload:"foo"}); }); }); - it('returns plain text when JSON fails to parse', function(done) { - var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"obj",url:getTestURL('/json-invalid')}, - {id:"n2", type:"helper"}]; - helper.load(httpRequestNode, flow, function() { - var n1 = helper.getNode("n1"); - var n2 = helper.getNode("n2"); - n2.on("input", function(msg) { - try { - msg.should.have.property('payload',"{a:1"); - msg.should.have.property('statusCode',200); - msg.should.have.property('headers'); - msg.headers.should.have.property('content-type').which.startWith('application/json'); - done(); - } catch(err) { - done(err); - } + describe('authentication', function() { + it('should authenticate on server', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"obj",url:getTestURL('/authenticate')}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n1.credentials = {user:'userfoo', password:'passwordfoo'}; + n2.on("input", function(msg) { + try { + msg.should.have.property('statusCode',200); + msg.payload.should.have.property('user', 'userfoo'); + msg.payload.should.have.property('pass', 'passwordfoo'); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"foo"}); + }); + }); + + it('should authenticate on proxy server', function(done) { + var flow = [{id:"n1",type:"http request", wires:[["n2"]],method:"GET",ret:"obj",url:getTestURL('/proxyAuthenticate')}, + {id:"n2", type:"helper"}]; + saveProxySetting(); + process.env.http_proxy = "http://foouser:barpassword@localhost:" + testProxyPort; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + restoreProxySetting(); + try { + msg.should.have.property('statusCode',200); + msg.payload.should.have.property('user', 'foouser'); + msg.payload.should.have.property('pass', 'barpassword'); + msg.payload.should.have.property('headers'); + msg.payload.headers.should.have.property('x-testproxy-header','foobar'); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"foo"}); + }); + }); + + it('should output an error when proxy authentication was failed', function(done) { + var flow = [{id:"n1",type:"http request", wires:[["n2"]],method:"GET",ret:"obj",url:getTestURL('/proxyAuthenticate')}, + {id:"n2", type:"helper"}]; + saveProxySetting(); + process.env.http_proxy = "http://xxxuser:barpassword@localhost:" + testProxyPort; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + restoreProxySetting(); + try { + msg.should.have.property('statusCode',407); + msg.headers.should.have.property('proxy-authenticate', 'BASIC realm="test"'); + msg.payload.should.have.property('headers'); + msg.payload.headers.should.have.property('x-testproxy-header','foobar'); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"foo"}); }); - n1.receive({payload:"foo"}); }); }); - - - it('return the status code', function(done) { - var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"txt",url:getTestURL('/statusCode204')}, - {id:"n2", type:"helper"}]; - helper.load(httpRequestNode, flow, function() { - var n1 = helper.getNode("n1"); - var n2 = helper.getNode("n2"); - n2.on("input", function(msg) { - try { - msg.should.have.property('payload',''); - msg.should.have.property('statusCode',204); - done(); - } catch(err) { - done(err); - } - }); - n1.receive({payload:"foo"}); - }); - }); - - it('allow the url to be missing the http:// prefix', function(done) { - var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"txt",url:getTestURL('/text').substring("http://".length)}, - {id:"n2", type:"helper"}]; - helper.load(httpRequestNode, flow, function() { - var n1 = helper.getNode("n1"); - var n2 = helper.getNode("n2"); - n2.on("input", function(msg) { - try { - msg.should.have.property('payload','hello'); - msg.should.have.property('statusCode',200); - msg.should.have.property('headers'); - msg.headers.should.have.property('content-length',''+('hello'.length)); - msg.headers.should.have.property('content-type').which.startWith('text/html'); - done(); - } catch(err) { - done(err); - } - }); - n1.receive({payload:"foo"}); - }); - }); - - it('reject non http:// schemes - node config', function(done) { - var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"txt",url:"ftp://foo"}, - {id:"n2", type:"helper"}]; - helper.load(httpRequestNode, flow, function() { - var n1 = helper.getNode("n1"); - var n2 = helper.getNode("n2"); - var inError = false; - n2.on("input", function(msg) { - inError = true; - }); - n1.receive({payload:"foo"}); - setTimeout(function() { - if (inError) { - done(new Error("non http(s):// scheme allowed through")) - } else { - done(); - } - },20) - }); - }); - - it('reject non http:// schemes - msg.url', function(done) { - var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"txt"}, - {id:"n2", type:"helper"}]; - helper.load(httpRequestNode, flow, function() { - var n1 = helper.getNode("n1"); - var n2 = helper.getNode("n2"); - var inError = false; - n2.on("input", function(msg) { - inError = true; - }); - n1.receive({payload:"foo",url:"ftp://foo"}); - setTimeout(function() { - if (inError) { - done(new Error("non http(s):// scheme allowed through")) - } else { - done(); - } - },20) - }); - }); - it('allow the message to provide the url', function(done) { - var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"txt"}, - {id:"n2", type:"helper"}]; - helper.load(httpRequestNode, flow, function() { - var n1 = helper.getNode("n1"); - var n2 = helper.getNode("n2"); - n2.on("input", function(msg) { - try { - msg.should.have.property('payload','hello'); - msg.should.have.property('statusCode',200); - msg.should.have.property('headers'); - msg.headers.should.have.property('content-length',''+('hello'.length)); - msg.headers.should.have.property('content-type').which.startWith('text/html'); - done(); - } catch(err) { - done(err); - } - }); - n1.receive({payload:"foo",url:getTestURL('/text')}); - }); - }); - - it('allow the message to provide the method', function(done) { - var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"use",ret:"txt",url:getTestURL('/text')}, - {id:"n2", type:"helper"}]; - helper.load(httpRequestNode, flow, function() { - var n1 = helper.getNode("n1"); - var n2 = helper.getNode("n2"); - n2.on("input", function(msg) { - try { - msg.should.have.property('payload','hello'); - msg.should.have.property('statusCode',200); - msg.should.have.property('headers'); - msg.headers.should.have.property('content-length',''+('hello'.length)); - msg.headers.should.have.property('content-type').which.startWith('text/html'); - done(); - } catch(err) { - done(err); - } - }); - n1.receive({payload:"foo",method:"get"}); - }); - }); - - it('allow the url to contain mustache placeholders', function(done) { - var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"txt",url:getTestURL('/te{{placeholder}}')}, - {id:"n2", type:"helper"}]; - helper.load(httpRequestNode, flow, function() { - var n1 = helper.getNode("n1"); - var n2 = helper.getNode("n2"); - n2.on("input", function(msg) { - try { - msg.should.have.property('payload','hello'); - msg.should.have.property('statusCode',200); - msg.should.have.property('headers'); - msg.headers.should.have.property('content-length',''+('hello'.length)); - msg.headers.should.have.property('content-type').which.startWith('text/html'); - done(); - } catch(err) { - done(err); - } - }); - n1.receive({payload:"foo",placeholder:"xt"}); - }); - }); - - it('send the payload as the body of a POST as application/json', function(done) { - var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"POST",ret:"obj",url:getTestURL('/postInspect')}, - {id:"n2", type:"helper"}]; - helper.load(httpRequestNode, flow, function() { - var n1 = helper.getNode("n1"); - var n2 = helper.getNode("n2"); - n2.on("input", function(msg) { - try { - msg.should.have.property('payload'); - msg.payload.body.should.eql('{"foo":"abcde"}'); - msg.payload.headers.should.have.property('content-type').which.startWith('application/json'); - - msg.should.have.property('statusCode',200); - msg.should.have.property('headers'); - msg.headers.should.have.property('content-type').which.startWith('application/json'); - done(); - } catch(err) { - done(err); - } - }); - n1.receive({payload:{foo:"abcde"}}); - }); - }); - - it('send a payload of 0 as the body of a POST as text/plain', function(done) { - var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"POST",ret:"obj",url:getTestURL('/postInspect')}, - {id:"n2", type:"helper"}]; - helper.load(httpRequestNode, flow, function() { - var n1 = helper.getNode("n1"); - var n2 = helper.getNode("n2"); - n2.on("input", function(msg) { - try { - msg.should.have.property('payload'); - msg.payload.body.should.eql('0'); - msg.payload.headers.should.have.property('content-length','1'); - msg.payload.headers.should.have.property('content-type').which.startWith('text/plain'); - - msg.should.have.property('statusCode',200); - msg.should.have.property('headers'); - done(); - } catch(err) { - done(err); - } - }); - n1.receive({payload:0, headers: { 'content-type': 'text/plain'}}); - }); - }); - - it('send an Object payload as the body of a POST', function(done) { - var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"POST",ret:"obj",url:getTestURL('/postInspect')}, - {id:"n2", type:"helper"}]; - helper.load(httpRequestNode, flow, function() { - var n1 = helper.getNode("n1"); - var n2 = helper.getNode("n2"); - n2.on("input", function(msg) { - try { - msg.should.have.property('payload'); - msg.payload.body.should.eql('{"foo":"abcde"}'); - msg.payload.headers.should.have.property('content-type').which.startWith('text/plain'); - - msg.should.have.property('statusCode',200); - msg.should.have.property('headers'); - msg.headers.should.have.property('content-type').which.startWith('application/json'); - done(); - } catch(err) { - done(err); - } - }); - n1.receive({payload:{foo:"abcde"}, headers: { 'content-type': 'text/plain'}}); - }); - }) - - it('send a Buffer as the body of a POST', function(done) { - var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"POST",ret:"obj",url:getTestURL('/postInspect')}, - {id:"n2", type:"helper"}]; - helper.load(httpRequestNode, flow, function() { - var n1 = helper.getNode("n1"); - var n2 = helper.getNode("n2"); - n2.on("input", function(msg) { - try { - msg.should.have.property('payload'); - msg.payload.body.should.eql('hello'); - msg.payload.headers.should.have.property('content-type').which.startWith('text/plain'); - - msg.should.have.property('statusCode',200); - msg.should.have.property('headers'); - msg.headers.should.have.property('content-type').which.startWith('application/json'); - done(); - } catch(err) { - done(err); - } - }); - n1.receive({payload:new Buffer('hello'), headers: { 'content-type': 'text/plain'}}); - }); - }) - - it('send a Buffer as the body of a POST', function(done) { - var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"POST",ret:"obj",url:getTestURL('/postInspect')}, - {id:"n2", type:"helper"}]; - helper.load(httpRequestNode, flow, function() { - var n1 = helper.getNode("n1"); - var n2 = helper.getNode("n2"); - n2.on("input", function(msg) { - try { - msg.should.have.property('payload'); - msg.payload.body.should.eql('hello'); - msg.payload.headers.should.have.property('content-type').which.startWith('text/plain'); - - msg.should.have.property('statusCode',200); - msg.should.have.property('headers'); - msg.headers.should.have.property('content-type').which.startWith('application/json'); - done(); - } catch(err) { - done(err); - } - }); - n1.receive({payload:new Buffer('hello'), headers: { 'content-type': 'text/plain'}}); - }); - }) - - it('ignores unmodified msg.headers property', function(done) { - var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"POST",ret:"obj",url:getTestURL('/postInspect')}, - {id:"n2", type:"helper"}]; - helper.load(httpRequestNode, flow, function() { - var n1 = helper.getNode("n1"); - var n2 = helper.getNode("n2"); - n2.on("input", function(msg) { - try { - msg.payload.headers.should.have.property('content-type').which.startWith('application/json'); - msg.payload.headers.should.not.have.property('x-node-red-request-node'); - done(); - } catch(err) { - done(err); - } - }); - // Pass in a headers property with an unmodified x-node-red-request-node hash - // This should cause the node to ignore the headers - n1.receive({payload:{foo:"bar"}, headers: { 'content-type': 'text/plain', "x-node-red-request-node":"67690139"}}); - }); - }) - - it('uses modified msg.headers property', function(done) { - var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"POST",ret:"obj",url:getTestURL('/postInspect')}, - {id:"n2", type:"helper"}]; - helper.load(httpRequestNode, flow, function() { - var n1 = helper.getNode("n1"); - var n2 = helper.getNode("n2"); - n2.on("input", function(msg) { - try { - msg.payload.headers.should.have.property('content-type').which.startWith('text/plain'); - msg.payload.headers.should.not.have.property('x-node-red-request-node'); - done(); - } catch(err) { - done(err); - } - }); - // Pass in a headers property with a x-node-red-request-node hash that doesn't match the contents - // This should cause the node to use the headers - n1.receive({payload:{foo:"bar"}, headers: { 'content-type': 'text/plain', "x-node-red-request-node":"INVALID_SUM"}}); - }); - }) - }); diff --git a/test/nodes/core/io/22-websocket_spec.js b/test/nodes/core/io/22-websocket_spec.js index 17a75c89c..b94aaa9af 100644 --- a/test/nodes/core/io/22-websocket_spec.js +++ b/test/nodes/core/io/22-websocket_spec.js @@ -17,7 +17,7 @@ var ws = require("ws"); var when = require("when"); var should = require("should"); -var helper = require("../../helper.js"); +var helper = require("node-red-node-test-helper"); var websocketNode = require("../../../../nodes/core/io/22-websocket.js"); var sockets = []; diff --git a/test/nodes/core/io/23-watch_spec.js b/test/nodes/core/io/23-watch_spec.js index 39d8733b7..8f6506c57 100644 --- a/test/nodes/core/io/23-watch_spec.js +++ b/test/nodes/core/io/23-watch_spec.js @@ -17,7 +17,7 @@ var fs = require("fs-extra"); var path = require("path"); var should = require("should"); -var helper = require("../../helper.js"); +var helper = require("node-red-node-test-helper"); var watchNode = require("../../../../nodes/core/io/23-watch.js"); diff --git a/test/nodes/core/io/31-tcpin_spec.js b/test/nodes/core/io/31-tcpin_spec.js index 1bc3e239b..4e92cc12e 100644 --- a/test/nodes/core/io/31-tcpin_spec.js +++ b/test/nodes/core/io/31-tcpin_spec.js @@ -17,7 +17,8 @@ var net = require("net"); var should = require("should"); var stoppable = require('stoppable'); -var helper = require("../../helper.js"); +var helper = require("node-red-node-test-helper"); + var tcpinNode = require("../../../../nodes/core/io/31-tcpin.js"); @@ -46,7 +47,7 @@ describe('TCP in Node', function() { sock.end(); } } - + function startServer(done) { server_port += 1; server = stoppable(net.createServer(function(c) { @@ -59,7 +60,7 @@ describe('TCP in Node', function() { function stopServer(done) { server.stop(done); } - + function send(wdata) { var opt = {port:port, host:"localhost"}; var client = net.createConnection(opt, function() { diff --git a/test/nodes/core/io/31-tcprequest_spec.js b/test/nodes/core/io/31-tcprequest_spec.js index 905ccc9b8..651384ad5 100644 --- a/test/nodes/core/io/31-tcprequest_spec.js +++ b/test/nodes/core/io/31-tcprequest_spec.js @@ -17,7 +17,7 @@ var net = require("net"); var should = require("should"); var stoppable = require('stoppable'); -var helper = require("../../helper.js"); +var helper = require("node-red-node-test-helper"); var tcpinNode = require("../../../../nodes/core/io/31-tcpin.js"); @@ -71,7 +71,7 @@ describe('TCP Request Node', function() { } }); } - + it('should send & recv data', function(done) { var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"time", splitc: "0", wires:[["n2"]] }, {id:"n2", type:"helper"}]; diff --git a/test/nodes/core/io/32-udpin_spec.js b/test/nodes/core/io/32-udpin_spec.js index f26fc6e5d..c767af5e6 100644 --- a/test/nodes/core/io/32-udpin_spec.js +++ b/test/nodes/core/io/32-udpin_spec.js @@ -16,7 +16,7 @@ var dgram = require("dgram"); var should = require("should"); -var helper = require("../../helper.js"); +var helper = require("node-red-node-test-helper"); var udpNode = require("../../../../nodes/core/io/32-udp.js"); diff --git a/test/nodes/core/io/32-udpout_spec.js b/test/nodes/core/io/32-udpout_spec.js index d614ed64b..fdeb032b6 100644 --- a/test/nodes/core/io/32-udpout_spec.js +++ b/test/nodes/core/io/32-udpout_spec.js @@ -16,7 +16,7 @@ var dgram = require("dgram"); var should = require("should"); -var helper = require("../../helper.js"); +var helper = require("node-red-node-test-helper"); var udpNode = require("../../../../nodes/core/io/32-udp.js"); diff --git a/test/nodes/core/logic/10-switch_spec.js b/test/nodes/core/logic/10-switch_spec.js index a5d057c92..e655a3fba 100644 --- a/test/nodes/core/logic/10-switch_spec.js +++ b/test/nodes/core/logic/10-switch_spec.js @@ -17,7 +17,7 @@ var should = require("should"); var switchNode = require("../../../../nodes/core/logic/10-switch.js"); -var helper = require("../../helper.js"); +var helper = require("node-red-node-test-helper"); var RED = require("../../../../red/red.js"); describe('switch Node', function() { @@ -103,7 +103,7 @@ describe('switch Node', function() { helperNode1.on("input", function(msg) { try { if (shouldReceive === true) { - msg.payload.should.equal(sendPayload); + should.equal(msg.payload,sendPayload); done(); } else { should.fail(null, null, "We should never get an input!"); @@ -168,8 +168,6 @@ describe('switch Node', function() { }); } - - it('should check if payload equals given value', function(done) { genericSwitchTest("eq", "Hello", true, true, "Hello", done); }); @@ -258,6 +256,43 @@ describe('switch Node', function() { genericSwitchTest("regex", "[abc]+", true, true, "abbabac", done); }); + it('should check if payload if of type string ', function(done) { + genericSwitchTest("istype", "string", true, true, "Hello", done); + }); + it('should check if payload if of type number ', function(done) { + genericSwitchTest("istype", "number", true, true, 999, done); + }); + it('should check if payload if of type number 0', function(done) { + genericSwitchTest("istype", "number", true, true, 0, done); + }); + it('should check if payload if of type boolean true', function(done) { + genericSwitchTest("istype", "boolean", true, true, true, done); + }); + it('should check if payload if of type boolean false', function(done) { + genericSwitchTest("istype", "boolean", true, true, true, done); + }); + it('should check if payload if of type array ', function(done) { + genericSwitchTest("istype", "array", true, true, [1,2,3,"a","b"], done); + }); + it('should check if payload if of type buffer ', function(done) { + genericSwitchTest("istype", "buffer", true, true, Buffer.from("Hello"), done); + }); + it('should check if payload if of type object ', function(done) { + genericSwitchTest("istype", "object", true, true, {a:1,b:"b",c:true}, done); + }); + it('should check if payload if of type JSON string ', function(done) { + genericSwitchTest("istype", "json", true, true, JSON.stringify({a:1,b:"b",c:true}), done); + }); + it('should check if payload if of type JSON string (and fail if not) ', function(done) { + genericSwitchTest("istype", "json", true, false, "Hello", done); + }); + it('should check if payload if of type null', function(done) { + genericSwitchTest("istype", "null", true, true, null, done); + }); + it('should check if payload if of type undefined', function(done) { + genericSwitchTest("istype", "undefined", true, true, undefined, done); + }); + it('should match regex with ignore-case flag set true', function(done) { var flow = [{id:"switchNode1",type:"switch",name:"switchNode",property:"payload",rules:[{"t":"regex","v":"onetwothree","case":true}],checkall:true,outputs:1,wires:[["helperNode1"]]}, {id:"helperNode1", type:"helper", wires:[]}]; @@ -433,7 +468,6 @@ describe('switch Node', function() { var flow = [{id:"switchNode1",type:"switch",name:"switchNode",property:"payload",rules:[{"t":"nnull"}],checkall:false,outputs:1,wires:[["helperNode1"]]}, {id:"helperNode1", type:"helper", wires:[]}]; - helper.load(switchNode, flow, function() { var switchNode1 = helper.getNode("switchNode1"); var helperNode1 = helper.getNode("helperNode1"); @@ -787,5 +821,4 @@ describe('switch Node', function() { n1.receive({payload:1, parts:{index:0, count:4, id:222}}); }); }); - }); diff --git a/test/nodes/core/logic/15-change_spec.js b/test/nodes/core/logic/15-change_spec.js index 2a08b1b3e..f5ca0f3d2 100644 --- a/test/nodes/core/logic/15-change_spec.js +++ b/test/nodes/core/logic/15-change_spec.js @@ -17,7 +17,7 @@ var should = require("should"); var changeNode = require("../../../../nodes/core/logic/15-change.js"); -var helper = require("../../helper.js"); +var helper = require("node-red-node-test-helper"); describe('change Node', function() { diff --git a/test/nodes/core/logic/16-range_spec.js b/test/nodes/core/logic/16-range_spec.js index d52e52158..79ca7ea45 100644 --- a/test/nodes/core/logic/16-range_spec.js +++ b/test/nodes/core/logic/16-range_spec.js @@ -17,7 +17,7 @@ var should = require("should"); var rangeNode = require("../../../../nodes/core/logic/16-range.js"); -var helper = require("../../helper.js"); +var helper = require("node-red-node-test-helper"); describe('range Node', function() { diff --git a/test/nodes/core/logic/17-split_spec.js b/test/nodes/core/logic/17-split_spec.js index 5fbfdeebb..dfc57fc3b 100644 --- a/test/nodes/core/logic/17-split_spec.js +++ b/test/nodes/core/logic/17-split_spec.js @@ -17,7 +17,7 @@ var should = require("should"); var splitNode = require("../../../../nodes/core/logic/17-split.js"); var joinNode = require("../../../../nodes/core/logic/17-split.js"); -var helper = require("../../helper.js"); +var helper = require("node-red-node-test-helper"); var RED = require("../../../../red/red.js"); describe('SPLIT node', function() { diff --git a/test/nodes/core/logic/18-sort_spec.js b/test/nodes/core/logic/18-sort_spec.js index 0c6cf5769..a585d7dd5 100644 --- a/test/nodes/core/logic/18-sort_spec.js +++ b/test/nodes/core/logic/18-sort_spec.js @@ -16,7 +16,7 @@ var should = require("should"); var sortNode = require("../../../../nodes/core/logic/18-sort.js"); -var helper = require("../../helper.js"); +var helper = require("node-red-node-test-helper"); var RED = require("../../../../red/red.js"); describe('SORT node', function() { diff --git a/test/nodes/core/logic/19-batch_spec.js b/test/nodes/core/logic/19-batch_spec.js index cffaf12e5..efe2d9312 100644 --- a/test/nodes/core/logic/19-batch_spec.js +++ b/test/nodes/core/logic/19-batch_spec.js @@ -16,7 +16,7 @@ var should = require("should"); var batchNode = require("../../../../nodes/core/logic/19-batch.js"); -var helper = require("../../helper.js"); +var helper = require("node-red-node-test-helper"); var RED = require("../../../../red/red.js"); describe('BATCH node', function() { diff --git a/test/nodes/core/parsers/70-CSV_spec.js b/test/nodes/core/parsers/70-CSV_spec.js index f3ddd2dea..6b12b4585 100644 --- a/test/nodes/core/parsers/70-CSV_spec.js +++ b/test/nodes/core/parsers/70-CSV_spec.js @@ -16,7 +16,7 @@ var should = require("should"); var csvNode = require("../../../../nodes/core/parsers/70-CSV.js"); -var helper = require("../../helper.js"); +var helper = require("node-red-node-test-helper"); describe('CSV node', function() { diff --git a/test/nodes/core/parsers/70-HTML_spec.js b/test/nodes/core/parsers/70-HTML_spec.js index 5874ed3ae..94eb0c7f4 100644 --- a/test/nodes/core/parsers/70-HTML_spec.js +++ b/test/nodes/core/parsers/70-HTML_spec.js @@ -19,7 +19,7 @@ var path = require("path"); var fs = require('fs-extra'); var htmlNode = require("../../../../nodes/core/parsers/70-HTML.js"); -var helper = require("../../helper.js"); +var helper = require("node-red-node-test-helper"); describe('html node', function() { @@ -69,7 +69,7 @@ describe('html node', function() { }); }); - it('should retrieve header contents if asked to by msg.select - alternative property', function(done) { + it('should retrieve header contents if asked to by msg.select - alternative in property', function(done) { fs.readFile(file, 'utf8', function(err, data) { var flow = [{id:"n1",type:"html",property:"foo",wires:[["n2"]],func:"return msg;"}, {id:"n2", type:"helper"}]; @@ -79,7 +79,7 @@ describe('html node', function() { var n2 = helper.getNode("n2"); n2.on("input", function(msg) { msg.should.have.property('topic', 'bar'); - should.equal(msg.foo, 'This is a test page for node 70-HTML'); + msg.foo[0].should.equal('This is a test page for node 70-HTML'); done(); }); n1.receive({foo:data,topic:"bar",select:"h1"}); @@ -87,6 +87,24 @@ describe('html node', function() { }); }); + it('should retrieve header contents if asked to by msg.select - alternative in and out properties', function(done) { + fs.readFile(file, 'utf8', function(err, data) { + var flow = [{id:"n1",type:"html",property:"foo",outproperty:"bar",tag:"h1",wires:[["n2"]],func:"return msg;"}, + {id:"n2", type:"helper"}]; + + helper.load(htmlNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + msg.should.have.property('topic', 'bar'); + msg.bar[0].should.equal('This is a test page for node 70-HTML'); + done(); + }); + n1.receive({foo:data,topic:"bar"}); + }); + }); + }); + it('should emit an empty array if no matching elements', function(done) { fs.readFile(file, 'utf8', function(err, data) { var flow = [{id:"n1",type:"html",wires:[["n2"]],func:"return msg;"}, diff --git a/test/nodes/core/parsers/70-JSON_spec.js b/test/nodes/core/parsers/70-JSON_spec.js index 371212b79..28fffde85 100644 --- a/test/nodes/core/parsers/70-JSON_spec.js +++ b/test/nodes/core/parsers/70-JSON_spec.js @@ -16,7 +16,7 @@ var should = require("should"); var jsonNode = require("../../../../nodes/core/parsers/70-JSON.js"); -var helper = require("../../helper.js"); +var helper = require("node-red-node-test-helper"); describe('JSON node', function() { diff --git a/test/nodes/core/parsers/70-XML_spec.js b/test/nodes/core/parsers/70-XML_spec.js index 2706892be..c45c896a8 100644 --- a/test/nodes/core/parsers/70-XML_spec.js +++ b/test/nodes/core/parsers/70-XML_spec.js @@ -16,7 +16,7 @@ var should = require("should"); var xmlNode = require("../../../../nodes/core/parsers/70-XML.js"); -var helper = require("../../helper.js"); +var helper = require("node-red-node-test-helper"); describe('XML node', function() { diff --git a/test/nodes/core/parsers/70-YAML_spec.js b/test/nodes/core/parsers/70-YAML_spec.js index 2acfd17fc..eacdfe082 100644 --- a/test/nodes/core/parsers/70-YAML_spec.js +++ b/test/nodes/core/parsers/70-YAML_spec.js @@ -16,7 +16,7 @@ var should = require("should"); var yamlNode = require("../../../../nodes/core/parsers/70-YAML.js"); -var helper = require("../../helper.js"); +var helper = require("node-red-node-test-helper"); describe('YAML node', function() { diff --git a/test/nodes/core/storage/28-tail_spec.js b/test/nodes/core/storage/28-tail_spec.js index b722a47d6..e3961ebfb 100644 --- a/test/nodes/core/storage/28-tail_spec.js +++ b/test/nodes/core/storage/28-tail_spec.js @@ -20,7 +20,7 @@ var os = require('os'); var fs = require('fs-extra'); var sinon = require('sinon'); var tailNode = require("../../../../nodes/core/storage/28-tail.js"); -var helper = require("../../helper.js"); +var helper = require("node-red-node-test-helper"); describe('tail Node', function() { diff --git a/test/nodes/core/storage/50-file_spec.js b/test/nodes/core/storage/50-file_spec.js index e79b26abe..f6eb41d56 100644 --- a/test/nodes/core/storage/50-file_spec.js +++ b/test/nodes/core/storage/50-file_spec.js @@ -20,7 +20,7 @@ var fs = require('fs-extra'); var os = require('os'); var sinon = require("sinon"); var fileNode = require("../../../../nodes/core/storage/50-file.js"); -var helper = require("../../helper.js"); +var helper = require("node-red-node-test-helper"); describe('file Nodes', function() { diff --git a/test/nodes/helper.js b/test/nodes/helper.js deleted file mode 100644 index 4e165103b..000000000 --- a/test/nodes/helper.js +++ /dev/null @@ -1,169 +0,0 @@ -/** - * 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 when = require("when"); -var request = require('supertest'); -var express = require("express"); -var stoppable = require('stoppable'); -var nock; -if (!process.version.match(/^v0\.[0-9]\./)) { - // only set nock for node >= 0.10 - try { - nock = require('nock'); - } catch (err) { - // nevermind, will skip nock tests - nock = null; - } -} -var RED = require("../../red/red.js"); -var redNodes = require("../../red/runtime/nodes"); -var flows = require("../../red/runtime/nodes/flows"); -var credentials = require("../../red/runtime/nodes/credentials"); -var comms = require("../../red/api/editor/comms.js"); -var log = require("../../red/util/log.js"); -var context = require("../../red/runtime/nodes/context.js"); -var events = require("../../red/runtime/events.js"); - -var http = require('http'); -var express = require('express'); -var app = express(); - -var address = '127.0.0.1'; -var listenPort = 0; // use ephemeral port -var port; -var url; -var logSpy; -var server; - -function helperNode(n) { - RED.nodes.createNode(this, n); -} - -module.exports = { - load: function(testNode, testFlows, testCredentials, cb) { - var i; - - logSpy = sinon.spy(log,"log"); - logSpy.FATAL = log.FATAL; - logSpy.ERROR = log.ERROR; - logSpy.WARN = log.WARN; - logSpy.INFO = log.INFO; - logSpy.DEBUG = log.DEBUG; - logSpy.TRACE = log.TRACE; - logSpy.METRIC = log.METRIC; - - if (typeof testCredentials === 'function') { - cb = testCredentials; - testCredentials = {}; - } - - var storage = { - getFlows: function() { - return when.resolve({flows:testFlows,credentials:testCredentials}); - } - }; - - var settings = { - available: function() { return false; } - }; - - var red = {}; - for (i in RED) { - if (RED.hasOwnProperty(i) && !/^(init|start|stop)$/.test(i)) { - var propDescriptor = Object.getOwnPropertyDescriptor(RED,i); - Object.defineProperty(red,i,propDescriptor); - } - } - - red["_"] = function(messageId) { - return messageId; - }; - - redNodes.init({events:events,settings:settings, storage:storage,log:log,}); - RED.nodes.registerType("helper", helperNode); - if (Array.isArray(testNode)) { - for (i = 0; i < testNode.length; i++) { - testNode[i](red); - } - } else { - testNode(red); - } - flows.load().then(function() { - flows.startFlows(); - should.deepEqual(testFlows, flows.getFlows().flows); - cb(); - }); - }, - - unload: function() { - // TODO: any other state to remove between tests? - redNodes.clearRegistry(); - logSpy.restore(); - context.clean({allNodes:[]}); - return flows.stopFlows(); - }, - - getNode: function(id) { - return flows.get(id); - }, - - credentials: credentials, - - clearFlows: function() { - return flows.stopFlows(); - }, - - request: function() { - return request(RED.httpAdmin); - }, - - startServer: function(done) { - server = stoppable(http.createServer(function(req,res) { app(req,res); }), 0); - RED.init(server, { - SKIP_BUILD_CHECK: true, - logging:{console:{level:'off'}} - }); - server.listen(listenPort, address); - server.on('listening', function() { - port = server.address().port; - url = 'http://' + address + ':' + port; - comms.start(); - done(); - }); - }, - - //TODO consider saving TCP handshake/server reinit on start/stop/start sequences - stopServer: function(done) { - if (server) { - try { - comms.stop(); - server.stop(done); - } catch(e) { - done(); - } - } else { - done(); - } - }, - - url: function() { return url; }, - - nock: nock, - - log: function() { return logSpy;} -}; diff --git a/test/red/runtime-api/settings_spec.js b/test/red/runtime-api/settings_spec.js index 534f615d6..c08739afb 100644 --- a/test/red/runtime-api/settings_spec.js +++ b/test/red/runtime-api/settings_spec.js @@ -45,6 +45,28 @@ describe("runtime-api/settings", function() { /* + +before(function() { + sinon.stub(theme,"settings",function() { return { test: 456 };}); + app = express(); + app.get("/settings",info.runtimeSettings); + app.get("/settingsWithUser",function(req,res,next) { + req.user = { + username: "nick", + permissions: "*", + image: "http://example.com", + anonymous: false, + private: "secret" + } + next(); + },info.runtimeSettings); +}); +after(function() { + theme.settings.restore(); +}); + + + it('returns the filtered settings', function(done) { info.init({ settings: { @@ -77,6 +99,42 @@ describe("runtime-api/settings", function() { res.body.should.have.property("testNodeSetting","helloWorld"); res.body.should.not.have.property("foo",123); res.body.should.have.property("flowEncryptionType","test-key-type"); + res.body.should.not.have.property("user"); + done(); + }); + }); + it('returns the filtered user in settings', function(done) { + info.init({ + settings: { + foo: 123, + httpNodeRoot: "testHttpNodeRoot", + version: "testVersion", + paletteCategories :["red","blue","green"], + exportNodeSettings: function(obj) { + obj.testNodeSetting = "helloWorld"; + } + }, + nodes: { + paletteEditorEnabled: function() { return true; }, + getCredentialKeyType: function() { return "test-key-type"} + }, + log: { error: console.error }, + storage: {} + }); + request(app) + .get("/settingsWithUser") + .expect(200) + .end(function(err,res) { + if (err) { + return done(err); + } + res.body.should.have.property("user"); + res.body.user.should.have.property("username","nick"); + res.body.user.should.have.property("permissions","*"); + res.body.user.should.have.property("image","http://example.com"); + res.body.user.should.have.property("anonymous",false); + res.body.user.should.not.have.property("private"); + done(); }); }); diff --git a/test/resources/ssl/server.crt b/test/resources/ssl/server.crt new file mode 100644 index 000000000..493afc252 --- /dev/null +++ b/test/resources/ssl/server.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDMDCCAhgCCQDPGPyu5M6ZaDANBgkqhkiG9w0BAQsFADBZMQswCQYDVQQGEwJB +VTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0 +cyBQdHkgTHRkMRIwEAYDVQQDDAlsb2NhbGhvc3QwIBcNMTgwMzE2MDY0ODU1WhgP +MjExODAyMjAwNjQ4NTVaMFkxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0 +YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMM +CWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMifGelM +k/b3HeIj98y9P5jS+Qblqpq7+gsCaL+qglMFmG0QXe6Ordkrh3xeY0uTkaFatwLM +WMzoX60nNdaVjC9U9RlQLK/3nncCveexxRUGtI8VpxN04ivBE/ULhtJeStQFrfyt +LWr1WWf8o8P/EWzZnh0Y1oHc0XqhOPHu9Nfd9kn5nfHNd/xbY8KXa4DkVSJ1lLFK +3t/nSWttchF8zKgNpoQznNGqUTjT28l0sS8fyH76DyRj3Ke6xdNxX2NRUU0PnGFI +RMsBG4Qrzo5xY7lQP7uVVgZUlxryw+NuZuC1PBXaUKJOf6CGwrTq5WB9zF1iBZCs +wD68NvtLd0kHEgECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAfqNOg2v90r5x4lFo +SYmPUoX24gdwHd/mfCzDJksB8n98X1eULYZqqRF2Q7LMkYu/twxfR3EKQX1HZxQY +LpGUYX4ubJdVTy13opJs8B4NkhvRuOAP0+b7RVt4RfuxLX9tYOB98tEbf7Mj0ccq +F4sHi+PMCh64K7rNWECHar0F51yNtNXcxJPMuHZVmj0/U7h6ZxNf+GzdTi8YKmVy +5OHI7xol/II/v3QOi1L+BaEIUkqYODKuQouJVIzu4zX6JRfAaxwjJmliYoJm7OEY +dFMEQUw1Ggsos+KbkGi9mCDbveYpWcZTR8nfPwmx+oJtt47DTHUC3KSdRxgtfjGs +otmVSw== +-----END CERTIFICATE----- diff --git a/test/resources/ssl/server.key b/test/resources/ssl/server.key new file mode 100644 index 000000000..4b8d6a1a5 --- /dev/null +++ b/test/resources/ssl/server.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAyJ8Z6UyT9vcd4iP3zL0/mNL5BuWqmrv6CwJov6qCUwWYbRBd +7o6t2SuHfF5jS5ORoVq3AsxYzOhfrSc11pWML1T1GVAsr/eedwK957HFFQa0jxWn +E3TiK8ET9QuG0l5K1AWt/K0tavVZZ/yjw/8RbNmeHRjWgdzReqE48e701932Sfmd +8c13/FtjwpdrgORVInWUsUre3+dJa21yEXzMqA2mhDOc0apRONPbyXSxLx/IfvoP +JGPcp7rF03FfY1FRTQ+cYUhEywEbhCvOjnFjuVA/u5VWBlSXGvLD425m4LU8FdpQ +ok5/oIbCtOrlYH3MXWIFkKzAPrw2+0t3SQcSAQIDAQABAoIBAGmbryUrpZxU24tG +idRiLw9Ax8yEq7lGiMqw2vlCRdZ0VJfdDMVeoE945ZpniXeoV/oLadl0Pq6nCG56 +/JFYKfJkk51eoheDjwxxCgzkfK2j2PqVWF0ao1CLE/ljtvYYouVXlA42D3mFbCoc +SQ0MwVx+dgg1If48gp0+L17T/ll/VOOQumts5UzoKC8YABLL00g5ZL9/jZlVipgl +HfENMPWOfy3q5kSgQqvTWTMdSE6644ryV890mrwcC/RzqQBSNgRh1Lqx3jcXQSdN +x5C19gEK60hZqcvzBkKYudMHUC6I0lcuao1xwBnHUQIVKmLFPZBUIQq3tVar/YUc +d65cJpECgYEA5D9QilQpHxv875wBvBOEbyt9TqDBBN/5JpGQ9sBKpA0eNp3UzrOr ++n0TlyoDZYjkxgNJScS4TpeKde1Hk5j2kkMngjS69dn4G6wmOI79gAOGrCiJd1/I +AWb09KxUKlWBbfKuLHdl1wSMCYQornDdXxYCxhv9sMZKbEJ//tsI420CgYEA4QPf +n/dRAm+6zwNQTWOYWlj5jsG1TilBBCtoRqUqVlrAgR6rS1lgOleHkVrWH0g0Lkmh +9DxWiWuNNXxdU/5zx9AQn/JuHuL8EjDLN5r7idcg2LtEElCkr12y0I9nzS2OOZnj +MTioIh+hghzNuk09NlVJrHi48bJUVL/6Ws7ruGUCgYBT9UJAD+MscVQiI2Wz9A30 +ArBOOu2lSGnSmRsU2PjbzYN+naIJAqhRNK7/HNIxCCD3AYB05SrSpgWliUmZ7ltM +w+0FhTX8d1g/fZx1k4uGCkYAj8y5H39nnKKgWb9/7wH0Gp+c9bJ9XEvSuE1qlVOo +xWTx0JwJ6Xa4yeFhMtrbJQKBgF/FfErjwvEciRBPQsCNoWzi7eUbAYYw/OE/cHSR +HAIBQmoymYnKkrCCTMtLNFPAMaV55ZrEi7iVtFaNhlOXu8PSBSFu1/wBdHRxnC0g +o+s5S1uz6Pc6p72UTeWDBBVKTHyryQ1MJhPQDrgIdm/TLDiR+HeWMnF9C3O++lno +NGAZAoGBAKhsmatxVD9B3jvUDd/CWhXVDSZQECrfJ+Uy1q6b5NO5yMibpIZv14Nj +VT+b2qXoO1wL6htTRJXXNPmrB/JtrLiLg/vxVuA7CPSgot8SDA+/lbRhf1n/SKnD +ECXrEUmq28SgBItbY4vcy5PVEHRvlzqO/LpD6Y7iGNpR7zw9Yk3b +-----END RSA PRIVATE KEY-----