Repository: saucelabs/node-saucelabs
Pull request #100 (2020-12-04 12:50:47)

uploadJobAssets needs a file instead of a buffer
The current `uploadJobAssets ` requests a file which results in storing the buffer into a file and then upload it to Sauce. Can we change this to a buffer and let the module handle this?

christian-bromann: I would say users should be free to either pass in an array of strings (for file names) or a string buffer. We can make the check in the library and handle it accordingly.

Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 3f7b12e..7f69244 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -102,18 +102,7 @@ git commit -s -m "adding X to change Y"
 
 ## Release Project
 
-Contributor with release rights can release the project by calling one of the following commands:
-
-```sh
-# for patch releases
-$ npm run release:patch
-# for minor releases
-$ npm run release:minor
-# for major releases
-$ npm run release:major
-```
-
-Ensure you release by following the [semantic versioning](https://semver.org/) principle.
+Contributor with push access to this repo can at any time make a release. To do so, just trigger the [GitHub Action](https://github.com/saucelabs/node-saucelabs/actions?query=workflow%3A%22Manual+NPM+Publish%22) that releases the package. Ensure you pick the correct release type by following the [semantic versioning](https://semver.org/) principle.
 
 ---
 
diff --git a/README.md b/README.md
index 9340d13..f917fc8 100644
--- a/README.md
+++ b/README.md
@@ -205,7 +205,17 @@ import SauceLabs from 'saucelabs';
     // upload additional log files and attach it to your Sauce job
     await myAccount.uploadJobAssets(
         '76e693dbe6ff4910abb0bc3d752a971e',
-        ['video.mp4', 'log.json']
+        [
+            // either pass in file names
+            './logs/video.mp4', './logs/log.json',
+            // or file objects
+            {
+                filename: 'myCustomLogFile.json',
+                data: {
+                    someLog: 'data'
+                }
+            }
+        ]
     )
 })()
 ```
diff --git a/src/index.js b/src/index.js
index 604e452..a661d06 100644
--- a/src/index.js
+++ b/src/index.js
@@ -358,12 +358,18 @@ export default class SauceLabs {
         const uri = getAPIHost(servers, basePath, this._options) + endpoint.replace('{jobId}', jobId)
         const body = new FormData()
 
-        for (const filePath of files) {
-            const readStream = fs.createReadStream(filePath.startsWith('/')
-                ? filePath
-                : path.join(process.cwd(), filePath)
-            )
-            body.append('file[]', readStream)
+        for (const file of files) {
+            if (typeof file === 'string') {
+                const readStream = fs.createReadStream(file.startsWith('/')
+                    ? file
+                    : path.join(process.cwd(), file)
+                )
+                body.append('file[]', readStream)
+            } else if (file && typeof file.filename === 'string') {
+                body.append('file[]', Buffer.from(JSON.stringify(file.data)), file.filename)
+            } else {
+                throw new Error('Invalid file parameter! Expected either a file path or a file object containing "filename" and "data" property.')
+            }
         }
 
         try {


Test patch:
diff --git a/apis/testrunner.json b/apis/testrunner.json
index 8bb3669..ee010f6 100644
--- a/apis/testrunner.json
+++ b/apis/testrunner.json
@@ -57,7 +57,7 @@
       }
     },
     "files": {
-      "description": "s path to upload and attach to your job",
+      "description": "asset to upload and attach to your job",
       "in": "path",
       "name": "files",
       "required": false,
diff --git a/tests/index.test.js b/tests/index.test.js
index 4b9933b..b1d368a 100644
--- a/tests/index.test.js
+++ b/tests/index.test.js
@@ -294,12 +294,20 @@ test('should allow to upload files', async () => {
         body: JSON.stringify(body)
     }))
     const result = await api.uploadJobAssets('some-id', {
-        files: ['log.json', 'selenium-server.json']
+        files: [
+            'log.json',
+            'selenium-server.json',
+            {
+                filename: 'foobar.json',
+                data: { foo: 'bar' }
+            }
+        ]
     })
 
     const { instances } = new FormData()
-    expect(instances[0].append).toBeCalledTimes(2)
+    expect(instances[0].append).toBeCalledTimes(3)
     expect(instances[0].append).toBeCalledWith('file[]', {name: '/somefile', path: 'somepath'})
+    expect(instances[0].append).toBeCalledWith('file[]', Buffer.from(JSON.stringify({ foo: 'bar' })), 'foobar.json')
 
     const uri = got.mock.calls[0][0]
     expect(uri).toBe('https://api.us-west-1.saucelabs.com/v1/testrunner/jobs/some-id/assets')
@@ -331,6 +339,15 @@ test('should not even try to upload if no files were selected', async () => {
     expect(result.message).toBe('No files to upload selected')
 })
 
+test('should fail if file parameter is invalid', async () => {
+    const api = new SauceLabs({ user: 'foo', key: 'bar' })
+    const result = await api.uploadJobAssets('some-id', {
+        files: [{ foo: 'bar' }]
+    }).catch((err) => err)
+
+    expect(result.message).toContain('Invalid file parameter')
+})
+
 test('should contain custom headers', async () => {
     const api = new SauceLabs({ user: 'foo', key: 'bar', headers: { 'user-agent': 'foo' } })
     got.mockReturnValue(Promise.resolve({