From f5269cb7bd438a3f418c89b3baa58f4864597b12 Mon Sep 17 00:00:00 2001 From: Jannik Stehle Date: Wed, 29 Mar 2023 16:36:05 +0200 Subject: [PATCH] Implement GDPR export for users --- changelog/unreleased/enhancement-gdpr-export | 6 + .../src/components/Account/GdprExport.vue | 134 ++++++++++++++++++ packages/web-runtime/src/pages/account.vue | 16 ++- .../components/Account/GdprExport.spec.ts | 93 ++++++++++++ .../tests/unit/pages/account.spec.ts | 15 +- 5 files changed, 261 insertions(+), 3 deletions(-) create mode 100644 changelog/unreleased/enhancement-gdpr-export create mode 100644 packages/web-runtime/src/components/Account/GdprExport.vue create mode 100644 packages/web-runtime/tests/unit/components/Account/GdprExport.spec.ts diff --git a/changelog/unreleased/enhancement-gdpr-export b/changelog/unreleased/enhancement-gdpr-export new file mode 100644 index 00000000000..cc90bc9593c --- /dev/null +++ b/changelog/unreleased/enhancement-gdpr-export @@ -0,0 +1,6 @@ +Enhancement: GDPR export + +Users can now request a GDPR export on their account page. Note that this is only supported when running oCIS as backend. + +https://github.com/owncloud/web/issues/8738 +https://github.com/owncloud/web/pull/8741 diff --git a/packages/web-runtime/src/components/Account/GdprExport.vue b/packages/web-runtime/src/components/Account/GdprExport.vue new file mode 100644 index 00000000000..9f51272e6e5 --- /dev/null +++ b/packages/web-runtime/src/components/Account/GdprExport.vue @@ -0,0 +1,134 @@ + + + diff --git a/packages/web-runtime/src/pages/account.vue b/packages/web-runtime/src/pages/account.vue index c35cdbd349a..341a5a54d71 100644 --- a/packages/web-runtime/src/pages/account.vue +++ b/packages/web-runtime/src/pages/account.vue @@ -94,6 +94,12 @@ +
+
+
+ +
+
@@ -113,22 +119,27 @@ import axios from 'axios' import { v4 as uuidV4 } from 'uuid' import { useGettext } from 'vue3-gettext' import { setCurrentLanguage } from 'web-runtime/src/helpers/language' -import { configurationManager } from 'web-pkg/src/configuration' +import GdprExport from 'web-runtime/src/components/Account/GdprExport.vue' +import { useConfigurationManager } from 'web-pkg/src/composables/configuration' export default defineComponent({ name: 'Personal', components: { - EditPasswordModal + EditPasswordModal, + GdprExport }, setup() { const store = useStore() const accessToken = useAccessToken({ store }) const language = useGettext() const clientService = useClientService() + const configurationManager = useConfigurationManager() // FIXME: Use graph capability when we have it const isLanguageSupported = useCapabilitySpacesEnabled() const isChangePasswordEnabled = useCapabilitySpacesEnabled() + // TODO: use correct capability + const isGdprExportEnabled = true const user = computed(() => { return store.getters.user }) @@ -235,6 +246,7 @@ export default defineComponent({ updateSelectedLanguage, accountEditLink, isChangePasswordEnabled, + isGdprExportEnabled, isLanguageSupported, groupNames, user, diff --git a/packages/web-runtime/tests/unit/components/Account/GdprExport.spec.ts b/packages/web-runtime/tests/unit/components/Account/GdprExport.spec.ts new file mode 100644 index 00000000000..3298eef48e7 --- /dev/null +++ b/packages/web-runtime/tests/unit/components/Account/GdprExport.spec.ts @@ -0,0 +1,93 @@ +import GdprExport from 'web-runtime/src/components/Account/GdprExport.vue' +import { + createStore, + defaultComponentMocks, + defaultPlugins, + shallowMount, + defaultStoreMockOptions +} from 'web-test-helpers' +import { mock, mockDeep } from 'jest-mock-extended' +import { ClientService } from 'web-pkg' +import { Resource } from 'web-client/src' + +const selectors = { + ocSpinnerStub: 'oc-spinner-stub', + requestGdprExportBtn: '[data-testid="request-gdpr-export-btn"]', + downloadGdprExportBtn: '[data-testid="download-gdpr-export-btn"]', + gdprExportInProgress: '[data-testid="gdpr-export-in-process"]' +} + +const downloadFile = jest.fn() +jest.mock('web-pkg/src/composables/download', () => ({ + useDownloadFile: jest.fn(() => ({ downloadFile })) +})) + +describe('GdprExport component', () => { + it('shows the loading spinner initially', () => { + const { wrapper } = getWrapper() + expect(wrapper.find(selectors.ocSpinnerStub).exists()).toBeTruthy() + }) + describe('request button', () => { + it('shows if no gdpr export exists', async () => { + const clientService = mockDeep() + clientService.webdav.getFileInfo.mockRejectedValue({ statusCode: 404 }) + const { wrapper } = getWrapper(clientService) + await wrapper.vm.loadExportTask.last + expect(wrapper.find(selectors.requestGdprExportBtn).exists()).toBeTruthy() + }) + it('does not show when an export is being processed', async () => { + const clientService = mockDeep() + clientService.webdav.getFileInfo.mockRejectedValue({ statusCode: 425 }) + const { wrapper } = getWrapper(clientService) + await wrapper.vm.loadExportTask.last + expect(wrapper.find(selectors.requestGdprExportBtn).exists()).toBeFalsy() + }) + it.todo('triggers the export when being clicked') + }) + describe('download button', () => { + it('shows if a gdpr export exists', async () => { + const { wrapper } = getWrapper() + await wrapper.vm.loadExportTask.last + expect(wrapper.find(selectors.downloadGdprExportBtn).exists()).toBeTruthy() + }) + it('does not show if no export exists', async () => { + const clientService = mockDeep() + clientService.webdav.getFileInfo.mockRejectedValue({}) + const { wrapper } = getWrapper(clientService) + await wrapper.vm.loadExportTask.last + expect(wrapper.find(selectors.downloadGdprExportBtn).exists()).toBeFalsy() + }) + it('triggers the download when being clicked', async () => { + const { wrapper } = getWrapper() + await wrapper.vm.loadExportTask.last + await wrapper.find(selectors.downloadGdprExportBtn).trigger('click') + expect(downloadFile).toHaveBeenCalled() + }) + }) + it('shows a "in progress"-hint', async () => { + const clientService = mockDeep() + clientService.webdav.getFileInfo.mockRejectedValue({ statusCode: 425 }) + const { wrapper } = getWrapper(clientService) + await wrapper.vm.loadExportTask.last + expect(wrapper.find(selectors.gdprExportInProgress).exists()).toBeTruthy() + }) +}) + +function getWrapper(clientService = undefined) { + if (!clientService) { + clientService = mockDeep() + clientService.webdav.getFileInfo.mockResolvedValue(mock()) + } + const mocks = defaultComponentMocks() + mocks.$clientService = clientService + const store = createStore(defaultStoreMockOptions) + return { + mocks, + wrapper: shallowMount(GdprExport, { + global: { + mocks, + plugins: [...defaultPlugins(), store] + } + }) + } +} diff --git a/packages/web-runtime/tests/unit/pages/account.spec.ts b/packages/web-runtime/tests/unit/pages/account.spec.ts index 948ca3b12cb..407ee396005 100644 --- a/packages/web-runtime/tests/unit/pages/account.spec.ts +++ b/packages/web-runtime/tests/unit/pages/account.spec.ts @@ -20,7 +20,8 @@ const selectors = { editUrlButton: '[data-testid="account-page-edit-url-btn"]', accountPageInfo: '.account-page-info', groupNames: '[data-testid="group-names"]', - groupNamesEmpty: '[data-testid="group-names-empty"]' + groupNamesEmpty: '[data-testid="group-names-empty"]', + gdprExport: '[data-testid="gdpr-export"]' } jest.mock('web-pkg/src/configuration', () => ({ @@ -88,6 +89,18 @@ describe('account page', () => { }) }) + // TODO: un-skip if capabilities are implemented + describe.skip('gdpr export section', () => { + it('does not show if not announced via capabilities', () => { + const { wrapper } = getWrapper() + expect(wrapper.find(selectors.gdprExport).exists()).toBeFalsy() + }) + it('does show if announced via capabilities', () => { + const { wrapper } = getWrapper({ capabilities: {} }) + expect(wrapper.find(selectors.gdprExport).exists()).toBeTruthy() + }) + }) + describe('method "editPassword"', () => { it('should show message on success', async () => { const { wrapper, mocks } = getWrapper()