Repository: nylas/nylas-python

Issue:
File.filename doesn't apply when type is stream:
Setting a filename on an attachment only works when using `.data`. When `.stream` is used, it falls back to the name of the file on the uploading system.

```
        myfile = self.nylas_client.files.create()
        myfile.content_type = 'application/pdf'
        myfile.filename = attachment_name
        with open(attachment_path, 'rb') as f:
            myfile.stream = f
            myfile.save()

        myfile.filename = attachment_name
        # Create a new draft
        draft = self.nylas_client.drafts.create()
        if type(recipients) == str:
            recipients = [recipients]
        draft.to = [{'email': recipient} for recipient in recipients]
        draft.subject = subject
        draft.body = message
        draft.attach(myfile)
```

Discussion:

Patch:
diff --git a/nylas/client/restful_models.py b/nylas/client/restful_models.py
index d8d1c98..7f31653 100644
--- a/nylas/client/restful_models.py
+++ b/nylas/client/restful_models.py
@@ -387,18 +387,27 @@ class File(NylasAPIObject):
     collection_name = 'files'
 
     def save(self):  # pylint: disable=arguments-differ
-        if hasattr(self, 'stream') and self.stream is not None:
-            data = {self.filename: self.stream}
-        elif hasattr(self, 'data') and self.data is not None:
-            data = {self.filename: StringIO(self.data)}
-        else:
+        stream = getattr(self, "stream", None)
+        if not stream:
+            data = getattr(self, "data", None)
+            if data:
+                stream = StringIO(data)
+
+        if not stream:
             message = (
                 "File object not properly formatted, "
                 "must provide either a stream or data."
             )
             raise FileUploadError(message=message)
 
-        new_obj = self.api._create_resources(File, data)
+        file_info = (
+            self.filename,
+            stream,
+            self.content_type,
+            {}, # upload headers
+        )
+
+        new_obj = self.api._create_resources(File, {"file": file_info})
         new_obj = new_obj[0]
         for attr in self.attrs:
             if hasattr(new_obj, attr):


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index f8fb9ad..4e8eb72 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,6 +1,7 @@
 import re
 import json
 import copy
+import cgi
 import random
 import string
 import pytest
@@ -674,24 +675,59 @@ def mock_draft_sent_response(mocked_responses, api_url):
 
 
 @pytest.fixture
-def mock_files(mocked_responses, api_url):
-    body = [{
-        "content_type": "text/plain",
-        "filename": "a.txt",
-        "id": "3qfe4k3siosfjtjpfdnon8zbn",
-        "account_id": "6aakaxzi4j5gn6f7kbb9e0fxs",
-        "object": "file",
-        "size": 762878
-    }]
+def mock_files(mocked_responses, api_url, account_id):
+    files_content = {
+        "3qfe4k3siosfjtjpfdnon8zbn": b"Hello, World!",
+    }
+    files_metadata = {
+        "3qfe4k3siosfjtjpfdnon8zbn": {
+            "id": "3qfe4k3siosfjtjpfdnon8zbn",
+            "content_type": "text/plain",
+            "filename": "hello.txt",
+            "account_id": account_id,
+            "object": "file",
+            "size": len(files_content["3qfe4k3siosfjtjpfdnon8zbn"])
+        }
+    }
     mocked_responses.add(
+        responses.GET,
+        api_url + '/files',
+        body=json.dumps(list(files_metadata.values())),
+    )
+    for file_id in files_content:
+        mocked_responses.add(
+            responses.POST,
+            "{base}/files/{file_id}".format(base=api_url, file_id=file_id),
+            body=json.dumps(files_metadata[file_id]),
+        )
+        mocked_responses.add(
+            responses.GET,
+            "{base}/files/{file_id}/download".format(base=api_url, file_id=file_id),
+            body=files_content[file_id],
+        )
+
+    def create_callback(request):
+        uploaded_lines = request.body.decode('utf8').splitlines()
+        content_disposition = uploaded_lines[1]
+        _, params = cgi.parse_header(content_disposition)
+        filename = params.get("filename", None)
+        content = "".join(uploaded_lines[3:-1])
+        size = len(content.encode('utf8'))
+
+        body = [{
+            "id": generate_id(),
+            "content_type": "text/plain",
+            "filename": filename,
+            "account_id": account_id,
+            "object": "file",
+            "size": size,
+        }]
+        return (200, {}, json.dumps(body))
+
+    mocked_responses.add_callback(
         responses.POST,
         api_url + '/files/',
-        body=json.dumps(body),
-    )
-    mocked_responses.add(
-        responses.GET,
-        api_url + '/files/3qfe4k3siosfjtjpfdnon8zbn/download',
-        body='test body',
+        callback=create_callback,
     )
 
 
diff --git a/tests/test_files.py b/tests/test_files.py
index 046c274..8da27fa 100644
--- a/tests/test_files.py
+++ b/tests/test_files.py
@@ -1,19 +1,65 @@
+import cgi
+from io import BytesIO
 import pytest
 from nylas.client.errors import FileUploadError
 
 
 @pytest.mark.usefixtures("mock_files")
-def test_file_upload(api_client):
+def test_file_upload_data(api_client, mocked_responses):
+    data = "Hello, World!"
+
     myfile = api_client.files.create()
-    myfile.filename = 'test.txt'
-    myfile.data = "Hello World."
+    myfile.filename = 'hello.txt'
+    myfile.data = data
+
+    assert not mocked_responses.calls
     myfile.save()
+    assert len(mocked_responses.calls) == 1
+
+    assert myfile.filename == 'hello.txt'
+    assert myfile.size == 13
 
-    assert myfile.filename == 'a.txt'
-    assert myfile.size == 762878
+    upload_body = mocked_responses.calls[0].request.body
+    upload_lines = upload_body.decode("utf8").splitlines()
+
+    content_disposition = upload_lines[1]
+    _, params = cgi.parse_header(content_disposition)
+    assert params["filename"] == "hello.txt"
+    assert "Hello, World!" in upload_lines
+
+
+@pytest.mark.usefixtures("mock_files")
+def test_file_upload_stream(api_client, mocked_responses):
+    stream = BytesIO(b"Hello, World!")
+    stream.name = "wacky.txt"
 
+    myfile = api_client.files.create()
+    myfile.filename = 'hello.txt'
+    myfile.stream = stream
+    assert not mocked_responses.calls
+    myfile.save()
+    assert len(mocked_responses.calls) == 1
+
+    assert myfile.filename == 'hello.txt'
+    assert myfile.size == 13
+
+    upload_body = mocked_responses.calls[0].request.body
+    upload_lines = upload_body.decode("utf8").splitlines()
+
+    content_disposition = upload_lines[1]
+    _, params = cgi.parse_header(content_disposition)
+    assert params["filename"] == "hello.txt"
+    assert "Hello, World!" in upload_lines
+
+
+@pytest.mark.usefixtures("mock_files")
+def test_file_download(api_client, mocked_responses):
+    assert not mocked_responses.calls
+    myfile = api_client.files.first()
+    assert len(mocked_responses.calls) == 1
     data = myfile.download().decode()
-    assert data == 'test body'
+    assert len(mocked_responses.calls) == 2
+    assert data == "Hello, World!"
 
 
 def test_file_invalid_upload(api_client):