From 06ccdaccde4b16978d640f879e6787f20944af16 Mon Sep 17 00:00:00 2001
From: Dominik Seeger <dominik.seeger@gmx.net>
Date: Fri, 4 Jan 2019 17:07:20 +0100
Subject: [PATCH] added tests for export mixin

added more tests for exportMixin
---
 .../src/components/export/InstanceExport.vue  |   2 +-
 frontend/src/components/mixins/mixins.ts      |   8 +-
 .../tests/unit/components/AutoLogout.spec.ts  |  12 +-
 .../tests/unit/mixins/exportMixin.spec.ts     | 153 ++++++++++++++++++
 frontend/tests/utils/testUtils.ts             |  40 +++++
 5 files changed, 204 insertions(+), 11 deletions(-)
 create mode 100644 frontend/tests/unit/mixins/exportMixin.spec.ts
 create mode 100644 frontend/tests/utils/testUtils.ts

diff --git a/frontend/src/components/export/InstanceExport.vue b/frontend/src/components/export/InstanceExport.vue
index 681e0e6b..844a0ab4 100644
--- a/frontend/src/components/export/InstanceExport.vue
+++ b/frontend/src/components/export/InstanceExport.vue
@@ -42,7 +42,7 @@ import { exportMixin, ExportType } from '@/components/mixins/mixins'
 export default class DataExport extends exportMixin {
   exportDialog = true
   mapFile: File | null = null
-  exportType = ExportType.JSON     // instance export is only available as JSON
+  exportType = ExportType.JSON // instance export is only available as JSON
 
   get studentMap () { return getters.state.studentMap }
 
diff --git a/frontend/src/components/mixins/mixins.ts b/frontend/src/components/mixins/mixins.ts
index 972371d0..bee6fc96 100644
--- a/frontend/src/components/mixins/mixins.ts
+++ b/frontend/src/components/mixins/mixins.ts
@@ -45,7 +45,7 @@ export class exportMixin extends Vue {
     } else {
       throw new Error('Unsupported export type')
     }
-    
+
     if (this.mapFile || this.mapFileLoaded) {
       this.getMappedExportFile(studentData)
     } else {
@@ -85,7 +85,7 @@ export class exportMixin extends Vue {
     }, '') + '\n' // add trailing newline
   }
 
-  optionalConvertAndCreatePopup(studentData: StudentExportItem[] | InstanceExportData) {
+  optionalConvertAndCreatePopup (studentData: StudentExportItem[] | InstanceExportData) {
     const convertedData = this.exportType === ExportType.CSV
       ? this.jsonToCSV(studentData as StudentExportItem[]) : studentData
       // we have a cast here because only student export may be converted to csv
@@ -136,6 +136,6 @@ export class exportMixin extends Vue {
     this.exportDialog = true
   }
 
-  applyMapping (exportData: StudentExportItem[] | InstanceExportData): void { throw new Error("Not implemented.") }
-  createDownloadPopup (content: string | StudentExportItem[] | InstanceExportData, fileType: ExportType): void { throw new Error("Not implemented.") }
+  applyMapping (exportData: StudentExportItem[] | InstanceExportData): void { throw new Error('Not implemented.') }
+  createDownloadPopup (content: string | StudentExportItem[] | InstanceExportData, fileType: ExportType): void { throw new Error('Not implemented.') }
 }
diff --git a/frontend/tests/unit/components/AutoLogout.spec.ts b/frontend/tests/unit/components/AutoLogout.spec.ts
index 5a5730f6..57501f45 100644
--- a/frontend/tests/unit/components/AutoLogout.spec.ts
+++ b/frontend/tests/unit/components/AutoLogout.spec.ts
@@ -15,15 +15,15 @@ describe('Auto Logout Unit Tests', () => {
   let store: any = null
   let consoleTemp = {
     warn: console.warn,
-    error: console.error,
+    error: console.error
   }
 
-  before(function() {
-    console.warn = function() {}
-    console.error = function() {}
+  before(function () {
+    console.warn = function () {}
+    console.error = function () {}
   })
 
-  after(function() {
+  after(function () {
     console.warn = consoleTemp.warn
     console.error = consoleTemp.error
   })
@@ -37,7 +37,7 @@ describe('Auto Logout Unit Tests', () => {
   afterEach(() => {
     sinon.restore()
   })
-  
+
   it('should be hidden by default', () => {
     let wrapper = mount(AutoLogout, { localVue: localVue, store })
     wrapper.vm.$data.logoutDialog.should.equal(false)
diff --git a/frontend/tests/unit/mixins/exportMixin.spec.ts b/frontend/tests/unit/mixins/exportMixin.spec.ts
new file mode 100644
index 00000000..a6c6cdd0
--- /dev/null
+++ b/frontend/tests/unit/mixins/exportMixin.spec.ts
@@ -0,0 +1,153 @@
+import { exportMixin, ExportType } from '@/components/mixins/mixins'
+import sinon from 'sinon'
+import ax from '@/api'
+import testUtils, { FakeFileReader } from '@/../tests/utils/testUtils'
+import chai from 'chai'
+import { mutations as mut } from '@/store/mutations'
+chai.should()
+
+describe('Export Mixin Unit Tests', () => {
+  describe('getExportFile()', () => {
+    let e = new exportMixin()
+
+    afterEach(() => {
+      sinon.restore()
+    })
+
+    it('should fetch from the dataExport endpoint when called with "data"', async () => {
+      let stub = sinon.stub().resolves({ data: testUtils.studentExports })
+      sinon.replace(ax, 'post', stub)
+      sinon.replace(e, 'optionalConvertAndCreatePopup', () => {})
+      await e.getExportFile('data')
+      stub.calledWith('/api/export/json/').should.equal(true)
+    })
+    it('should fetch from instanceExport endpoint when called with "instance"', async () => {
+      let stub = sinon.stub().resolves({ data: testUtils.instanceExports })
+      sinon.replace(ax, 'get', stub)
+      sinon.replace(e, 'optionalConvertAndCreatePopup', () => {})
+      await e.getExportFile('instance')
+      stub.calledWith('/api/instance/export').should.equal(true)
+    })
+    it('should try to apply mapping when map file is given', async () => {
+      let spy = sinon.spy()
+      e.mapFile = testUtils.fakeFile
+      sinon.replace(ax, 'post', sinon.stub().resolves({ data: testUtils.studentExports }))
+      sinon.replace(e, 'getMappedExportFile', spy)
+      await e.getExportFile('data')
+      e.mapFile = null
+      spy.calledOnce.should.equal(true)
+      spy.calledWith(testUtils.studentExports).should.equal(true)
+    })
+    it('should call popup creation method with proper arguments when no mapping file is given', async () => {
+      let spy = sinon.spy()
+      sinon.replace(ax, 'post', sinon.stub().resolves({ data: testUtils.studentExports }))
+      sinon.replace(e, 'optionalConvertAndCreatePopup', spy)
+      await e.getExportFile('data')
+      spy.calledOnce.should.equal(true)
+      spy.calledWith(testUtils.studentExports).should.equal(true)
+    })
+  })
+  describe('optionalConvertAndCreatePopup()', () => {
+    let e = new exportMixin()
+
+    afterEach(() => {
+      sinon.restore()
+    })
+
+    it('should convert data to csv when csv is selected in dropdown', () => {
+      let stub = sinon.stub().returns('students as csv')
+      let spy = sinon.spy()
+      e.exportType = ExportType.CSV
+      sinon.replace(e, 'jsonToCSV', stub)
+      sinon.replace(e, 'createDownloadPopup', spy)
+      e.optionalConvertAndCreatePopup(testUtils.studentExports)
+      stub.calledWith(testUtils.studentExports).should.equal(true)
+      spy.calledWith('students as csv', ExportType.CSV).should.equal(true)
+    })
+  })
+  describe('readMapFileAndCommit', () => {
+    let e = new exportMixin()
+    // @ts-ignore
+    global.FileReader = FakeFileReader
+
+    afterEach(() => {
+      sinon.restore()
+    })
+
+    it('should throw an error when no mapfile was given', () => {
+      e.readMapFileAndCommit().catch((res) => {
+        res.should.not.equal(undefined)
+      })
+    })
+    it('should read selected mapFile', async () => {
+      e.mapFile = testUtils.fakeFile
+      let stub = sinon.stub()
+      // @ts-ignore
+      sinon.replace(global.FileReader.prototype, 'readAsText', stub)
+      e.readMapFileAndCommit()
+      e.mapFile = null
+      stub.args[0][0].name.should.equal(testUtils.fakeFile.name)
+    })
+    it('should parse file and trigger mutation when file was read', async () => {
+      e.mapFile = testUtils.fakeFile
+      let stub = sinon.stub().returns(testUtils.studentMap)
+      let mutSpy = sinon.spy()
+      sinon.replace(e, 'parseCSVMap', stub)
+      sinon.replace(mut, 'SET_STUDENT_MAP', mutSpy)
+      await e.readMapFileAndCommit()
+      e.mapFile = null
+      stub.calledOnce.should.equal(true)
+      stub.calledWithExactly('Grady testUtils fake result').should.equal(true)
+      mutSpy.calledWithExactly(testUtils.studentMap).should.equal(true)
+    })
+  })
+  describe('getMappedExportFile', () => {
+    let e = new exportMixin
+
+    afterEach(() => {
+      sinon.restore()
+    })
+
+    it('should read mapFile if it is not already loaded when used with instanceExports then apply mapping and create popup', async () => {
+      e.mapFile = testUtils.fakeFile
+      let spy = sinon.spy()
+      sinon.replace(e, 'readMapFileAndCommit', spy)
+      sinon.replace(e, 'applyMapping', () => {})
+      sinon.replace(e, 'optionalConvertAndCreatePopup', () => {})
+      await e.getMappedExportFile(testUtils.instanceExports)
+      e.mapFile = null
+      spy.called.should.equal(true)
+    })
+    it('should read mapFile if it is not already loaded when used with dataExport then apply mapping and create popup', async () => {
+      e.mapFile = testUtils.fakeFile
+      let spy = sinon.spy()
+      sinon.replace(e, 'readMapFileAndCommit', spy)
+      sinon.replace(e, 'applyMapping', () => {})
+      sinon.replace(e, 'optionalConvertAndCreatePopup', () => {})
+      await e.getMappedExportFile(testUtils.studentExports)
+      e.mapFile = null
+      spy.called.should.equal(true)
+    })
+    it('should apply the mapping and then open popup when mapFile is loaded', async () => {
+      sinon.replaceGetter(e, 'mapFileLoaded', () => { return true })
+      let mappingSpy = sinon.spy()
+      let popupSpy = sinon.spy()
+      sinon.replace(e, 'applyMapping', mappingSpy)
+      sinon.replace(e, 'optionalConvertAndCreatePopup', popupSpy)
+      await e.getMappedExportFile(testUtils.instanceExports)
+      e.mapFile = null
+      mappingSpy.calledWithExactly(testUtils.instanceExports).should.equal(true)
+      popupSpy.calledWithExactly(testUtils.instanceExports).should.equal(true)
+    })
+  })
+  describe('jsonToCSV()', () => {
+    let e = new exportMixin
+
+    it('should correctly parse JSON input to CSV with ; delimiter', () => {
+      let csv = e.jsonToCSV(testUtils.studentExports)
+      let expected =  'Matrikel;Name;Username;Sum;Exam;Password;test01\n' + 
+      '1000000;name;username;100;exam;pwd;100\n'
+      csv.should.equal(expected)
+    })
+  })
+})
diff --git a/frontend/tests/utils/testUtils.ts b/frontend/tests/utils/testUtils.ts
new file mode 100644
index 00000000..599cb750
--- /dev/null
+++ b/frontend/tests/utils/testUtils.ts
@@ -0,0 +1,40 @@
+import { StudentExportItem, InstanceExportData } from '@/api'
+
+export class FakeFileReader {
+  result: String = 'Grady testUtils fake result'
+
+  constructor () {}
+  // readAsText will execute the callback that was provided to onload
+  readAsText (file: File) { this.onload({ target: this }) }
+  onload: Function = () => { }
+}
+
+export default {
+  studentExports: <StudentExportItem[]> [{
+    Matrikel: '1000000',
+    Name: 'name',
+    Username: 'username',
+    Sum: 100,
+    Exam: 'exam',
+    Password: 'pwd',
+    Scores: [{ type: 'test01', score: 100 }]
+  }],
+  instanceExports: <InstanceExportData> {
+    students: [{
+      name: 'test',
+      matrikelNo: '1000000'
+    }]
+  },
+  fakeFile: <File> {
+    lastModified: 1,
+    name: 'Grady testUtils fake file',
+    size: 1,
+    slice: (start?: number | undefined, end?: number | undefined, contentType?: string | undefined): Blob => { return new Blob() },
+    type: 'fake file'
+  },
+  studentMap: {
+    '1000000': {
+      matrikelNo: '1000000', name: 'test'
+    }
+  }
+}
-- 
GitLab