diff --git a/.circleci/config.yml b/.circleci/config.yml index 574e5b5..23d3f7f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -29,7 +29,7 @@ jobs: edgedb-server-5 --background --data-dir ~/edgedb/data --runstate-dir ~/edgedb/runstate --log-to ~/edgedb/logs --security insecure_dev_mode edgedb instance link --host localhost --port 5656 --user edgedb --branch main --non-interactive --trust-tls-cert tweasel-platform - edgedb migration apply + edgedb migrate - cypress/run-tests: start-command: 'yarn build && yarn npm-run-all -p mock-analysis-runner start' cypress-command: "npx wait-on 'http://localhost:4321' && if [ -z ${CYPRESS_RECORD_KEY+x} ]; then yarn cypress run; else yarn cypress run --record; fi" diff --git a/cypress/e2e/use-cases/android.cy.ts b/cypress/e2e/use-cases/android.cy.ts index 29dd7c0..13d8d3e 100644 --- a/cypress/e2e/use-cases/android.cy.ts +++ b/cypress/e2e/use-cases/android.cy.ts @@ -65,7 +65,7 @@ describe('Use case: Analysing Android apps', () => { cy.contains('To Whom It May Concern:'); cy.get('#upload').selectFile('cypress/fixtures/qd/notice.eml'); - cy.get('button').contains('Upload').click(); + cy.get('button').contains('Upload').scrollIntoView().click({ force: true }); cy.contains('Waiting for the developer'); cy.get('#upload'); @@ -101,19 +101,19 @@ describe('Use case: Analysing Android apps', () => { cy.contains('Irish Data Protection Commission').click(); cy.contains('How to contact the DPA about the app?'); - if (useCase === 'informal-complaint') cy.contains('Informally ask the DPA to look in the app').click(); + if (useCase === 'informal-complaint') cy.contains('Informally ask the DPA to look into the app').click(); else cy.contains('Check whether I am personally affected').click(); if (useCase === 'formal-complaint') { cy.contains('Prove that you are affected by the tracking'); cy.get('#upload').selectFile('cypress/fixtures/qd/trackercontrol.csv'); - cy.get('button').contains('Upload').click(); + cy.get('button').contains('Upload').scrollIntoView().click({ force: true }); cy.contains('How did you install the app?'); cy.contains('through the Google Play Store').click(); - cy.contains('Do you have a SIM card in your phone?'); + cy.contains('Do you have a SIM card in your device?'); cy.contains('I have a SIM card').click(); } diff --git a/dbschema/default.esdl b/dbschema/default.esdl index 9cad41a..44ccbba 100644 --- a/dbschema/default.esdl +++ b/dbschema/default.esdl @@ -3,6 +3,30 @@ module default { scalar type AnalysisType extending enum; scalar type ControllerResponse extending enum; scalar type ComplaintType extending enum; + scalar type ProceedingState extending + enum; + scalar type ComplaintState extending + enum; + abstract type CreatedOn { required property createdOn: datetime { @@ -26,7 +50,9 @@ module default { } type Analysis { - required proceeding: Proceeding { on target delete delete source; }; + single proceeding := + .'formal' and not exists(.userNetworkActivity) else - 'askLoggedIntoAppStore' if .complaintType ?= 'formal' and not exists(.loggedIntoAppStore) else - 'askDeviceHasRegisteredSimCard' if .complaintType ?= 'formal' and not exists(.deviceHasRegisteredSimCard) else - 'askDeveloperAddress' if not exists(.developerAddress) else - 'readyToSend' + ComplaintState.notYet if .state != ProceedingState.awaitingComplaint else + ComplaintState.askIsUserOfApp if not exists(.complainantIsUserOfApp) else + ComplaintState.askAuthority if not exists(.complaintAuthority) else + ComplaintState.askComplaintType if not exists(.complaintType) else + ComplaintState.askUserNetworkActivity if .complaintType ?= ComplaintType.formal and not exists(.userNetworkActivity) else + ComplaintState.askLoggedIntoAppStore if .complaintType ?= ComplaintType.formal and not exists(.loggedIntoAppStore) else + ComplaintState.askDeviceHasRegisteredSimCard if .complaintType ?= ComplaintType.formal and not exists(.deviceHasRegisteredSimCard) else + ComplaintState.askDeveloperAddress if not exists(.developerAddress) else + ComplaintState.readyToSend ); - single initialAnalysis := (select Analysis filter .proceeding = Proceeding and .type = 'initial' limit 1); - single secondAnalysis := (select Analysis filter .proceeding = Proceeding and .type = 'second' limit 1); + stateUpdatedOn := (.; + ALTER TYPE default::Proceeding { + CREATE REQUIRED PROPERTY state: default::ProceedingState { + SET default := (default::ProceedingState.needsInitialAnalysis); + CREATE REWRITE + INSERT + USING ((default::ProceedingState.erased IF EXISTS (.erased) ELSE (default::ProceedingState.expired IF EXISTS (.expired) ELSE (default::ProceedingState.needsInitialAnalysis IF (NOT (EXISTS (.initialAnalysis)) AND EXISTS (.requestedAnalysis)) ELSE (default::ProceedingState.initialAnalysisFailed IF (NOT (EXISTS (.initialAnalysis)) AND NOT (EXISTS (.requestedAnalysis))) ELSE (default::ProceedingState.initialAnalysisFoundNothing IF NOT (.initialAnalysis.foundTrackers) ELSE (default::ProceedingState.awaitingControllerNotice IF (NOT (EXISTS (.noticeSent)) AND .initialAnalysis.foundTrackers) ELSE (default::ProceedingState.awaitingControllerResponse IF NOT (EXISTS (.controllerResponse)) ELSE (default::ProceedingState.needsSecondAnalysis IF (NOT (EXISTS (.secondAnalysis)) AND EXISTS (.requestedAnalysis)) ELSE (default::ProceedingState.secondAnalysisFailed IF (NOT (EXISTS (.secondAnalysis)) AND NOT (EXISTS (.requestedAnalysis))) ELSE (default::ProceedingState.secondAnalysisFoundNothing IF NOT (.secondAnalysis.foundTrackers) ELSE (default::ProceedingState.awaitingComplaint IF (NOT (EXISTS (.complaintSent)) AND .secondAnalysis.foundTrackers) ELSE default::ProceedingState.complaintSent)))))))))))); + CREATE REWRITE + UPDATE + USING ((default::ProceedingState.erased IF EXISTS (.erased) ELSE (default::ProceedingState.expired IF EXISTS (.expired) ELSE (default::ProceedingState.needsInitialAnalysis IF (NOT (EXISTS (.initialAnalysis)) AND EXISTS (.requestedAnalysis)) ELSE (default::ProceedingState.initialAnalysisFailed IF (NOT (EXISTS (.initialAnalysis)) AND NOT (EXISTS (.requestedAnalysis))) ELSE (default::ProceedingState.initialAnalysisFoundNothing IF NOT (.initialAnalysis.foundTrackers) ELSE (default::ProceedingState.awaitingControllerNotice IF (NOT (EXISTS (.noticeSent)) AND .initialAnalysis.foundTrackers) ELSE (default::ProceedingState.awaitingControllerResponse IF NOT (EXISTS (.controllerResponse)) ELSE (default::ProceedingState.needsSecondAnalysis IF (NOT (EXISTS (.secondAnalysis)) AND EXISTS (.requestedAnalysis)) ELSE (default::ProceedingState.secondAnalysisFailed IF (NOT (EXISTS (.secondAnalysis)) AND NOT (EXISTS (.requestedAnalysis))) ELSE (default::ProceedingState.secondAnalysisFoundNothing IF NOT (.secondAnalysis.foundTrackers) ELSE (default::ProceedingState.awaitingComplaint IF (NOT (EXISTS (.complaintSent)) AND .secondAnalysis.foundTrackers) ELSE default::ProceedingState.complaintSent)))))))))))); + }; + }; + CREATE SCALAR TYPE default::ComplaintState EXTENDING enum; + ALTER TYPE default::Proceeding { + CREATE REQUIRED PROPERTY complaintState := ((default::ComplaintState.notYet IF (.state != default::ProceedingState.awaitingComplaint) ELSE (default::ComplaintState.askIsUserOfApp IF NOT (EXISTS (.complainantIsUserOfApp)) ELSE (default::ComplaintState.askAuthority IF NOT (EXISTS (.complaintAuthority)) ELSE (default::ComplaintState.askComplaintType IF NOT (EXISTS (.complaintType)) ELSE (default::ComplaintState.askUserNetworkActivity IF ((.complaintType ?= default::ComplaintType.formal) AND NOT (EXISTS (.userNetworkActivity))) ELSE (default::ComplaintState.askLoggedIntoAppStore IF ((.complaintType ?= default::ComplaintType.formal) AND NOT (EXISTS (.loggedIntoAppStore))) ELSE (default::ComplaintState.askDeviceHasRegisteredSimCard IF ((.complaintType ?= default::ComplaintType.formal) AND NOT (EXISTS (.deviceHasRegisteredSimCard))) ELSE (default::ComplaintState.askDeveloperAddress IF NOT (EXISTS (.developerAddress)) ELSE default::ComplaintState.readyToSend))))))))); + }; + ALTER TYPE default::Proceeding { + CREATE TRIGGER logStateUpdate + AFTER UPDATE + FOR EACH + WHEN ((__old__.state != __new__.state)) + DO (UPDATE + default::ProceedingUpdateLog + FILTER + (.proceeding = __new__) + SET { + stateUpdatedOn := std::datetime_of_statement() + }); + }; + UPDATE default::Proceeding SET { + secondAnalysis := (SELECT . diff --git a/src/pages/a/index.astro b/src/pages/a/index.astro index d03b5b5..7e2ae22 100644 --- a/src/pages/a/index.astro +++ b/src/pages/a/index.astro @@ -19,23 +19,34 @@ const searchResults = language: Astro.currentLocale || 'en', }).then((r) => r.slice(0, 10))); -const resultsWithAnalysis = - searchResults && searchResults.length > 0 - ? await e - .group( - e.select(e.Analysis, (a) => ({ - filter: e.all( - e.set( - e.op(a.app.platform, '=', e.literal(e.Platform, platform)), - e.op(a.type, '=', e.literal(e.AnalysisType, 'initial')), - e.op(a.app.appId, 'in', e.set(...searchResults.map((r) => r.id))), +let resultsWithAnalysis: { + key: { app_id: string | null }; + elements: { id: string }[]; +}[] = []; + +try { + resultsWithAnalysis = + searchResults && searchResults.length > 0 + ? await e + .group( + e.select(e.Analysis, (a) => ({ + filter: e.all( + e.set( + e.op(a.app.platform, '=', e.literal(e.Platform, platform)), + e.op(a.type, '=', e.literal(e.AnalysisType, 'initial')), + e.op(a.app.appId, 'in', e.set(...searchResults.map((r) => r.id))), + ), ), - ), - })), - (a) => ({ id: true, by: { app_id: a.app.appId } }), - ) - .run(client) - : []; + })), + (a) => ({ id: true, by: { app_id: a.app.appId } }), + ) + .run(client) + : []; +} catch (error) { + console.log(error); + return new Response('Database Error', { status: 500 }); +} + const results = searchResults && searchResults.map((r) => ({ diff --git a/src/pages/p/[platform]/[appId]/index.ts b/src/pages/p/[platform]/[appId]/index.ts index 3d84236..6c07b90 100644 --- a/src/pages/p/[platform]/[appId]/index.ts +++ b/src/pages/p/[platform]/[appId]/index.ts @@ -24,35 +24,39 @@ export const POST: APIRoute = async ({ params, redirect, currentLocale, clientAd const { token: analysisToken } = await startAnalysis(platform, appMeta.appId); - await e - .insert(e.Proceeding, { - app: e - .insert(e.App, { - platform, - appId: appMeta.appId, - adamId: appMeta.adamId, - }) - .unlessConflict((app) => ({ - on: e.tuple([app.platform, app.appId]), - else: app, - })), - - token, - reference: generateReference(new Date()), - - appName: appMeta.appName, - developerName: appMeta.developerName, - developerEmail: appMeta.developerEmail, - developerAddress: appMeta.developerAddress, - ...(appMeta.developerAddress && { developerAddressSourceUrl: appMeta.storeUrl }), - privacyPolicyUrl: appMeta.privacyPolicyUrl, - - requestedAnalysis: e.insert(e.RequestedAnalysis, { - type: 'initial', - token: analysisToken, - }), - }) - .run(client); + try { + await e + .insert(e.Proceeding, { + app: e + .insert(e.App, { + platform, + appId: appMeta.appId, + adamId: appMeta.adamId, + }) + .unlessConflict((app) => ({ + on: e.tuple([app.platform, app.appId]), + else: app, + })), + + token, + reference: generateReference(new Date()), + + appName: appMeta.appName, + developerName: appMeta.developerName, + developerEmail: appMeta.developerEmail, + developerAddress: appMeta.developerAddress, + ...(appMeta.developerAddress && { developerAddressSourceUrl: appMeta.storeUrl }), + privacyPolicyUrl: appMeta.privacyPolicyUrl, + + requestedAnalysis: e.insert(e.RequestedAnalysis, { + type: 'initial', + token: analysisToken, + }), + }) + .run(client); + } catch (err) { + return new Response('Database Error', { status: 500 }); + } return redirect(`/p/${token}`); }; diff --git a/src/pages/p/[token]/attachment/[type].ts b/src/pages/p/[token]/attachment/[type].ts index 050a844..ddc3c38 100644 --- a/src/pages/p/[token]/attachment/[type].ts +++ b/src/pages/p/[token]/attachment/[type].ts @@ -155,7 +155,7 @@ export const POST: APIRoute = async ({ params, currentLocale, request }) => { .formData({ complainantAddress: zfd.text(), complainantContactDetails: zfd.text(), - complainantAgreesToUnencryptedCommunication: zfd.text(z.enum(['yes', 'no'])), + complainantAgreesToUnencryptedCommunication: zfd.text(z.enum(['yes', 'no-letter'])), }) .parse(await request.formData()); diff --git a/src/pages/private-api/analysis-result.ts b/src/pages/private-api/analysis-result.ts index 19b6163..73d85df 100644 --- a/src/pages/private-api/analysis-result.ts +++ b/src/pages/private-api/analysis-result.ts @@ -49,9 +49,12 @@ export const POST: APIRoute = async ({ request }) => { const deleteRequestedAnalysis = () => e - .delete(e.RequestedAnalysis, () => ({ + .update(e.Proceeding, () => ({ // eslint-disable-next-line camelcase - filter_single: { id: requestedAnalysis.id }, + filter_single: { id: proceeding.id }, + set: { + requestedAnalysis: null, + }, })) .run(client); @@ -68,21 +71,24 @@ export const POST: APIRoute = async ({ request }) => { if (app.id !== proceeding.app.appId) throw new Error('The HAR file contains traffic for a different app.'); await e - .insert(e.Analysis, { - proceeding: e - // eslint-disable-next-line camelcase - .select(e.Proceeding, () => ({ filter_single: { id: proceeding.id } })), - type: requestedAnalysis.type, + .update(e.Proceeding, () => ({ + // eslint-disable-next-line camelcase + filter_single: { id: proceeding.id }, + set: { + [requestedAnalysis.type === 'initial' ? 'initialAnalysis' : 'secondAnalysis']: e.insert(e.Analysis, { + type: requestedAnalysis.type, - startDate: new Date(res.har.log._tweasel.startDate), - endDate: new Date(res.har.log._tweasel.endDate), + startDate: new Date(res.har.log._tweasel.startDate), + endDate: new Date(res.har.log._tweasel.endDate), - appVersion: app.version, - appVersionCode: app.versionCode, + appVersion: app.version, + appVersionCode: app.versionCode, - har: JSON.stringify(res.har), - trackHarResult: res.trackHarResult, - }) + har: JSON.stringify(res.har), + trackHarResult: res.trackHarResult, + }), + }, + })) .run(client); await deleteRequestedAnalysis();