Repository: shlinkio/shlink-web-client (ts)

Issue:
Import from file not working (Windows 10 Pro)
<!--
Before opening an issue, just take into account that this is a completely free of charge and open source project.
I'm always happy to help and provide support, but some understanding will be expected.
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personal if an issue gets eventually closed.
You may also be asked to provide tests or ways to reproduce reported bugs.
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.

With that said, please fill in the information requested next. More information might be requested once the issue is open.
-->

#### Shlink web client version

* Version: 3.2.0
* How do you use shlink-web-client: app.shlink.io

#### Summary

<!-- Provide a summary describing the problem you are experiencing. -->
If I try to import one / several servers from a file it gives the error:
"The servers could not be imported. Make sure the format is correct."

It's a file exported today so it's not outdated and formatted correctly.
On iMac importing from this file works.

#### Current behavior

<!-- How is it actually behaving (and it shouldn't)? -->
Importing form file is not working on Windows 10.

#### Expected behavior

<!-- How did you expected to behave? -->
It should work :)

#### How to reproduce

<!-- Provide steps to reproduce the bug. -->
Click "Add a server" on home page.
Click "Import from file".
Select the file on your pc.
The error occurs.

[shlink-servers.csv](https://github.com/shlinkio/shlink-web-client/files/7110468/shlink-servers.csv)

Pull request description:
Feature/import servers win
Closes #480 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2bd4a0e3..ee249394 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 
 ### Fixed
 * [#478](https://github.com/shlinkio/shlink-web-client/pull/478) Fixed tags including special chars not being properly URL encoded before using them as query params.
+* [#480](https://github.com/shlinkio/shlink-web-client/pull/480) Fixed servers import on Chromium-based browsers when using windows.
 
 
 ## [3.2.0] - 2021-07-12
diff --git a/src/servers/services/ServersImporter.ts b/src/servers/services/ServersImporter.ts
index 7dcf8175..28f43b65 100644
--- a/src/servers/services/ServersImporter.ts
+++ b/src/servers/services/ServersImporter.ts
@@ -1,29 +1,37 @@
 import { CsvJson } from 'csvjson';
 import { ServerData } from '../data';
 
-interface CsvFile extends File {
-  type: 'text/csv' | 'text/comma-separated-values' | 'application/csv';
-}
+const validateServer = (server: any): server is ServerData =>
+  typeof server.url === 'string' && typeof server.apiKey === 'string' && typeof server.name === 'string';
 
-const CSV_MIME_TYPES = [ 'text/csv', 'text/comma-separated-values', 'application/csv' ];
-const isCsv = (file?: File | null): file is CsvFile => !!file && CSV_MIME_TYPES.includes(file.type);
+const validateServers = (servers: any): servers is ServerData[] =>
+  Array.isArray(servers) && servers.every(validateServer);
 
 export default class ServersImporter {
-  public constructor(private readonly csvjson: CsvJson, private readonly fileReaderFactory: () => FileReader) {}
+  public constructor(private readonly csvJson: CsvJson, private readonly fileReaderFactory: () => FileReader) {}
 
   public readonly importServersFromFile = async (file?: File | null): Promise<ServerData[]> => {
-    if (!isCsv(file)) {
-      throw new Error('No file provided or file is not a CSV');
+    if (!file) {
+      throw new Error('No file provided');
     }
 
     const reader = this.fileReaderFactory();
 
-    return new Promise((resolve) => {
+    return new Promise((resolve, reject) => {
       reader.addEventListener('loadend', (e: ProgressEvent<FileReader>) => {
-        const content = e.target?.result?.toString() ?? '';
-        const servers = this.csvjson.toObject<ServerData>(content);
+        try {
+          // TODO Read as stream, otherwise, if the file is too big, this will block the browser tab
+          const content = e.target?.result?.toString() ?? '';
+          const servers = this.csvJson.toObject(content);
+
+          if (!validateServers(servers)) {
+            throw new Error('Provided file does not have the right format.');
+          }
 
-        resolve(servers);
+          resolve(servers);
+        } catch (e) {
+          reject(e);
+        }
       });
       reader.readAsText(file);
     });


Test patch:
diff --git a/test/servers/services/ServersImporter.test.ts b/test/servers/services/ServersImporter.test.ts
index 60e5dfc4..3f26ca96 100644
--- a/test/servers/services/ServersImporter.test.ts
+++ b/test/servers/services/ServersImporter.test.ts
@@ -21,23 +21,70 @@ describe('ServersImporter', () => {
   describe('importServersFromFile', () => {
     it('rejects with error if no file was provided', async () => {
       await expect(importer.importServersFromFile()).rejects.toEqual(
-        new Error('No file provided or file is not a CSV'),
+        new Error('No file provided'),
       );
     });
 
-    it('rejects with error if provided file is not a CSV', async () => {
+    it('rejects with error if parsing the file fails', async () => {
+      const expectedError = new Error('Error parsing file');
+
+      toObject.mockImplementation(() => {
+        throw expectedError;
+      });
+
+      await expect(importer.importServersFromFile(Mock.of<File>({ type: 'text/html' }))).rejects.toEqual(expectedError);
+    });
+
+    it.each([
+      [{}],
+      [ undefined ],
+      [[{ foo: 'bar' }]],
+      [
+        [
+          {
+            url: 1,
+            apiKey: 1,
+            name: 1,
+          },
+        ],
+      ],
+      [
+        [
+          {
+            url: 'foo',
+            apiKey: 'foo',
+            name: 'foo',
+          },
+          { bar: 'foo' },
+        ],
+      ],
+    ])('rejects with error if provided file does not parse to valid list of servers', async (parsedObject) => {
+      toObject.mockReturnValue(parsedObject);
+
       await expect(importer.importServersFromFile(Mock.of<File>({ type: 'text/html' }))).rejects.toEqual(
-        new Error('No file provided or file is not a CSV'),
+        new Error('Provided file does not have the right format.'),
       );
     });
 
-    it.each([
-      [ 'text/csv' ],
-      [ 'text/comma-separated-values' ],
-      [ 'application/csv' ],
-    ])('reads file when a CSV is provided', async (type) => {
-      await importer.importServersFromFile(Mock.of<File>({ type }));
+    it('reads file when a CSV containing valid servers is provided', async () => {
+      const expectedServers = [
+        {
+          url: 'foo',
+          apiKey: 'foo',
+          name: 'foo',
+        },
+        {
+          url: 'bar',
+          apiKey: 'bar',
+          name: 'bar',
+        },
+      ];
+
+      toObject.mockReturnValue(expectedServers);
+
+      const result = await importer.importServersFromFile(Mock.all<File>());
 
+      expect(result).toEqual(expectedServers);
       expect(readAsText).toHaveBeenCalledTimes(1);
       expect(toObject).toHaveBeenCalledTimes(1);
     });