Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
8 changes: 4 additions & 4 deletions cypress/e2e/use-cases/android.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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();
}

Expand Down
139 changes: 104 additions & 35 deletions dbschema/default.esdl
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,30 @@ module default {
scalar type AnalysisType extending enum<initial, second>;
scalar type ControllerResponse extending enum<promise, denial, none>;
scalar type ComplaintType extending enum<formal, informal>;
scalar type ProceedingState extending
enum<erased,
expired,
needsInitialAnalysis,
initialAnalysisFailed,
initialAnalysisFoundNothing,
awaitingControllerNotice,
awaitingControllerResponse,
needsSecondAnalysis,
secondAnalysisFailed,
secondAnalysisFoundNothing,
awaitingComplaint,
complaintSent>;
scalar type ComplaintState extending
enum<notYet,
askIsUserOfApp,
askAuthority,
askComplaintType,
askUserNetworkActivity,
askLoggedIntoAppStore,
askDeviceHasRegisteredSimCard,
askDeveloperAddress,
readyToSend>;


abstract type CreatedOn {
required property createdOn: datetime {
Expand All @@ -26,7 +50,9 @@ module default {
}

type Analysis {
required proceeding: Proceeding { on target delete delete source; };
single proceeding :=
.<initialAnalysis[is Proceeding] if .type = AnalysisType.initial else
.<secondAnalysis[is Proceeding];
required type: AnalysisType;

required startDate: datetime;
Expand All @@ -37,10 +63,14 @@ module default {

required har: str { constraint max_size_bytes(52428800); };
required trackHarResult: json;
required foundTrackers: bool {
rewrite insert, update using (
any(std::json_typeof(json_array_unpack(.trackHarResult)) != 'null') if __specified__.trackHarResult else .foundTrackers
);
default := false;
}

single app := .proceeding.app;

constraint exclusive on ((.proceeding, .type));
}

type Proceeding extending CreatedOn {
Expand All @@ -55,42 +85,71 @@ module default {
developerAddressSourceUrl: str { constraint max_len_value(200); };
privacyPolicyUrl: str { constraint max_len_value(250); };

required state := (
'erased' if exists(.erased) else
'expired' if exists(.expired) else
'needsInitialAnalysis' if not exists(.initialAnalysis) and exists(.requestedAnalysis) else
'initialAnalysisFailed' if not exists(.initialAnalysis) and not exists(.requestedAnalysis) else
'initialAnalysisFoundNothing' if all(std::json_typeof(json_array_unpack(.initialAnalysis.trackHarResult)) = 'null') else
'awaitingControllerNotice' if not exists(.noticeSent) and any(std::json_typeof(json_array_unpack(.initialAnalysis.trackHarResult)) != 'null') else
'awaitingControllerResponse' if not exists(.controllerResponse) else
'needsSecondAnalysis' if not exists(.secondAnalysis) and exists(.requestedAnalysis) else
'secondAnalysisFailed' if not exists(.secondAnalysis) and not exists(.requestedAnalysis) else
'secondAnalysisFoundNothing' if all(std::json_typeof(json_array_unpack(.secondAnalysis.trackHarResult)) = 'null') else
'awaitingComplaint' if not exists(.complaintSent) and any(std::json_typeof(json_array_unpack(.secondAnalysis.trackHarResult)) != 'null') else
'complaintSent'
);
required stateUpdatedOn: datetime {
rewrite update using (
datetime_of_statement()
if __specified__.state and __old__.state != __subject__.state
else __old__.stateUpdatedOn
required state: ProceedingState {
rewrite insert, update using (
ProceedingState.erased if exists(.erased) else
ProceedingState.expired if exists(.expired) else
ProceedingState.needsInitialAnalysis if not exists(.initialAnalysis) and exists(.requestedAnalysis) else
ProceedingState.initialAnalysisFailed if not exists(.initialAnalysis) and not exists(.requestedAnalysis) else
ProceedingState.initialAnalysisFoundNothing if not .initialAnalysis.foundTrackers else
ProceedingState.awaitingControllerNotice if not exists(.noticeSent) and .initialAnalysis.foundTrackers else
ProceedingState.awaitingControllerResponse if not exists(.controllerResponse) else
ProceedingState.needsSecondAnalysis if not exists(.secondAnalysis) and exists(.requestedAnalysis) else
ProceedingState.secondAnalysisFailed if not exists(.secondAnalysis) and not exists(.requestedAnalysis) else
ProceedingState.secondAnalysisFoundNothing if not .secondAnalysis.foundTrackers else
ProceedingState.awaitingComplaint if not exists(.complaintSent) and .secondAnalysis.foundTrackers else
ProceedingState.complaintSent
);
default := datetime_current();
default := ProceedingState.needsInitialAnalysis;
};

# We need to use these triggers to create separate log entries because we need to compare the state after
# modification, but can not edit the triggering entry in a trigger.
trigger logStateUpdate after update for each
when (__old__.state != __new__.state)
do (
update ProceedingUpdateLog filter .proceeding = __new__ set {
stateUpdatedOn := datetime_of_statement()
}
);

trigger createUpdateLog after insert for each
do (
insert ProceedingUpdateLog {
proceeding := __new__,
stateUpdatedOn := __new__.createdOn
}
);

# This is to ensure there are no orphaned RequestedAnalysis
trigger removeRequestedAnalysis after update for each
when (__old__.requestedAnalysis not in __new__.requestedAnalysis)
do (
delete __old__.requestedAnalysis
);

required complaintState := (
'notYet' if .state != 'awaitingComplaint' else
'askIsUserOfApp' if not exists(.complainantIsUserOfApp) else
'askAuthority' if not exists(.complaintAuthority) else
'askComplaintType' if not exists(.complaintType) else
'askUserNetworkActivity' if .complaintType ?= <ComplaintType>'formal' and not exists(.userNetworkActivity) else
'askLoggedIntoAppStore' if .complaintType ?= <ComplaintType>'formal' and not exists(.loggedIntoAppStore) else
'askDeviceHasRegisteredSimCard' if .complaintType ?= <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 = <AnalysisType>'initial' limit 1);
single secondAnalysis := (select Analysis filter .proceeding = Proceeding and .type = <AnalysisType>'second' limit 1);
stateUpdatedOn := (.<proceeding[is ProceedingUpdateLog]).stateUpdatedOn;

single initialAnalysis: Analysis {
constraint exclusive;
on source delete delete target;
};
single secondAnalysis: Analysis {
constraint exclusive;
on source delete delete target;
};

noticeSent: datetime;
controllerResponse: ControllerResponse;
Expand All @@ -110,11 +169,21 @@ module default {
multi uploads := .<proceeding[is MessageUpload];
single requestedAnalysis: RequestedAnalysis {
constraint exclusive;
on target delete allow;
on target delete deferred restrict;
on source delete delete target;
};
}

type ProceedingUpdateLog {
required single proceeding: Proceeding {
on target delete delete source;
constraint exclusive;
}
required stateUpdatedOn: datetime {
default := datetime_current();
};
}

type MessageUpload extending CreatedOn {
required proceeding: Proceeding { on target delete delete source; };

Expand Down
111 changes: 111 additions & 0 deletions dbschema/migrations/00005-m1smzp7.edgeql
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
CREATE MIGRATION m1osgfejsdyzdi2zrfrlsxokbs6nrk4aomuqd4hqpvktoicyxly5ta
ONTO m14kn6nhfn4ujx45frwhr6h4xkjvmgvgshzxbafecelruhz37rra2a
{
ALTER TYPE default::Proceeding {
ALTER LINK initialAnalysis {
RESET EXPRESSION;
RESET EXPRESSION;
ON SOURCE DELETE DELETE TARGET;
RESET OPTIONALITY;
CREATE CONSTRAINT std::exclusive;
SET TYPE default::Analysis;
};
ALTER LINK secondAnalysis {
RESET EXPRESSION;
RESET EXPRESSION;
ON SOURCE DELETE DELETE TARGET;
RESET OPTIONALITY;
CREATE CONSTRAINT std::exclusive;
SET TYPE default::Analysis;
};
};
CREATE TYPE default::ProceedingUpdateLog {
CREATE REQUIRED SINGLE LINK proceeding: default::Proceeding {
ON TARGET DELETE DELETE SOURCE;
CREATE CONSTRAINT std::exclusive;
};
CREATE REQUIRED PROPERTY stateUpdatedOn: std::datetime {
SET default := (std::datetime_current());
};
};
FOR p IN (SELECT default::Proceeding {id, stateUpdatedOn}) UNION (
INSERT ProceedingUpdateLog {
proceeding := p,
stateUpdatedOn := p.stateUpdatedOn
}
);
ALTER TYPE default::Proceeding {
DROP PROPERTY complaintState;
DROP PROPERTY stateUpdatedOn;
};
ALTER TYPE default::Proceeding {
DROP PROPERTY state;
};
ALTER TYPE default::Analysis {
CREATE REQUIRED PROPERTY foundTrackers: std::bool {
SET default := false;
CREATE REWRITE
INSERT
USING ((std::any((std::json_typeof(std::json_array_unpack(.trackHarResult)) != 'null')) IF __specified__.trackHarResult ELSE .foundTrackers));
CREATE REWRITE
UPDATE
USING ((std::any((std::json_typeof(std::json_array_unpack(.trackHarResult)) != 'null')) IF __specified__.trackHarResult ELSE .foundTrackers));
};
};
UPDATE default::Analysis SET {
foundTrackers := (std::any((std::json_typeof(std::json_array_unpack(.trackHarResult)) != 'null')))
};
CREATE SCALAR TYPE default::ProceedingState EXTENDING enum<erased, expired, needsInitialAnalysis, initialAnalysisFailed, initialAnalysisFoundNothing, awaitingControllerNotice, awaitingControllerResponse, needsSecondAnalysis, secondAnalysisFailed, secondAnalysisFoundNothing, awaitingComplaint, complaintSent>;
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<notYet, askIsUserOfApp, askAuthority, askComplaintType, askUserNetworkActivity, askLoggedIntoAppStore, askDeviceHasRegisteredSimCard, askDeveloperAddress, readyToSend>;
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 .<proceeding[IS default::Analysis] FILTER .type = default::AnalysisType.second LIMIT 1),
initialAnalysis := (SELECT .<proceeding[IS default::Analysis] FILTER .type = default::AnalysisType.initial LIMIT 1)
};
ALTER TYPE default::Analysis {
DROP CONSTRAINT std::exclusive ON ((.proceeding, .type));
ALTER LINK proceeding {
USING ((.<initialAnalysis[IS default::Proceeding] IF (.type = default::AnalysisType.initial) ELSE .<secondAnalysis[IS default::Proceeding]));
RESET ON TARGET DELETE;
SET SINGLE;
RESET OPTIONALITY;
};
};
ALTER TYPE default::Proceeding {
CREATE PROPERTY stateUpdatedOn := (.<proceeding[IS default::ProceedingUpdateLog].stateUpdatedOn);
CREATE TRIGGER createUpdateLog
AFTER INSERT
FOR EACH DO (INSERT
default::ProceedingUpdateLog
{
proceeding := __new__,
stateUpdatedOn := __new__.createdOn
});
};
};
16 changes: 16 additions & 0 deletions dbschema/migrations/00006-m1o43u7.edgeql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
CREATE MIGRATION m1o43u7awhxou3dd7aa6nkf7d2hyi5u62jbvykj2c2vi4rknloatcq
ONTO m1osgfejsdyzdi2zrfrlsxokbs6nrk4aomuqd4hqpvktoicyxly5ta
{
ALTER TYPE default::Proceeding {
ALTER LINK requestedAnalysis {
ON TARGET DELETE DEFERRED RESTRICT;
};
CREATE TRIGGER removeRequestedAnalysis
AFTER UPDATE
FOR EACH
WHEN ((__old__.requestedAnalysis NOT IN __new__.requestedAnalysis))
DO (DELETE
__old__.requestedAnalysis
);
};
};
Loading