Repository: serverless/serverless (JavaScript) — Bug Fix

Issue:
Variable Defaults cannot have + in them
<!--
1. If you have a question and not a bug report please ask first at http://forum.serverless.com
2. Please check if an issue already exists. This bug may have already been documented
3. Check out and follow our Guidelines: https://github.com/serverless/serverless/blob/master/CONTRIBUTING.md
4. Fill out the whole template so we have a good overview on the issue
5. Do not remove any section of the template. If something is not applicable leave it empty but leave it in the Issue
6. Please follow the template, otherwise we'll have to ask you to update it
-->

# Variable defaults cannot have `+` in them

## Description

- What went wrong?
When putting in a default value for a variable (at least for opt and env), the default value cannot have a `+` character, even if it is enclosed in single quotes (double quotes as well). When parsing, there is no error, but it prints out the entire line to be interpreted: `TEST: ${env:MYVAR, 'test+'}`.
- What did you expect should have happened?
It should have printed out `TEST: 'test'`
- What was the config you used?
```
service: development
frameworkVersion: '>=1.28.0 <2.0.0'

provider:
  name: aws
  runtime: go1.x
  environment:
    TEST: ${env:MYVAR,'test+'}
    TEST2: ${env:MYVAR,'test'}
package:
  exclude:
    - ./**
  include:
    - ./bin/**

functions:
  hello:
    handler: bin/hello
    events:
      - http:
          path: hello
          method: get
  world:
    handler: bin/world
    events:
      - http:
          path: world
          method: get
```

- What stacktrace or error message from your provider did you see?
No error messages

Similar or dependent issues:

## Additional Data

- **_Serverless Framework Version you're using_**: 1.50.0
- **_Operating System_**: Linux Mint 19.1
- **_Stack Trace_**: None
- **_Provider Error messages_**: None



Discussion:

Resolving patch:
diff --git a/lib/classes/Service.js b/lib/classes/Service.js
index afd489d1a3f..24b107cc37d 100644
--- a/lib/classes/Service.js
+++ b/lib/classes/Service.js
@@ -21,7 +21,7 @@ class Service {
     this.serviceObject = null;
     this.provider = {
       stage: 'dev',
-      variableSyntax: '\\${([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}',
+      variableSyntax: '\\${([^{}]+?)}',
     };
     this.custom = {};
     this.plugins = [];
diff --git a/lib/plugins/print/print.js b/lib/plugins/print/print.js
index 1c71ae9bebd..1057744fb93 100644
--- a/lib/plugins/print/print.js
+++ b/lib/plugins/print/print.js
@@ -54,7 +54,7 @@ class Print {
       {
         stage: 'dev',
         region: 'us-east-1',
-        variableSyntax: '\\${([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}',
+        variableSyntax: '\\${([^{}]+?)}',
       },
       service.provider
     );


Test patch:
diff --git a/lib/classes/Service.test.js b/lib/classes/Service.test.js
index 88bf11f4331..3f8048af909 100644
--- a/lib/classes/Service.test.js
+++ b/lib/classes/Service.test.js
@@ -31,7 +31,7 @@ describe('Service', () => {
       expect(serviceInstance.serviceObject).to.be.equal(null);
       expect(serviceInstance.provider).to.deep.equal({
         stage: 'dev',
-        variableSyntax: '\\${([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}',
+        variableSyntax: '\\${([^{}]+?)}',
       });
       expect(serviceInstance.custom).to.deep.equal({});
       expect(serviceInstance.plugins).to.deep.equal([]);
diff --git a/lib/classes/Variables.test.js b/lib/classes/Variables.test.js
index 73d7bef1a9a..c58e4118b03 100644
--- a/lib/classes/Variables.test.js
+++ b/lib/classes/Variables.test.js
@@ -144,13 +144,26 @@ describe('Variables', () => {
   });
 
   describe('fallback', () => {
-    it('should fallback if ${self} syntax fail to populate but fallback is provided', () => {
-      serverless.variables.service.custom = {
-        settings: '${self:nonExistent, "fallback"}',
-      };
-      return serverless.variables.populateService().should.be.fulfilled.then(result => {
-        expect(result.custom).to.be.deep.eql({
-          settings: 'fallback',
+    describe('should fallback if ${self} syntax fail to populate but fallback is provided', () => {
+      [
+        { value: 'fallback123_*', description: 'regular ASCII characters' },
+        { value: 'hello+world^*$@(!', description: 'different ASCII characters' },
+        { value: '+++++', description: 'plus sign' },
+        { value: 'システム管理者*', description: 'japanese characters' },
+        { value: 'deす', description: 'mixed japanese ending' },
+        { value: 'でsu', description: 'mixed japanese leading' },
+        { value: 'suごi', description: 'mixed japanese middle' },
+        { value: '①⑴⒈⒜Ⓐⓐⓟ ..▉가Ὠ', description: 'random unicode' },
+      ].forEach(testCase => {
+        it(testCase.description, () => {
+          serverless.variables.service.custom = {
+            settings: `\${self:nonExistent, "${testCase.value}"}`,
+          };
+          return serverless.variables.populateService().should.be.fulfilled.then(result => {
+            expect(result.custom).to.be.deep.eql({
+              settings: testCase.value,
+            });
+          });
         });
       });
     });
diff --git a/lib/plugins/print/print.test.js b/lib/plugins/print/print.test.js
index c417790a725..917760b6ae0 100644
--- a/lib/plugins/print/print.test.js
+++ b/lib/plugins/print/print.test.js
@@ -37,10 +37,6 @@ describe('Print', () => {
     };
   });
 
-  afterEach(() => {
-    serverless.service.provider.variableSyntax = '\\${([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}';
-  });
-
   it('should print standard config', () => {
     const conf = {
       service: 'my-service',
@@ -361,4 +357,41 @@ describe('Print', () => {
       expect(YAML.load(message)).to.eql(expected);
     });
   });
+
+  describe('should resolve fallback', () => {
+    [
+      { value: 'hello_123@~:/+', description: 'ascii chars' },
+      { value: '①⑴⒈⒜Ⓐⓐⓟ ..▉가Ὠ', description: 'unicode chars' },
+    ].forEach(testCase => {
+      it(testCase.description, () => {
+        const conf = {
+          custom: {
+            me: `\${self:none, '${testCase.value}'}`,
+          },
+          provider: {},
+        };
+        getServerlessConfigFileStub.resolves(conf);
+
+        serverless.processedInput = {
+          commands: ['print'],
+          options: {},
+        };
+
+        const expected = {
+          custom: {
+            me: testCase.value,
+          },
+          provider: {},
+        };
+
+        return print.print().then(() => {
+          const message = print.serverless.cli.consoleLog.args.join();
+
+          expect(getServerlessConfigFileStub.calledOnce).to.equal(true);
+          expect(print.serverless.cli.consoleLog.called).to.be.equal(true);
+          expect(YAML.load(message)).to.eql(expected);
+        });
+      });
+    });
+  });
 });