diff --git a/fe1-web/src/features/wallet/screens/WalletCreateSeed.tsx b/fe1-web/src/features/wallet/screens/WalletCreateSeed.tsx index 28dc4624a9..b25a01a07b 100644 --- a/fe1-web/src/features/wallet/screens/WalletCreateSeed.tsx +++ b/fe1-web/src/features/wallet/screens/WalletCreateSeed.tsx @@ -116,7 +116,9 @@ const WalletCreateSeed = () => { {STRINGS.wallet_welcome_text_wallet_explanation_4} - {seed} + + {seed} + {STRINGS.wallet_welcome_text_wallet_explanation_5} diff --git a/fe1-web/src/features/wallet/screens/__tests__/__snapshots__/WalletCreateSeed.test.tsx.snap b/fe1-web/src/features/wallet/screens/__tests__/__snapshots__/WalletCreateSeed.test.tsx.snap index 7e5045c0dc..768ce311e2 100644 --- a/fe1-web/src/features/wallet/screens/__tests__/__snapshots__/WalletCreateSeed.test.tsx.snap +++ b/fe1-web/src/features/wallet/screens/__tests__/__snapshots__/WalletCreateSeed.test.tsx.snap @@ -475,6 +475,7 @@ exports[`Wallet create seed screen renders correctly 1`] = ` "textAlign": "left", } } + testID="seed_wallet_text" > one two three diff --git a/tests/karate/docs/README.md b/tests/karate/docs/README.md index 04a80e5e2a..3eae6f0f91 100644 --- a/tests/karate/docs/README.md +++ b/tests/karate/docs/README.md @@ -14,18 +14,18 @@ We then check that the responses the mock components receive are as expected. ## Architecture ### Features and Scenarios Karate test cases are called scenarios and they are grouped within different feature files. -Each feature file tests a different message type (i.e. electionOpen, createRollCall etc.). +Each feature file tests a different message type (i.e. electionOpen, createRollCall etc.). Scenarios are written in the Gerkhin syntax using the following keywords: -- **Given**: Prepare the JSON payload to be sent to the component being tested. +- **Given**: Prepare the JSON payload to be sent to the component being tested. - **When**: Defines the action that is to be performed with the payload. -For instance, `publish` creates a message of type publish that contains some high-level message data, or `send` to send raw JSON data. +For instance, `publish` creates a message of type publish that contains some high-level message data, or `send` to send raw JSON data. - **Then**: Asserts that the action taken in the 'When' step has the expected outcome. - **And**: Connector that can be used after any of the other keywords. ### Background section -Code defined in the background section of a feature file runs before each scenario. This is especially useful for: +Code defined in the background section of a feature file runs before each scenario. This is especially useful for: - **Sharing scopes with other features**: The call to `read(classpath: "path/to/feature")` is used to make the current feature share the same scope as the feature that is called. This means they share definitions (def variables) and configurations. @@ -36,15 +36,15 @@ For instance, reading `mockClient.feature` exposes functions like `createMockFro `simpleScenarios.feature` contains many such useful setup steps. ### Data model -To generate valid message data for JSON payloads dynamically, a simplified version of the model is implemented in Java code. +To generate valid message data for JSON payloads dynamically, a simplified version of the model is implemented in Java code. Mock components can create valid objects (for instance LAO, RollCall, Elections etc.), that can be used to handcraft messages. -These objects also provide functions to override their own data to some invalid values, to craft invalid messages. +These objects also provide functions to override their own data to some invalid values, to craft invalid messages. Some care needs to be taken if more of these functions to override valid data are implemented in the future. For example, when setting an invalid LAO name to test if the server rejects this, the LAO id also needs to be recomputed. The LAO id depends on the LAO name, and if the id is not recomputed the test might fail due to invalid LAO id, and not because the name was invalid. As of February 2024, there is no way to distinguish this as only the error code is asserted and not the error message. -This is a possible improvement that could be done in the future. +This is a possible improvement that could be done in the future.
Architecture @@ -105,31 +105,56 @@ The Karate plugin for IntelliJ is also recommended. To test a backend, you don't need more setup that what is needed to build that backend. Use the resources provided by those projects. -### Android Front-end +### Frontend + +#### Appium +Appium provides the API that will allow us to test both frontends on multiple platforms. You can install the CLI version with the command `npm install -g appium` or install the [Desktop App](https://github.com/appium/appium-desktop/releases/) instead. -To test the Android Frontend, you need to have an Android emulator installed. -The easiest way to achieve it is to install it through [Android Studio](https://developer.android.com/studio) : -Go to `Tools -> AVD Manager` and create an emulator. +#### Android +You need to add the Android driver to Appium with the following command. +```shell +appium driver install uiautomator2 +``` -Then you need to install Appium. -You can install either the command line app with `npm install -g appium`, or the [Desktop App](https://github.com/appium/appium-desktop/releases/). +Then, you need to create an Android emulator in Android studio if you don't have one yet. The easiest way to achieve it is to install it through [Android Studio](https://developer.android.com/studio): Go to `Tools -> Device Manager` and create a virtual device. Finally, you need to set the environment variable `ANDROID_HOME` (The previous name was`ANDROID_SDK_ROOT` and it still works) to your Android SDK installation. Find it by opening Android Studio and going to `Tools -> SDK Manager`. It stands next to `Android SDK Location`. -If your Computer runs on Windows : we strongly advise that you do not use a VM or WSL. -You will encounter problems you would not have otherwise, some of which might even be technically impossible to solve. +If your Computer runs on Windows: we strongly advise that you do not use a VM or WSL. You will encounter problems you would not have otherwise, some of which might even be technically impossible to solve. -### Web Front-end +### Google Chrome & Microsoft Edge +Make sure you have Google Chrome and/or Microsoft Edge installed. + +Then, install the Appium driver. +``` +appium driver install chromium +``` +### Firefox +Make sure you have Firefox installed. Then download the latest release of [geckodriver](https://github.com/mozilla/geckodriver/releases) and add it to your path. + +Then, install the Appium driver. +``` +appium driver install gecko +``` + +### Safari +Install the Appium driver. +``` +appium driver install safari +``` -Make sure you have [Google Chrome](https://www.google.com/intl/en/chrome/) and [npm](https://nodejs.org/en/download/) installed. +Then, allow remote automation of Safari. +``` +safaridriver --enable +``` ## Running the Tests ### Backend -Build the backend you want to test. +Build the backend you want to test. Follow the steps described in the corresponding subproject. Keep the executables in their default build location, Karate will find them there. @@ -150,51 +175,39 @@ With the Karate plugin for IntelliJ, the full tests can also be run directly fro ### Android Front-end - Build the application by running `./gradlew assembleDebug` in the corresponding directory. -Start the Android Emulator. -Start Appium : if you use the GUI, delete the text in Host and Port and click on the start server button. -If you use the terminal, run `appium`. - -With Android Hedgehog the emulator can either run in a tool window or a standalone window. -(To have it in a standalone window, go to `File -> Settings -> Tools -> Emulator` and unselect `Launch in a tool window`). -- Standalone window : \ -The emulator window name should match : `Android Emulator - avd:id` \ -Ex: `Android Emulator - Pixel_4_API_30:5554` -- Tool window : open the `Extended Controls` (the 3 points above the emulator) - - The Extended Controls window name is the `avd` but with ' ' instead of '_' \ - Ex: `Pixel 4 API 30` for an `avd` of `Pixel_4_API_30` - - Go to `Help`, under `Emulator ADB serial number` it should match `emulator-id`\ - Ex: `emulator-5554` - -Emulator -Emulator standalone window with Extended Controls.

+Then, start an emulator from Android Studio and launch the Appium server (using the command `appium`). +Finally run the tests. +``` +mvn test -Dkarate.env=android -Dtest=FrontEndTest#fullTest +``` -Make sure the [karate-config](src/test/java/karate-config.js) is correct. -More precisely : +In case you have multiple emulators running, you may specify one by avd id. To find the avd id of some emulator, go to the Device Manager (`Tools -> Device Manager`) and follow the steps in the image below. -- `deviceName` is set to `emulator-`, here it would be `emulator-5554`. -- `avd` is set to the avd name indicated on the emulator window, here it would be `Pixel_4_API_30`. +![Find avd id Android Studio](./images/android_studio_find_avd_id.png) -Run the test with : -`mvn test -DargLine="-Dkarate.env=android"` +Once you have the avd id of your emulator, you can use the command below to run the tests on this specific emulator. +```shell +mvn test -Dkarate.env=android -Davd= -Dtest=FrontEndTest#fullTest +#e.g. mvn test -Dkarate.env=android -Davd=Galaxy_Note_9_API_29 -Dtest=FrontEndTest#fullTest +``` ### Web Front-end - Build the app with `npm run build-web` in the corresponding directory. -If your Chrome installation is not one of these : - -- mac: `/Applications/Google Chrome.app/Contents/MacOS/Google Chrome` -- win: `C:/Program Files (x86)/Google/Chrome/Application/chrome.exe` \ - (You should check, it is also common for Chrome to installed in `/Programm Files/` rather than `/Program Files (x86)/`) +Launch the Appium server (with `appium`). -You need to set the executable manually in the driver definition in the [Web page object](src/test/java/fe/utils/web.feature) (line 6). - -Change the line from `* configure driver = { type: 'chrome' }`to `* configure driver = { type: 'chrome', executable: 'PATH' }`\ -where PATH is the path to your Chrome installation.\ +Run the tests. +``` +mvn test -Dkarate.env=web -Dtest=FrontEndTest#fullTest +``` -Run the test with : -`mvn test -DargLine="-Dkarate.env=web"` +The following options are available (option names must be prefixed by `-D`). +| Name | Description | Default | +|--------------|------------------------------------------------|-------------------------------------------| +| browser | One of 'chrome', 'safari', 'edge' or 'firefox' | 'chrome' | +| url | URL of the web app | 'file:../../fe1-web/web-build/index.html' | +| screenWidth | Width of the browser | 1920 | +| screenHeight | Height of the browser | 1080 | diff --git a/tests/karate/docs/images/android_studio_find_avd_id.png b/tests/karate/docs/images/android_studio_find_avd_id.png new file mode 100644 index 0000000000..9662931fed Binary files /dev/null and b/tests/karate/docs/images/android_studio_find_avd_id.png differ diff --git a/tests/karate/src/test/java/fe/LAO/create_lao.feature b/tests/karate/src/test/java/fe/LAO/create_lao.feature deleted file mode 100644 index 28dd269c8b..0000000000 --- a/tests/karate/src/test/java/fe/LAO/create_lao.feature +++ /dev/null @@ -1,28 +0,0 @@ -@env=android,web -Feature: Create LAO - - Background: Driver basic setup - # This page object will add view selectors variables to the current scope, start the app and setup mock backend - # The is a basic setup (selected by its name tag): it will start on the home page being connected to the backend - # More info on tag selection: https://github.com/karatelabs/karate#call-tag-selector - * def page_object = 'classpath:fe/utils/.feature@name=basic_setup' - * replace page_object.env = karate.env - * call read(page_object) - - Scenario: Create a LAO send the right messages to the backend - When backend.setLaoCreateMode() - And input(tab_launch_lao_name_selector, 'Lao Name') - And click(tab_launch_create_lao_selector) - - # Retrieving sent messages - * json create_lao = buffer.takeTimeout(timeout) - * json subscribe = buffer.takeTimeout(withMethod('subscribe'), timeout) - * json catchup = buffer.takeTimeout(withMethod('catchup'), timeout) - - # TODO Test consensus subscription when it is implemented on both fe - # * json subscribe_consensus = backend.takeTimeout(withMethod('subscribe'), timeout) - # * json catchup_consensus = backend.takeTimeout(withMethod('catchup'), timeout) - - Then match create_lao contains deep { method: 'publish', params: { channel: '/root' }} - Then match subscribe contains deep { method: 'subscribe' } - Then match catchup contains deep { method: 'catchup' } \ No newline at end of file diff --git a/tests/karate/src/test/java/fe/RollCall/rollCallClose.feature b/tests/karate/src/test/java/fe/RollCall/rollCallClose.feature deleted file mode 100644 index 5f9874492c..0000000000 --- a/tests/karate/src/test/java/fe/RollCall/rollCallClose.feature +++ /dev/null @@ -1,54 +0,0 @@ -@env=android,web -Feature: - - Scenario: Closing a roll call without attendees include only organizer - # Do all the steps until (and included) opening a roll call - * call read('classpath:fe/utils/simpleScenarios.feature@name=open_roll_call') - - # Close the opened roll-call - * def rc_page_object = 'classpath:fe/utils/.feature@name=close_roll_call' - * replace rc_page_object.env = karate.env - And call read(rc_page_object) - - # Retrieving sent messages - * json close_rc_json = buffer.takeTimeout(timeout) - * string close_rc_string = close_rc_json - - # General message verification - Then match close_rc_json contains deep { method: 'publish' } - * match messageVerification.verifyMessageIdField(close_rc_string) == true - And match messageVerification.verifyMessageSignature(close_rc_string) == true - - # Roll Call specific verification - And match verificationUtils.getObject(close_rc_string) == constants.ROLL_CALL - * match verificationUtils.getAction(close_rc_string) == constants.CLOSE - And match (rollCallVerification.verifyRollCallUpdateId(close_rc_string, constants.CLOSE)) == true - * match rollCallVerification.verifyAttendeesPresence(close_rc_string) == true - - And match backend.receiveNoMoreResponses() == true - - Scenario: RC close several attendees - # Do all the steps until (and included) opening a roll call - * call read('classpath:fe/utils/simpleScenarios.feature@name=open_roll_call') - - # Close the opened roll-call - * def rc_page_object = 'classpath:fe/utils/.feature@name=close_roll_call_w_attendees' - * replace rc_page_object.env = karate.env - And call read(rc_page_object) {token1 : 'J9fBzJV70Jk5c-i3277Uq4CmeL4t53WDfUghaK0HpeM=', token2: 'oKHk3AivbpNXk_SfFcHDaVHcCcY8IBfHE7auXJ7h4ms='} - - # Retrieving sent messages - * json close_rc_json = buffer.takeTimeout(timeout) - * string close_rc_string = close_rc_json - - # General message verification - Then match close_rc_json contains deep { method: 'publish' } - * match messageVerification.verifyMessageIdField(close_rc_string) == true - And match messageVerification.verifyMessageSignature(close_rc_string) == true - - # Roll Call specific verification - And match verificationUtils.getObject(close_rc_string) == constants.ROLL_CALL - * match verificationUtils.getAction(close_rc_string) == constants.CLOSE - And match (rollCallVerification.verifyRollCallUpdateId(close_rc_string, constants.CLOSE)) == true - * match (rollCallVerification.verifyAttendeesPresence(close_rc_string, token1, token2)) == true - - And match backend.receiveNoMoreResponses() == true diff --git a/tests/karate/src/test/java/fe/RollCall/rollCallCreation.feature b/tests/karate/src/test/java/fe/RollCall/rollCallCreation.feature deleted file mode 100644 index bfe0afccc4..0000000000 --- a/tests/karate/src/test/java/fe/RollCall/rollCallCreation.feature +++ /dev/null @@ -1,40 +0,0 @@ -@env=android,web -Feature: Create RollCall - - Scenario: Creating a Roll Call sends right message to backend and displays element - - # Call the lao creation scenario that is front-end agnostic - * call read('classpath:fe/utils/simpleScenarios.feature@name=create_lao') - - # Call the roll call creation util that is front-end dependant - * def rc_page_object = 'classpath:fe/utils/.feature@name=create_roll_call' - * replace rc_page_object.env = karate.env - - Given call read(rc_page_object) - * backend.clearBuffer() - And backend.setValidBroadcastMode() - - When click(roll_call_confirm_selector) - - # Retrieving sent messages - * json create_rc_json = buffer.takeTimeout(timeout) - * string create_rc_string = create_rc_json - - # General message verification - Then match create_rc_json contains deep { method: 'publish' } - * match messageVerification.verifyMessageIdField(create_rc_string) == true - And match messageVerification.verifyMessageSignature(create_rc_string) == true - - # Roll Call specific verification - And match verificationUtils.getObject(create_rc_string) == constants.ROLL_CALL - * match verificationUtils.getAction(create_rc_string) == constants.CREATE - * match verificationUtils.getName(create_rc_string) == constants.RC_NAME - And match rollCallVerification.verifyRollCallId(create_rc_string) == true - - And match backend.receiveNoMoreResponses() == true - - # check display - And match text(event_name_selector) contains constants.RC_NAME - - - diff --git a/tests/karate/src/test/java/fe/RollCall/rollCallOpen.feature b/tests/karate/src/test/java/fe/RollCall/rollCallOpen.feature deleted file mode 100644 index a1e2f39fcf..0000000000 --- a/tests/karate/src/test/java/fe/RollCall/rollCallOpen.feature +++ /dev/null @@ -1,27 +0,0 @@ -@env=android,web -Feature: Open Roll Call - - Scenario: Creating a Roll Call send right message to backend and displays element - Given call read('classpath:fe/utils/simpleScenarios.feature@name=create_roll_call') - * def rc_page_object = 'classpath:fe/utils/.feature@name=open_roll_call' - * replace rc_page_object.env = karate.env - - - * backend.setValidBroadcastMode() - And call read(rc_page_object) - - # Retrieving sent messages - * json open_rc_json = buffer.takeTimeout(timeout) - * string open_rc_string = open_rc_json - - # General message verification - Then match open_rc_json contains deep { method: 'publish' } - * match messageVerification.verifyMessageIdField(open_rc_string) == true - And match messageVerification.verifyMessageSignature(open_rc_string) == true - - # Roll Call specific verification - And match verificationUtils.getObject(open_rc_string) == constants.ROLL_CALL - * match verificationUtils.getAction(open_rc_string) == constants.OPEN - And match (rollCallVerification.verifyRollCallUpdateId(open_rc_string, constants.OPEN)) == true - - And match backend.receiveNoMoreResponses() == true diff --git a/tests/karate/src/test/java/fe/RollCall/rollCallReopen.feature b/tests/karate/src/test/java/fe/RollCall/rollCallReopen.feature deleted file mode 100644 index 89e1a5c3b9..0000000000 --- a/tests/karate/src/test/java/fe/RollCall/rollCallReopen.feature +++ /dev/null @@ -1,27 +0,0 @@ -@env=android,web -Feature: - - Scenario: - # Do all the steps up until (and including) closing a roll call - * call read('classpath:fe/utils/simpleScenarios.feature@name=close_roll_call') - - # Reopen the closed roll-call - * def rc_page_object = 'classpath:fe/utils/.feature@name=reopen_roll_call' - * replace rc_page_object.env = karate.env - And call read(rc_page_object) - - # Retrieving sent messages - * json reopen_rc_json = buffer.takeTimeout(timeout) - * string reopen_rc_string = reopen_rc_json - - # General message verification - Then match reopen_rc_json contains deep { method: 'publish' } - * match messageVerification.verifyMessageIdField(reopen_rc_string) == true - And match messageVerification.verifyMessageSignature(reopen_rc_string) == true - - # Roll Call specific verification - And match verificationUtils.getObject(reopen_rc_string) == constants.ROLL_CALL - * match verificationUtils.getAction(reopen_rc_string) == constants.REOPEN - And match (rollCallVerification.verifyRollCallUpdateId(reopen_rc_string, constants.REOPEN)) == true - - And match backend.receiveNoMoreResponses() == true diff --git a/tests/karate/src/test/java/fe/election/castVote.feature b/tests/karate/src/test/java/fe/election/castVote.feature deleted file mode 100644 index 15408a94e2..0000000000 --- a/tests/karate/src/test/java/fe/election/castVote.feature +++ /dev/null @@ -1,28 +0,0 @@ -@env=android,web -Feature: - - Scenario: Casting a vote sends the correct message to the backend - # Do all the steps up until (and including) opening an election - Given call read('classpath:fe/utils/simpleScenarios.feature@name=open_election') - - # Casting a vote - When def election_page_object = 'classpath:fe/utils/.feature@name=cast_vote' - * replace election_page_object.env = karate.env - And call read(election_page_object) - - # Retrieving sent messages - * json election_json = buffer.takeTimeout(timeout) - * string election_string = election_json - - # General message verification - Then match election_json contains deep { method: 'publish' } - * match messageVerification.verifyMessageIdField(election_string) == true - And match messageVerification.verifyMessageSignature(election_string) == true - - # Election specific verification - ballot index is 1 (second option) - And match verificationUtils.getObject(election_string) == constants.ELECTION - * match verificationUtils.getAction(election_string) == constants.CAST_VOTE - And match electionVerification.getVote(election_string) == '1' - * match (electionVerification.verifyVoteId(election_string, 1)) == true - - And match backend.receiveNoMoreResponses() == true diff --git a/tests/karate/src/test/java/fe/election/electionEnd.feature b/tests/karate/src/test/java/fe/election/electionEnd.feature deleted file mode 100644 index ee6ee41b6d..0000000000 --- a/tests/karate/src/test/java/fe/election/electionEnd.feature +++ /dev/null @@ -1,26 +0,0 @@ -@env=android,web -Feature: - - Scenario: Casting a vote sends the correct message to the backend - # Do all the steps up until (and including) casting a vote - Given call read('classpath:fe/utils/simpleScenarios.feature@name=cast_vote') - - # end an election - When def election_page_object = 'classpath:fe/utils/.feature@name=end_election' - * replace election_page_object.env = karate.env - And call read(election_page_object) - - # Retrieving sent messages - * json election_json = buffer.takeTimeout(timeout) - * string election_string = election_json - - # General message verification - Then match election_json contains deep { method: 'publish' } - * match messageVerification.verifyMessageIdField(election_string) == true - And match messageVerification.verifyMessageSignature(election_string) == true - - # Election specific verification - And match verificationUtils.getObject(election_string) == constants.ELECTION - * match verificationUtils.getAction(election_string) == constants.END - - And match backend.receiveNoMoreResponses() == true diff --git a/tests/karate/src/test/java/fe/election/electionOpen.feature b/tests/karate/src/test/java/fe/election/electionOpen.feature deleted file mode 100644 index 89228fd565..0000000000 --- a/tests/karate/src/test/java/fe/election/electionOpen.feature +++ /dev/null @@ -1,28 +0,0 @@ -@env=android,web -Feature: - - Scenario: - - Scenario: Creating an election sends the correct message to the backend - # Do all the steps up until (and including) closing a roll call - * call read('classpath:fe/utils/simpleScenarios.feature@name=setup_election') - - # Setup an election - * def election_page_object = 'classpath:fe/utils/.feature@name=open_election' - * replace election_page_object.env = karate.env - And call read(election_page_object) - - # Retrieving sent messages - * json election_json = buffer.takeTimeout(timeout) - * string election_string = election_json - - # General message verification - Then match election_json contains deep { method: 'publish' } - * match messageVerification.verifyMessageIdField(election_string) == true - And match messageVerification.verifyMessageSignature(election_string) == true - - # Election specific verification - And match verificationUtils.getObject(election_string) == constants.ELECTION - * match verificationUtils.getAction(election_string) == constants.OPEN - - And match backend.receiveNoMoreResponses() == true diff --git a/tests/karate/src/test/java/fe/election/electionSetup.feature b/tests/karate/src/test/java/fe/election/electionSetup.feature deleted file mode 100644 index 740303230b..0000000000 --- a/tests/karate/src/test/java/fe/election/electionSetup.feature +++ /dev/null @@ -1,38 +0,0 @@ -@env=android,web -Feature: - - Scenario: Creating an election sends the correct message to the backend - # Do all the steps up until (and including) closing a roll call - Given call read('classpath:fe/utils/simpleScenarios.feature@name=close_roll_call') - - # Setup an election - When def election_page_object = 'classpath:fe/utils/.feature@name=setup_election' - * replace election_page_object.env = karate.env - And call read(election_page_object) - - # Retrieving sent messages - * json election_json = buffer.takeTimeout(timeout) - * string election_string = election_json - * json subscribe = buffer.takeTimeout(withMethod('subscribe'), timeout) - * json catchup = buffer.takeTimeout(withMethod('catchup'), timeout) - - # General message verification - Then match election_json contains deep { method: 'publish' } - Then match subscribe contains deep { method: 'subscribe' } - Then match catchup contains deep { method: 'catchup' } - * match messageVerification.verifyMessageIdField(election_string) == true - And match messageVerification.verifyMessageSignature(election_string) == true - - # Election specific verification - And match verificationUtils.getObject(election_string) == constants.ELECTION - * match verificationUtils.getAction(election_string) == constants.SETUP - And match verificationUtils.getVersion(election_string) == constants.OPEN_BALLOT - * match verificationUtils.getName(election_string) == constants.ELECTION_NAME - And match electionVerification.verifyElectionId(election_string) == true - * match electionVerification.verifyQuestionId(election_string) == true - And match electionVerification.getQuestionContent(election_string) == constants.QUESTION_CONTENT - * match (electionVerification.getBallotOption(election_string, 0)) == constants.BALLOT_1 - And match (electionVerification.getBallotOption(election_string, 1)) == constants.BALLOT_2 - * match electionVerification.getVotingMethod(election_string) == constants.PLURALITY - - And match backend.receiveNoMoreResponses() == true diff --git a/tests/karate/src/test/java/fe/features/wallet.feature b/tests/karate/src/test/java/fe/features/wallet.feature new file mode 100644 index 0000000000..c307741ec0 --- /dev/null +++ b/tests/karate/src/test/java/fe/features/wallet.feature @@ -0,0 +1,8 @@ +Feature: Wallet + Background: + * call read('classpath:fe/utils/constants.feature') + + Scenario: Open the app for the first time and see the wallet seed + When call read(PLATFORM_FEATURES) { name: "#(OPEN_APP)" } + Then match text(wallet_seed_wallet_text) == "#regex ^([a-z]+\\s){11}[a-z]+$" + And screenshot() diff --git a/tests/karate/src/test/java/fe/net/MockBackend.java b/tests/karate/src/test/java/fe/net/MockBackend.java deleted file mode 100644 index 0860b4d3ce..0000000000 --- a/tests/karate/src/test/java/fe/net/MockBackend.java +++ /dev/null @@ -1,147 +0,0 @@ -package fe.net; - -import com.intuit.karate.Json; -import com.intuit.karate.Logger; -import com.intuit.karate.http.WebSocketServerBase; -import common.net.MessageBuffer; -import common.net.MessageQueue; -import karate.io.netty.channel.Channel; -import karate.io.netty.channel.ChannelHandlerContext; -import karate.io.netty.channel.SimpleChannelInboundHandler; -import karate.io.netty.handler.codec.http.websocketx.TextWebSocketFrame; - -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.function.Function; - -import static common.utils.Constants.*; - -/** Defines a mock backend server that is fully customisable. */ -public class MockBackend extends SimpleChannelInboundHandler { - - private final Logger logger = new Logger(getClass().getSimpleName()); - - private final MessageQueue queue; - private final WebSocketServerBase server; - // Will be set to true once the connection is established - private final CompletableFuture connected = new CompletableFuture<>(); - - // Defines the rule to apply on incoming messages to produce its reply. - // Can be null if no reply should be sent back. - private Function> replyProducer = ReplyMethods.ALWAYS_VALID; - private Channel channel; - private Json laoCreationMessageData; - - private String laoID; - - public MockBackend(MessageQueue queue, int port) { - this.queue = queue; - server = new WebSocketServerBase(port, "/", this); - logger.info("Mock Backend started"); - } - - /** - * Sets the reply producer of the backend. - * - *

It can be set to null if no reply should be sent back - * - * @param replyProducer to set - */ - public void setReplyProducer(Function> replyProducer) { - this.replyProducer = replyProducer; - } - - @Override - public void channelActive(ChannelHandlerContext ctx) { - channel = ctx.channel(); - connected.complete(true); - logger.trace("Client connected from the server side"); - } - - // As this object is the channel handler of the server, this function is called whenever a new - // message is received by it. - // The text message is held is a TextWebSocketFrame which is the primitive that is sent over the - // network - @Override - protected void channelRead0( - ChannelHandlerContext channelHandlerContext, TextWebSocketFrame frame) { - String frameText = frame.text(); - logger.info("message received : {}", frameText); - if (!frameText.toLowerCase().contains(CONSENSUS) && !frameText.toLowerCase().contains(COIN) && !frameText.contains(SOCIAL)) { - // We don't want consensus or coin messages to interfere since we do not test them yet - queue.onNewMsg(frameText); - } - if (replyProducer != null) replyProducer.apply(frameText).forEach(this::send); - } - - public int getPort() { - return server.getPort(); - } - - public boolean waitForConnection(long timeout) { - logger.info("Waiting for connection..."); - long start = System.currentTimeMillis(); - try { - connected.get(timeout, TimeUnit.MILLISECONDS); - logger.info("Connection established in {}s", (System.currentTimeMillis() - start) / 1000.0); - return true; - } catch (InterruptedException | ExecutionException e) { - e.printStackTrace(); - return false; - } catch (TimeoutException e) { - logger.error("timeout while waiting for connection to backend"); - return false; - } - } - - public boolean isConnected() { - return connected.isDone(); - } - - public void stop() { - logger.info("stopping server..."); - server.stop(); - } - - public void send(String text) { - logger.info("sending message : {}", text); - channel.eventLoop().submit(() -> channel.writeAndFlush(new TextWebSocketFrame(text))); - } - - public MessageBuffer getBuffer() { - return queue; - } - - /** Empties the buffer */ - public void clearBuffer() { - logger.info("Buffer cleared"); - queue.clear(); - } - - /** - * @return true if the message buffer is empty - */ - public boolean receiveNoMoreResponses() { - return queue.takeTimeout(5000) == null; - } - - /** - * Backend behaviour is specific to Lao Creation. It stores publish message and replies with a - * valid message It also replies with valid to subscribe and with the Lao creation message to the - * catch-up - */ - public void setLaoCreateMode() { - replyProducer = ReplyMethods.CATCHUP_VALID_RESPONSE; - } - - /** - * Backend behaviour is to respond to publish message with both broadcast and a valid response. It - * replies with valid to subscribes and empty (valid) message to catch-ups - */ - public void setValidBroadcastMode() { - replyProducer = ReplyMethods.BROADCAST_VALID_RESPONSE; - } -} diff --git a/tests/karate/src/test/java/fe/net/ReplyMethods.java b/tests/karate/src/test/java/fe/net/ReplyMethods.java deleted file mode 100644 index a1d856dce1..0000000000 --- a/tests/karate/src/test/java/fe/net/ReplyMethods.java +++ /dev/null @@ -1,102 +0,0 @@ -package fe.net; - -import com.intuit.karate.Json; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.function.Function; - -import static common.utils.Constants.*; -import static common.utils.JsonUtils.getJSON; - -/** - * This class contains useful message replies that can be used to tailor the response of the - * mockbackend depending on what action is mocked {@link MockBackend#setReplyProducer(Function)} - */ -public class ReplyMethods { - - private static final String VALID_REPLY_TEMPLATE = - "{\"jsonrpc\":\"2.0\",\"id\":%ID%,\"result\":0}"; - private static final String VALID_CATCHUP_REPLY_TEMPLATE = - "{\"jsonrpc\":\"2.0\",\"id\":%ID%,\"result\":[]}"; - - private static final String RC_CREATE_BROADCAST_TEMPLATE = - "{\"jsonrpc\":\"2.0\",\"method\": \"broadcast\",\"params\":%PARAM%}"; - - private static Json laoCreatePublishJson; - - private static List buildSingleton(String string) { - return Collections.singletonList(string); - } - - /** Always reply with a valid response */ - public static Function> ALWAYS_VALID = - msg -> { - Json msgJson = Json.of(msg); - int id = msgJson.get(ID); - String template = - msgJson.get(METHOD).equals(CATCHUP) - ? VALID_CATCHUP_REPLY_TEMPLATE - : VALID_REPLY_TEMPLATE; - return buildSingleton(template.replace("%ID%", Integer.toString(id))); - }; - - public static Function> LAO_CREATE_CATCHUP = - msg -> { - if (msg.contains(CONSENSUS) || msg.contains(COIN)) { - return ALWAYS_VALID.apply(msg); - } - Json msgJson = Json.of(msg); - String replaceId = - VALID_CATCHUP_REPLY_TEMPLATE.replace("%ID%", Integer.toString(msgJson.get(ID))); - if (laoCreatePublishJson == null) { - throw new IllegalStateException( - "When creating a catchup the laoCreate should not be null"); - } - return buildSingleton(replaceId.replace("[]", "[" + laoCreatePublishJson + "]")); - }; - - /** - * This returns a valid reply to subscribe messages and replies with the published lao to catch-ups - * It is specific to the LAO creation process - */ - public static Function> CATCHUP_VALID_RESPONSE = - msg -> { - Json msgJson = Json.of(msg); - String method = msgJson.get(METHOD); - if (PUBLISH.equals(method)) { - laoCreatePublishJson = getJSON(getJSON(Json.of(msg), PARAMS), MESSAGE); - } - if (CATCHUP.equals(method)) { - return LAO_CREATE_CATCHUP.apply(msg); - } else { // We want to respond valid result for both publish and subscribe - return ALWAYS_VALID.apply(msg); - } - }; - - /** This replies with a broadcast of the publish message and a valid response */ - public static Function> BROADCAST_VALID_RESPONSE = - msg -> { - Json msgJson = Json.of(msg); - String method = msgJson.get(METHOD); - - if (PUBLISH.equals(method)) { - Json param = getJSON(Json.of(msg), PARAMS); - String channel = param.get(CHANNEL); - - Map msgMap = param.get(MESSAGE); - Json send = Json.object(); - send.set(CHANNEL, channel); - send.set(MESSAGE, msgMap); - - String broadCast = RC_CREATE_BROADCAST_TEMPLATE.replace("%PARAM%", send.toString()); - String result = ALWAYS_VALID.apply(msg).get(0); - - return Arrays.asList(broadCast, result); - } else { - return ALWAYS_VALID.apply(msg); - } - }; -} diff --git a/tests/karate/src/test/java/fe/net/mockBackend.feature b/tests/karate/src/test/java/fe/net/mockBackend.feature deleted file mode 100644 index ad2f1c0d00..0000000000 --- a/tests/karate/src/test/java/fe/net/mockBackend.feature +++ /dev/null @@ -1,83 +0,0 @@ -@ignore @report=false -Feature: Mock Backend - - Scenario: Setup Mock-backend - * def newBuffer = - """ - function() { - var Queue = Java.type("common.net.MessageQueue") - return new Queue() - } - """ - * def getBackend = - """ - function() { - var Backend = Java.type("fe.net.MockBackend") - return new Backend(newBuffer(), port) - } - """ - - * def backend = call getBackend - - * def getRollCallVerification = - """ - function(){ - var RollCallVerification = Java.type("fe.utils.verification.RollCallVerification") - return new RollCallVerification() - } - """ - * def rollCallVerification = call getRollCallVerification - - * def getMessageVerification = - """ - function (){ - var MessageVerification = Java.type("fe.utils.verification.MessageVerification") - return new MessageVerification() - } - """ - - * def messageVerification = call getMessageVerification - - * def getVerificationUtils = - """ - function(){ - var VerificationUtils = Java.type("fe.utils.verification.VerificationUtils") - return new VerificationUtils() - } - """ - - * def verificationUtils = call getVerificationUtils - - * def getElectionVerification = - """ - function (){ - var ElectionVerification = Java.type("fe.utils.verification.ElectionVerification") - return new ElectionVerification() - } - """ - * def electionVerification = call getElectionVerification - - * def getConstants = - """ - function(){ - var Consants = Java.type("common.utils.Constants") - return new Consants() - } - """ - * def wait = - """ - function(secs) { - java.lang.Thread.sleep(secs*1000) - } - """ - - * def constants = call getConstants - - * karate.log('Backend started at ', backend.getPort()) - * def buffer = backend.getBuffer() - - * def stopBackend = function() { backend.stop() } - - # Shutdown backend automatically after the end of a scenario and feature - * configure afterScenario = stopBackend - * configure afterFeature = stopBackend diff --git a/tests/karate/src/test/java/fe/utils/android.feature b/tests/karate/src/test/java/fe/utils/android.feature index b09c9fa1c0..355e00f9fd 100644 --- a/tests/karate/src/test/java/fe/utils/android.feature +++ b/tests/karate/src/test/java/fe/utils/android.feature @@ -1,172 +1,11 @@ @ignore @report=false Feature: android page object - - Background: Android Preset - * configure driver = { type: 'android', webDriverPath : "/wd/hub", start: false, httpConfig : { readTimeout: 120000 }} - - * def capabilities = android.desiredConfig - # Replace the relative path to apk the absolute path - * capabilities.app = karate.toAbsolutePath('file:' + capabilities.app) - * def driverOptions = { webDriverSession: { desiredCapabilities : "#(capabilities)" } } - - # ================= Page Object Start ==================== - - # Tab buttons - * def tab_home_selector = '#com.github.dedis.popstellar:id/home_home_menu' - * def tab_connect_selector = '#com.github.dedis.popstellar:id/home_connect_menu' - * def launch_selector = '#com.github.dedis.popstellar:id/home_launch_menu' - * def tab_wallet_selector = '#com.github.dedis.popstellar:id/home_wallet_menu' - - # Launch tab - * def tab_launch_lao_name_selector = '#com.github.dedis.popstellar:id/entry_box_launch' - * def tab_launch_create_lao_selector = '#com.github.dedis.popstellar:id/button_launch' - - # Wallet tab - * def tab_wallet_new_wallet_selector = '#com.github.dedis.popstellar:id/button_new_wallet' - * def tab_wallet_confirm_selector = '#com.github.dedis.popstellar:id/button_confirm_seed' - - # Lao Event List - * def add_event_selector = '#com.github.dedis.popstellar:id/add_event' - * def add_roll_call_selector = '#com.github.dedis.popstellar:id/add_roll_call' - * def roll_call_title_selector = '#com.github.dedis.popstellar:id/roll_call_title_text' - * def roll_call_open_selector = '#com.github.dedis.popstellar:id/roll_call_open' - * def roll_call_confirm_selector = '#com.github.dedis.popstellar:id/roll_call_confirm' - * def roll_call_close_confirm_selector = '#com.github.dedis.popstellar:id/add_attendee_confirm' - * def event_name_selector = '#com.github.dedis.popstellar:id/event_card_text_view' - - # Roll Call Screen - * def roll_call_action_selector = '#com.github.dedis.popstellar:id/roll_call_management_button' - * def roll_call_close_selector = '#com.github.dedis.popstellar:id/add_attendee_confirm' - * def roll_call_manual_selector = '#com.github.dedis.popstellar:id/permission_manual_rc' - * def allow_camera_selector = '#com.github.dedis.popstellar:id/allow_camera_button' - * def manual_add_text_selector = '#com.github.dedis.popstellar:id/manual_add_edit_text' - * def manual_add_confirm_selector = '#com.github.dedis.popstellar:id/manual_add_confirm' - - # Election Screen - * def add_election_selector = '#com.github.dedis.popstellar:id/add_election' - * def election_name_selector = '#com.github.dedis.popstellar:id/election_setup_name' - * def election_question_selector = '#com.github.dedis.popstellar:id/election_question' - * def election_confirm_selector = '#com.github.dedis.popstellar:id/election_submit_button' - * def election_ballot_selector_1 = '#com.github.dedis.popstellar:id/new_ballot_option_text' - # This relies on the fact that the ballot 1 has already been modified with an input, - # which leaves the second ballot option the only one with the hint text - * def election_ballot_selector_2 = '//*[@text="ballot option"]' - * def election_management_selector = '#com.github.dedis.popstellar:id/election_management_button' - * def election_action_selector = '#com.github.dedis.popstellar:id/election_action_button' - - # Cast vote screen - * def cast_vote_ballot_selector_2 = '//*[@text="choice 2"]' - * def cast_vote_button_selector = '#com.github.dedis.popstellar:id/cast_vote_button' - - @name=basic_setup - Scenario: Setup connection to the backend and complete wallet initialization - Given driver driverOptions - - # Create and import mock backend - * call read('classpath:fe/net/mockBackend.feature') - * def backendURL = 'ws://10.0.2.2:' + backend.getPort() - # Import message filters - * call read('classpath:common/net/filters.feature') - - # As the settings tab does not have an id, this is how we click on it. - # If this breaks, use this code to log the page hierarchy : - # karate.log(driver.getHttp().path("source").get().value) - And click('//*[@content-desc="More options"]') - * click('#com.github.dedis.popstellar:id/title') - - # Input the mock backend url and connect to it - * input('#com.github.dedis.popstellar:id/entry_box_server_url', backendURL) - * click('#com.github.dedis.popstellar:id/button_apply') - * match backend.waitForConnection(5000) == true - - # Initialize wallet - * click(tab_wallet_selector) - * click(tab_wallet_new_wallet_selector) - * click(tab_wallet_confirm_selector) - * dialog(true) - - * retry(5,1000).click(launch_selector) - - # Roll call create android procedure - @name=create_roll_call - Scenario: Creates a roll call for an already created LAO - When click(add_event_selector) - And click(add_roll_call_selector) - - # Provide roll call information - And input(roll_call_title_selector, constants.RC_NAME) - - # Roll call open android procedure - @name=open_roll_call - Scenario: Opens the created roll-call - * click(event_name_selector) - * backend.clearBuffer() - * click(roll_call_action_selector) - - @name=close_roll_call - Scenario: Closes a roll call with only the organizer attending - # Close roll call - * retry(5,200).click(roll_call_close_selector) - * backend.clearBuffer() - * dialog(true) - - - @name=close_roll_call_w_attendees - Scenario: Closes a roll call with 2 attendees and the organizer - # Add attendees - * input(manual_add_text_selector, token1) - * click(manual_add_confirm_selector) - * input(manual_add_text_selector, token2) - * click(manual_add_confirm_selector) - * backend.clearBuffer() - - # wait for popup - * wait(3) - # Close roll call - * click(roll_call_close_selector) - * dialog(true) - - # Roll call open android procedure - @name=reopen_roll_call - Scenario: reopens the created roll-call - * click(event_name_selector) - * backend.clearBuffer() - * click(roll_call_action_selector) - - # Election setup android procedure - @name=setup_election - Scenario: Create election with 1 question and 2 ballots - * retry(5, 1000).click(add_event_selector) - * click(add_election_selector) - * input(election_name_selector, constants.ELECTION_NAME) - * input(election_question_selector, constants.QUESTION_CONTENT) - * input(election_ballot_selector_1, constants.BALLOT_1) - * input(election_ballot_selector_2, constants.BALLOT_2) - * backend.clearBuffer() - * click(election_confirm_selector) - - # Election open android procedure - @name=open_election - Scenario: Open election - * click(event_name_selector) - * backend.clearBuffer() - * click(election_management_selector) - * retry(5,1000).dialog(true) - * wait(1) - - # Election cast vote android procedure - @name=cast_vote - Scenario: Cast a vote for the second ballot - * click(election_action_selector) - * click(cast_vote_ballot_selector_2) - * backend.clearBuffer() - * click(cast_vote_button_selector) - * wait(5) - - @name=end_election - Scenario: End an election - * click(event_name_selector) - * click(election_management_selector) - * backend.clearBuffer() - * retry(5,1000).dialog(true) - * wait(1) + Background: + # Wallet screen + * def wallet_button_empty_ok = '//*[@text="OK"]' + * def wallet_seed_wallet_text = '#com.github.dedis.popstellar:id/seed_wallet_text' + + @name=open_app + Scenario: + Given driver webDriverOptions + Then waitFor(wallet_button_empty_ok).click() diff --git a/tests/karate/src/test/java/fe/utils/constants.feature b/tests/karate/src/test/java/fe/utils/constants.feature new file mode 100644 index 0000000000..f78a8e0661 --- /dev/null +++ b/tests/karate/src/test/java/fe/utils/constants.feature @@ -0,0 +1,5 @@ +@ignore @report=false +Feature: Constants + Scenario: Creates constants that will be used by other features + * def PLATFORM_FEATURES = 'classpath:fe/utils/platform.feature' + * def OPEN_APP = 'open_app' diff --git a/tests/karate/src/test/java/fe/utils/platform.feature b/tests/karate/src/test/java/fe/utils/platform.feature new file mode 100644 index 0000000000..b66e80a9df --- /dev/null +++ b/tests/karate/src/test/java/fe/utils/platform.feature @@ -0,0 +1,7 @@ +@ignore @report=false +Feature: current env page object + Scenario: + * def page_object = 'classpath:fe/utils/.feature@name=' + * replace page_object.env = karate.env + * replace page_object.name = name + * call read(page_object) diff --git a/tests/karate/src/test/java/fe/utils/simpleScenarios.feature b/tests/karate/src/test/java/fe/utils/simpleScenarios.feature deleted file mode 100644 index e5cf8d2fca..0000000000 --- a/tests/karate/src/test/java/fe/utils/simpleScenarios.feature +++ /dev/null @@ -1,60 +0,0 @@ -@ignore @report=false @env=android,web -Feature: Simple Scenarios - - @name=basic_setup - Scenario: Basic setup - * def page_object = 'classpath:fe/utils/.feature@name=basic_setup' - * replace page_object.env = karate.env - * call read(page_object) - - @name=create_lao - Scenario: Create a LAO send the right messages to the backend - Given call read('classpath:fe/utils/simpleScenarios.feature@name=basic_setup') - * backend.setLaoCreateMode() - * input(tab_launch_lao_name_selector, 'Lao Name') - And click(tab_launch_create_lao_selector) - - @name=create_roll_call - Scenario: Create a roll-call and everything needed before - Given call read('classpath:fe/utils/simpleScenarios.feature@name=create_lao') - * def rc_page_object = 'classpath:fe/utils/.feature@name=create_roll_call' - * replace rc_page_object.env = karate.env - * call read(rc_page_object) - * backend.clearBuffer() - * backend.setValidBroadcastMode() - And click(roll_call_confirm_selector) - - @name=open_roll_call - Scenario: Open a roll call - Given call read('classpath:fe/utils/simpleScenarios.feature@name=create_roll_call') - * def rc_page_object = 'classpath:fe/utils/.feature@name=open_roll_call' - * replace rc_page_object.env = karate.env - * call read(rc_page_object) - - @name=close_roll_call - Scenario: Close a roll-call - Given call read('classpath:fe/utils/simpleScenarios.feature@name=open_roll_call') - * def rc_page_object = 'classpath:fe/utils/.feature@name=close_roll_call' - * replace rc_page_object.env = karate.env - * call read(rc_page_object) - - @name=setup_election - Scenario: Setup an election - Given call read('classpath:fe/utils/simpleScenarios.feature@name=close_roll_call') - * def election_page_object = 'classpath:fe/utils/.feature@name=setup_election' - * replace election_page_object.env = karate.env - * call read(election_page_object) - - @name=open_election - Scenario: Setup an election - Given call read('classpath:fe/utils/simpleScenarios.feature@name=setup_election') - * def election_page_object = 'classpath:fe/utils/.feature@name=open_election' - * replace election_page_object.env = karate.env - * call read(election_page_object) - - @name=cast_vote - Scenario: Setup an election - Given call read('classpath:fe/utils/simpleScenarios.feature@name=open_election') - * def election_page_object = 'classpath:fe/utils/.feature@name=cast_vote' - * replace election_page_object.env = karate.env - * call read(election_page_object) diff --git a/tests/karate/src/test/java/fe/utils/verification/ElectionVerification.java b/tests/karate/src/test/java/fe/utils/verification/ElectionVerification.java deleted file mode 100644 index daebf34aa6..0000000000 --- a/tests/karate/src/test/java/fe/utils/verification/ElectionVerification.java +++ /dev/null @@ -1,141 +0,0 @@ -package fe.utils.verification; - -import be.utils.Hash; -import com.intuit.karate.Json; -import com.intuit.karate.Logger; -import common.utils.Constants; - -import java.security.NoSuchAlgorithmException; -import java.util.List; - -import static common.utils.Constants.*; -import static fe.utils.verification.VerificationUtils.getMsgDataJson; -import static fe.utils.verification.VerificationUtils.getStringFromIntegerField; - -/** This class contains functions used to test fields specific to Roll-Call */ -public class ElectionVerification { - private static final Logger logger = new Logger(ElectionVerification.class.getSimpleName()); - private Constants constants = new Constants(); - - /** - * Verifies that the election id is coherently computed - * @param message the network message - * @return true if the computed election id matches what is expected - */ - public boolean verifyElectionId(String message) { - Json setupMessageJson = getMsgDataJson(message); - - String electionId = setupMessageJson.get(ID); - String createdAt = getStringFromIntegerField(setupMessageJson, CREATED_AT); - String laoId = setupMessageJson.get(LAO); - String electionName = setupMessageJson.get(NAME); - - try { - return electionId.equals( - Hash.hash( - "Election".getBytes(), - laoId.getBytes(), - createdAt.getBytes(), - electionName.getBytes())); - } catch (NoSuchAlgorithmException e) { - logger.info("verification failed with error: " + e); - return false; - } - } - - /** - * Verifies that the question id is coherently computed - * @param message the network message - * @return true if the computed question id matches what is expected - */ - public boolean verifyQuestionId(String message){ - Json setupMessageJson = getMsgDataJson(message); - Json questionJson = getElectionQuestion(message); - - String electionId = setupMessageJson.get(ID); - String questionId = questionJson.get(ID); - String question = questionJson.get(QUESTION); - - try { - return questionId.equals( - Hash.hash( - "Question".getBytes(), - electionId.getBytes(), - question.getBytes())); - } catch (NoSuchAlgorithmException e) { - logger.info("verification failed with error: " + e); - return false; - } - } - - /** - * Verifies that the vote id is coherently computed - * @param message the network message - * @param index the index of the ballot that was selected - * @return true if the computed question id matches what is expected - */ - public boolean verifyVoteId(String message, int index){ - Json voteMessageJson = getMsgDataJson(message); - Json voteJson = getVotes(message); - - String electionId = voteMessageJson.get(constants.ELECTION); - String questionId = voteJson.get(QUESTION); - String voteId = voteJson.get(ID); - String vote = getStringFromIntegerField(voteJson, VOTE); - - try { - return voteId.equals( - Hash.hash( - "Vote".getBytes(), - electionId.getBytes(), - questionId.getBytes(), - vote.getBytes())); - } catch (NoSuchAlgorithmException e) { - logger.info("verification failed with error: " + e); - return false; - } - } - - public String getQuestionContent(String message){ - Json questionJson = getElectionQuestion(message); - return questionJson.get(QUESTION); - } - - public String getVotingMethod(String message){ - Json questionJson = getElectionQuestion(message); - return questionJson.get(VOTING_METHOD); - } - - public String getBallotOption(String message, int index){ - Json questionJson = getElectionQuestion(message); - List ballots = questionJson.get(BALLOT_OPTIONS); - return ballots.get(index); - } - - /** - * Gets the vote field - * @param message an element of the "votes" field array of a cast vote message - * @return the "vote" field of the message in argument - */ - public String getVote(String message){ - Json votes = getVotes(message); - return getStringFromIntegerField(votes, VOTE); - } - - private Json getElectionQuestion(String message){ - Json setupMessageJson = getMsgDataJson(message); - List questionArray = setupMessageJson.get(QUESTIONS); - return Json.of(questionArray.get(0)); - } - - /** - * gets the first element of the "votes" field of a cast vote network message - * @param message the network message - * @return the first element of the "votes" field of a cast vote network message - */ - private Json getVotes(String message){ - Json msgJson = getMsgDataJson(message); - List questionArray = msgJson.get(VOTES); - return Json.of(questionArray.get(0)); - } -} diff --git a/tests/karate/src/test/java/fe/utils/verification/MessageVerification.java b/tests/karate/src/test/java/fe/utils/verification/MessageVerification.java deleted file mode 100644 index 2081d8bfb2..0000000000 --- a/tests/karate/src/test/java/fe/utils/verification/MessageVerification.java +++ /dev/null @@ -1,66 +0,0 @@ -package fe.utils.verification; - -import be.utils.Hash; -import com.google.crypto.tink.PublicKeyVerify; -import com.google.crypto.tink.subtle.Ed25519Verify; -import com.intuit.karate.Json; -import com.intuit.karate.Logger; - -import java.security.GeneralSecurityException; -import java.security.NoSuchAlgorithmException; - -import static common.utils.Constants.*; -import static common.utils.Base64Utils.convertB64URLToByteArray; - -/** - * This class contains functions useful to test message fields for several kind of high level messages - */ -public class MessageVerification { - private final static Logger logger = new Logger(MessageVerification.class.getSimpleName()); - - /** - * Verify the message_id of a network message - * @param message the network message - * @return true if the computed message_id matches the one provided in Json - */ - public boolean verifyMessageIdField(String message) { - Json messageFieldJson = VerificationUtils.getMessageFieldFromMessage(message); - - String data = messageFieldJson.get(DATA); - String signature = messageFieldJson.get(SIGNATURE); - String msgId = messageFieldJson.get(MESSAGE_ID); - try { - return msgId.equals(Hash.hash(data.getBytes(), signature.getBytes())); - } catch (NoSuchAlgorithmException e) { - logger.info("verification failed with error: " + e); - return false; - } - } - - /** - * Verify the signature of a network message - * @param message the "message" field of the network message - * @return true if signature field of the message matches the sender and data - */ - public boolean verifyMessageSignature(String message) { - Json messageFieldJson = VerificationUtils.getMessageFieldFromMessage(message); - - String senderB64 = messageFieldJson.get(SENDER); - String signatureB64 = messageFieldJson.get(SIGNATURE); - String dataB64 = messageFieldJson.get(DATA); - - byte[] sender = convertB64URLToByteArray(senderB64); - byte[] signature = convertB64URLToByteArray(signatureB64); - byte[] data = convertB64URLToByteArray(dataB64); - - PublicKeyVerify verify = new Ed25519Verify(sender); - - try { - verify.verify(signature, data); - return true; - } catch (GeneralSecurityException e) { - logger.info("verification failed with error: " + e); - return false; - } - } -} diff --git a/tests/karate/src/test/java/fe/utils/verification/RollCallVerification.java b/tests/karate/src/test/java/fe/utils/verification/RollCallVerification.java deleted file mode 100644 index e790fdd5c7..0000000000 --- a/tests/karate/src/test/java/fe/utils/verification/RollCallVerification.java +++ /dev/null @@ -1,115 +0,0 @@ -package fe.utils.verification; - -import be.utils.Hash; -import com.intuit.karate.Json; -import com.intuit.karate.Logger; - -import java.security.NoSuchAlgorithmException; -import java.util.List; - -import static common.utils.Constants.*; -import static common.utils.JsonUtils.getJSON; -import static fe.utils.verification.VerificationUtils.getMsgDataJson; -import static fe.utils.verification.VerificationUtils.getStringFromIntegerField; - -/** This class contains functions used to test fields specific to Roll-Call */ -public class RollCallVerification { - private static final Logger logger = new Logger(RollCallVerification.class.getSimpleName()); - - /** - * Verifies that the roll call id is computed as expected - * - * @param message the message sent over the network - * @return true if the roll call id field matches expectations - */ - public boolean verifyRollCallId(String message) { - String laoId = getLaoId(message); - Json createMessageJson = getMsgDataJson(message); - - return verifyRollCallId(createMessageJson, laoId); - } - - /** - * Verifies that the roll call open update_id field is valid - * - * @param message the message sent over the network - * @return true if the update_id field match expectations - */ - public boolean verifyRollCallUpdateId(String message, String action) { - String laoId = getLaoId(message); - Json msgDataJson = getMsgDataJson(message); - return verifyUpdateId(msgDataJson, laoId, action); - } - - /** - * Verifies the presence of the attendees in the network message and that the number of attendees - * implies the presence of the organizer - * - * @param message the network message - * @param attendees the attendees added - * @return true if every specified attendees is in the message and if the number of attendees in - * the message = number of specified attendees + 1 (for the organizer) - */ - public boolean verifyAttendeesPresence(String message, String... attendees) { - Json msgDataJson = getMsgDataJson(message); - List msgAttendees = msgDataJson.get(ATTENDEES); - logger.info("Nbr attendees " + attendees.length + " message " + msgAttendees.toString()); - for (String attendee : attendees) { - if (!msgAttendees.contains(attendee)) { - return false; - } - } - // The attendee list should be the organizer and all added attendees - return attendees.length + 1 == msgAttendees.size(); - } - - ////////////////////// Utils ////////////////////// - - private boolean verifyRollCallId(Json createMessageJson, String laoId) { - String rcId = createMessageJson.get(ID); - String creation = getStringFromIntegerField(createMessageJson, CREATION); - String rcName = createMessageJson.get(NAME); - - try { - return rcId.equals( - Hash.hash( - "R".getBytes(), laoId.getBytes(), creation.getBytes(), rcName.getBytes())); - } catch (NoSuchAlgorithmException e) { - logger.info("verification failed with error: " + e); - return false; - } - } - - private String getLaoId(String message) { - Json paramsFieldJson = getJSON(Json.of(message), PARAMS); - String channel = paramsFieldJson.get(CHANNEL); - - // The laoId is the channel without leading /root/ and end \ characters - return channel.replace("/root/", "").replace("\\", ""); - } - - /** - * Verifies the update_id of a roll-call message - * - * @param rollCallMessageJson the message_data of the roll-call message - * @param laoId the laoId of the LAO in which the roll-call is taking place - * @param action the roll call action (e.g. open) - * @return true if the computed id matches the one provided in the message_data - */ - private boolean verifyUpdateId(Json rollCallMessageJson, String laoId, String action) { - String referenceKey = action.equals(CLOSE_STATIC) ? CLOSES : OPENS; - String timeKey = action.equals(CLOSE_STATIC) ? CLOSED_AT : OPENED_AT; - String updateId = rollCallMessageJson.get(UPDATE_ID); - String reference = rollCallMessageJson.get(referenceKey); - String time = getStringFromIntegerField(rollCallMessageJson, timeKey); - - try { - return updateId.equals( - Hash.hash( - "R".getBytes(), laoId.getBytes(), reference.getBytes(), time.getBytes())); - } catch (NoSuchAlgorithmException e) { - logger.info("verification failed with error: " + e); - return false; - } - } -} diff --git a/tests/karate/src/test/java/fe/utils/verification/VerificationUtils.java b/tests/karate/src/test/java/fe/utils/verification/VerificationUtils.java deleted file mode 100644 index d9b670021c..0000000000 --- a/tests/karate/src/test/java/fe/utils/verification/VerificationUtils.java +++ /dev/null @@ -1,71 +0,0 @@ -package fe.utils.verification; - -import com.intuit.karate.Json; -import common.utils.Base64Utils; -import common.utils.JsonUtils; - -import static common.utils.Constants.*; - -/** - * This class contains useful utils functions for the verification process - */ -public class VerificationUtils { - - /** - * Returns the Json object containing the "message" field of the provided network message - * @param message the network message - * @return the Json object containing the "message" field - */ - public static Json getMessageFieldFromMessage(String message){ - Json jsonMessage = Json.of(message); - Json paramFieldJson = JsonUtils.getJSON(jsonMessage, PARAMS); - return Json.of(paramFieldJson.get(MESSAGE)); - } - - /** - * Returns the "data" field of the provided network message - * @param message the network message - * @return the "data" field in base64 format - */ - public static String getDataFieldFromMessage(String message){ - Json messageField = getMessageFieldFromMessage(message); - return messageField.get(DATA); - } - - /** - * Returns the Json object containing the decoded "data" field of the provided network message - * @param message the network message - * @return the Json object containing the decoded "data" field - */ - public static Json getMsgDataJson(String message){ - String b64Data = getDataFieldFromMessage(message); - String data = new String(Base64Utils.convertB64URLToByteArray(b64Data)); - return Json.of(data); - } - - public String getObject(String message){ - Json data = getMsgDataJson(message); - return data.get(OBJECT); - } - - public String getAction(String message){ - Json data = getMsgDataJson(message); - return data.get(ACTION); - } - - public String getVersion(String message){ - Json data = getMsgDataJson(message); - return data.get(VERSION); - } - - public String getName(String message){ - Json data = getMsgDataJson(message); - return data.get(NAME); - } - - /** Because of internal type used by karate, doing casting in 2 steps is required */ - public static String getStringFromIntegerField(Json json, String key) { - Integer intTemp = json.get(key); - return String.valueOf(intTemp); - } -} diff --git a/tests/karate/src/test/java/fe/utils/web.feature b/tests/karate/src/test/java/fe/utils/web.feature index 69dd176a99..0935e23a4e 100644 --- a/tests/karate/src/test/java/fe/utils/web.feature +++ b/tests/karate/src/test/java/fe/utils/web.feature @@ -1,205 +1,13 @@ @ignore @report=false -Feature: web test - - Background: App Preset - * configure driver = { type: 'chrome', executable: 'C:/Program Files/Google/Chrome/Application/chrome.exe'} - #* configure driver = { type: 'chrome' } - * def driverOptions = karate.toAbsolutePath('file:../../fe1-web/web-build/index.html') - - # ================= Page Object Start ==================== - - # Introduction screen - * def exploring_selector = "[data-testid='exploring_selector']" - - #Home Screen - * def tab_connect_selector = '{}Connect' - * def launch_selector = "[data-testid='launch_selector']" - - # Launch screen - * def tab_launch_lao_name_selector = "input[data-testid='launch_organization_name_selector']" - * def backend_address_selector = "input[data-testid='launch_address_selector']" - * def tab_launch_create_lao_selector = "[data-testid='launch_launch_selector']" - - # Lao Event List - * def past_header_selector = '{^}Past' - * def add_event_selector = "[data-testid='create_event_selector']" - * def tab_events_selector = '{}Events' - * def roll_call_title_selector = "input[data-testid='roll_call_name_selector']" - * def roll_call_location_selector = "input[data-testid='roll_call_location_selector']" - * def roll_call_confirm_selector = "[data-testid='roll_call_confirm_selector']" - * def event_name_selector = "[data-testid='current_event_selector_0']" - - # Roll Call Screen - * def roll_call_option_selector = "[data-testid='roll_call_options']" - * def roll_call_stop_scanning_selector = "[data-testid='roll_call_open_stop_scanning']" - * def roll_call_manual_selector = "[data-testid='roll_call_open_add_manually']" - * def manual_add_description_selector = '{^}Enter token:' - * def manual_add_confirm_selector = '{}Add' - * def manual_add_done_selector = '{}Done' - - # Election - * def election_name_selector = "[data-testid='election_name_selector']" - * def election_question_selector = "[data-testid='question_selector_0']" - * def election_ballot_selector_1 = "[data-testid='question_0_ballots_option_0_input']" - * def election_ballot_selector_2 = "[data-testid='question_0_ballots_option_1_input']" - * def election_confirm_selector = "[data-testid='election_confirm_selector']" - * def election_event_selector = "[data-testid='current_event_selector_0']" - * def election_option_selector = "[data-testid='election_option_selector']" - * def election_opened_option_selector = "[data-testid='election_opened_option_selector']" - - # Cast vote screen - * def cast_vote_button_selector = "[data-testid='election_vote_selector']" - * def cast_vote_ballot_selector_2 = "[data-testid='questions_0_ballots_option_1_checkbox']" - - @name=basic_setup - Scenario: Setup connection to the backend and complete on the home page - Given driver driverOptions - - # Create and import mock backend - And call read('classpath:fe/net/mockBackend.feature') - * def backendURL = 'ws://localhost:' + backend.getPort() - # Import message filters - And call read('classpath:common/net/filters.feature') - - # The default input function is not consistent and does not work every time. - # This replaces the input function with one that just tries again until it works. - * def input = - """ - function(selector, data) { - tries = 0 - while (driver.attribute(selector, "value") != data) { - if (tries++ >= max_input_retry) - throw "Could not input " + data + " - max number of retry reached." - driver.clear(selector) - driver.input(selector, data) - delay(10) - } - } - """ - - * click(exploring_selector) - # Click on the connect navigation item - * retry(5,1000).click(tab_connect_selector) - # Click on launch button - * click(launch_selector) - # Connect to the backend - * input(backend_address_selector, backendURL) - - # Roll call create web procedure - @name=create_roll_call - Scenario: Creates a roll call for an already created LAO - Given retry(10, 200).click(tab_events_selector) - And click(add_event_selector) - - # Clicking on Create Roll-Call. This is because it is (as of now) an actionSheet element which does not have an id - # If it breaks down, check that the name of the button has not changed, try to add more delay. Otherwise maybe karate - # added a way to directly do that after the time of our writing. - # - # script allows the evaluation of arbitrary javascript code and document.evaluate - # (https://developer.mozilla.org/en-US/docs/Web/API/Document/evaluate) allows the evaluation of an XPath expression. - # - # Somehow this turned out to work, at least if it was wrapped - # in a setTimeout which delays the execution of the script. - # The XPath selector is described here: https://stackoverflow.com/a/29289196/2897827 - * script("setTimeout(() => document.evaluate('//div[text()=\\'Create Roll-Call\\']', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue.click(), 1000)") - - # Provide roll call required information - And retry(5, 1000).input(roll_call_title_selector, constants.RC_NAME) - And input(roll_call_location_selector, 'EPFL') - - # Roll call open web procedure - @name=open_roll_call - Scenario: Opens the created roll-call - * retry(5,1000).click(event_name_selector) - * retry(5,1000).click(roll_call_option_selector) - * backend.clearBuffer() - * script("setTimeout(() => document.evaluate('//div[text()=\\'Open Roll-Call\\']', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue.click(), 500)") - - @name=close_roll_call - Scenario: Closes a roll call with only the organizer attending - * wait(1) - * retry(5,1000).click(roll_call_option_selector) - # We need to start scanning for the organizer token to be added - * script("setTimeout(() => document.evaluate('//div[text()=\\'Scan Attendees\\']', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue.click(), 1000)") - * retry(5,1000).click(roll_call_stop_scanning_selector) - * backend.clearBuffer() - * click(roll_call_option_selector) - * script("setTimeout(() => document.evaluate('//div[text()=\\'Close Roll-Call\\']', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue.click(), 900)") - # needed to work - * wait(2) - - @name=close_roll_call_w_attendees - Scenario: Closes a roll call with 2 attendees and the organizer - * wait(1) - * retry(5,1000).click(roll_call_option_selector) - # We need to start scanning for the organizer token to be added - * script("setTimeout(() => document.evaluate('//div[text()=\\'Scan Attendees\\']', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue.click(), 1000)") - * retry(5,1000).click(roll_call_manual_selector) - - # Add attendees - * below(manual_add_description_selector).input(token1) - * click(manual_add_confirm_selector) - * below(manual_add_description_selector).clear() - * below(manual_add_description_selector).input(token2) - * click(manual_add_confirm_selector) - * click(manual_add_done_selector) - * retry(5,1000).click(roll_call_stop_scanning_selector) - * backend.clearBuffer() - * click(roll_call_option_selector) - * script("setTimeout(() => document.evaluate('//div[text()=\\'Close Roll-Call\\']', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue.click(), 1000)") - # needed to work - * wait(2) - - @name=reopen_roll_call - Scenario: Reopen a closed roll call - * click(past_header_selector) - * retry(5,1000).click(event_name_selector) - * wait(1) - * click(roll_call_option_selector) - * backend.clearBuffer() - * script("setTimeout(() => document.evaluate('//div[text()=\\'Re-open Roll-Call\\']', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue.click(), 1000)") - * wait(2) - - # Election setup web procedure - @name=setup_election - Scenario: Create election with 1 question and 2 ballots - And click(add_event_selector) - * script("setTimeout(() => document.evaluate('//div[text()=\\'Create Election\\']', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue.click(), 1000)") - * wait(1) - * retry(5, 1000).input(election_name_selector, constants.ELECTION_NAME) - * input(election_question_selector, constants.QUESTION_CONTENT) - * input(election_ballot_selector_1, constants.BALLOT_1) - * input(election_ballot_selector_2, constants.BALLOT_2) - * backend.clearBuffer() - * click(election_confirm_selector) - - # Election open web procedure - @name=open_election - Scenario: Open election - * retry(5,1000).click(election_event_selector) - * retry(5,1000).click(election_option_selector) - * backend.clearBuffer() - * script("setTimeout(() => document.evaluate('//div[text()=\\'Open Election\\']', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue.click(), 1000)") - * wait(2) - - # Election cast vote web procedure - @name=cast_vote - Scenario: Cast a vote for the second ballot - * wait(1) - # Click on second ballot checkbox - * click(cast_vote_ballot_selector_2) - * wait(1) - * backend.clearBuffer() - * click(cast_vote_button_selector) - * wait(1) - - - # Election end web procedure - @name=end_election - Scenario: End an election - * wait(1) - * retry(5,1000).click(election_opened_option_selector) - * script("setTimeout(() => document.evaluate('//div[text()=\\'End Election and Tally Votes\\']', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue.click(), 1000)") - * backend.clearBuffer() - * wait(2) - +Feature: web page object + Background: + # Wallet screen + * def wallet_seed_wallet_text = "[data-testid='seed_wallet_text']" + + @name=open_app + Scenario: + Given driver webDriverOptions + Given driver 'about:blank' + And driver.dimensions = { left: 0, top: 0, width: screenWidth, height: screenHeight } + Then driver frontendURL + And delay(1000) diff --git a/tests/karate/src/test/java/karate-config.js b/tests/karate/src/test/java/karate-config.js index 542bca7b2f..65090d4374 100644 --- a/tests/karate/src/test/java/karate-config.js +++ b/tests/karate/src/test/java/karate-config.js @@ -32,27 +32,88 @@ function fn() { config.backendPath = 'server'; config.frontendWsURL = `ws://${config.host}:${config.frontendPort}/${config.frontendPath}`; config.backendWsURL = `ws://${config.host}:${config.backendPort}/${config.backendPath}`; - } else { - config.port = 9005; - config.timeout = 1000; + } else if (env === 'web') { + config.frontendURL = karate.properties['url'] || `file://${karate.toAbsolutePath('file:../../fe1-web/web-build/index.html')}`; + config.screenWidth = karate.properties['screenWidth'] || 1920; + config.screenHeight = karate.properties['screenHeight'] || 1080; - if (env === 'web') { - config.max_input_retry = 10; - } else if (env === 'android') { - const android = {}; - android["desiredConfig"] = { - "app" : "../../fe2-android/app/build/outputs/apk/debug/app-debug.apk", - "newCommandTimeout" : 1000, - "platformVersion" : "9.0", - "platformName" : "Android", - "connectHardwareKeyboard" : true, - "deviceName" : "emulator-5554", - "avd" : "Pixel_4_API_30", - "automationName" : "UiAutomator2", - "autoGrantPermissions" : true - } - config["android"] = android + let platform = karate.properties['platform'] || karate.os.type; + if (platform === 'macosx') { + platform = 'mac'; } + + const browser = karate.properties['browser'] || 'chrome'; + const browserOptions = { + chrome: { + type: 'chromedriver', + capabilities: { + alwaysMatch: { + 'platformName': platform, + 'appium:automationName': 'Chromium', + 'browserName': 'chrome' + } + } + }, + edge: { + type: 'chromedriver', + capabilities: { + alwaysMatch: { + 'platformName': platform, + 'appium:automationName': 'Chromium', + 'browserName': 'MicrosoftEdge' + } + } + }, + firefox: { + type: 'geckodriver', + capabilities: { + alwaysMatch: { + 'platformName': platform, + 'appium:automationName': 'Gecko', + 'browserName': 'firefox' + } + } + }, + safari: { + type: 'safaridriver', + capabilities: { + alwaysMatch: { + 'platformName': platform, + 'appium:automationName': 'Safari', + 'browserName': 'safari' + } + } + } + }; + + const { type, capabilities } = browserOptions[browser]; + + + karate.configure('driver', { type, port: 4723, webDriverPath : "/", start: false }); + config.webDriverOptions = { + webDriverSession: { + capabilities, + desiredCapabilities: {} + } + }; + } else if (env === 'android') { + karate.configure('driver', { type: 'android', webDriverPath : "/", start: false }); + const app = karate.properties['app'] || '../../fe2-android/app/build/outputs/apk/debug/app-debug.apk'; + config.webDriverOptions = { + webDriverSession: { + capabilities: { + alwaysMatch: { + 'appium:platformName': 'Android', + 'appium:automationName': 'uiautomator2', + 'appium:app': `${karate.toAbsolutePath(`file:${app}`)}`, + 'appium:autoGrantPermissions': true, + 'appium:avd': karate.properties['avd'], + } + }, + desiredCapabilities: { + } + } + }; } return config;