@suffolklitlab/alkiln 5.12.0 → 5.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/github_server.yml +56 -11
- package/.github/workflows/playground.yml +9 -4
- package/CHANGELOG.md +22 -0
- package/README.md +1 -1
- package/action.yml +1 -1
- package/docs/story_tables.md +1 -1
- package/lib/docassemble/docassemble_api_REST.js +67 -1
- package/lib/docassemble/docassemble_api_interface.js +179 -1
- package/lib/run_cucumber.js +5 -3
- package/lib/scope.js +79 -12
- package/lib/steps.js +129 -18
- package/package.json +3 -3
- package/setup.py +1 -1
- package/tests/log_codes/check_codes.sh +1 -1
|
@@ -43,18 +43,22 @@ jobs:
|
|
|
43
43
|
#### Developer note: You can probably leave out these specific
|
|
44
44
|
#### environment variables. You may not even need the `env`
|
|
45
45
|
#### keyword or values at all. See
|
|
46
|
-
#### https://suffolklitlab.org/
|
|
46
|
+
#### https://assemblyline.suffolklitlab.org/docs/alkiln/writing#env-vars
|
|
47
47
|
env:
|
|
48
48
|
SECRET_VAR1: secret-var1-value
|
|
49
49
|
SECRET_VAR2: secret-var2-value
|
|
50
50
|
SECRET_FOR_MISSING_FIELD: secret for missing field
|
|
51
|
+
INVALID_API_KEY: invalidAPIkey
|
|
52
|
+
EMPTY_STRING: ""
|
|
53
|
+
USER_NO_PERMISSIONS_EMAIL: alkiln@example.com
|
|
54
|
+
USER_NO_PERMISSIONS_PASSWORD: User123^
|
|
51
55
|
|
|
52
56
|
steps:
|
|
53
57
|
# Place the root directory in this branch to access
|
|
54
58
|
# relative paths to action files
|
|
55
59
|
- uses: actions/checkout@v4
|
|
56
60
|
|
|
57
|
-
- name: ALKiln - Start the isolated temporary docassemble server on GitHub
|
|
61
|
+
- name: "ALKiln - Start the isolated temporary docassemble server on GitHub"
|
|
58
62
|
id: github_server
|
|
59
63
|
#### Developer note: you'll need to replace `uses: ./action_for_github_server`
|
|
60
64
|
#### with the path to ALKiln’s repository name and branch name. For example,
|
|
@@ -71,13 +75,54 @@ jobs:
|
|
|
71
75
|
shell: bash
|
|
72
76
|
|
|
73
77
|
#### Developer note: You can probably leave this out
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
+
- name: "Add a low-level user and their API key (in an env var)"
|
|
79
|
+
env:
|
|
80
|
+
SERVER_URL: "${{ steps.github_server.outputs.SERVER_URL }}"
|
|
81
|
+
KEY: ${{ steps.github_server.outputs.DOCASSEMBLE_DEVELOPER_API_KEY }}
|
|
82
|
+
run: |
|
|
83
|
+
# Create user and their API key
|
|
84
|
+
|
|
85
|
+
# Install json processor
|
|
86
|
+
sudo apt-get install jq
|
|
87
|
+
|
|
88
|
+
# https://docassemble.org/docs/api.html#user_new
|
|
89
|
+
user_data=$(curl -X POST \
|
|
90
|
+
${{ env.SERVER_URL }}/api/user/new \
|
|
91
|
+
-H 'Content-Type: application/json' \
|
|
92
|
+
-d '{
|
|
93
|
+
"key": "${{ env.KEY }}",
|
|
94
|
+
"username": "${{ env.USER_NO_PERMISSIONS_EMAIL }}",
|
|
95
|
+
"password": "${{ env.USER_NO_PERMISSIONS_PASSWORD }}"
|
|
96
|
+
}')
|
|
97
|
+
echo "User data:"
|
|
98
|
+
echo "$user_data"
|
|
99
|
+
|
|
100
|
+
user_id=$(echo "$user_data" | jq '.user_id' -r)
|
|
101
|
+
|
|
102
|
+
# https://docassemble.org/docs/api.html#api_user_user_id_api_post
|
|
103
|
+
new_key_with_quotes=$(curl -X POST \
|
|
104
|
+
${{ env.SERVER_URL }}/api/user/"$user_id"/api \
|
|
105
|
+
-H 'Content-Type: application/json' \
|
|
106
|
+
-d '{
|
|
107
|
+
"key": "${{ env.KEY }}",
|
|
108
|
+
"name": "alkiln_key",
|
|
109
|
+
"method": "none"
|
|
110
|
+
}')
|
|
111
|
+
echo "new key (watch out, this contains actual quotation marks): $new_key_with_quotes"
|
|
112
|
+
no_end_quote="${new_key_with_quotes%\"}"
|
|
113
|
+
unquoted="${no_end_quote#\"}"
|
|
114
|
+
echo "unquoted: $unquoted"
|
|
115
|
+
|
|
116
|
+
echo "USER_NO_PERMISSIONS_API_KEY=$unquoted" >> "$GITHUB_ENV"
|
|
117
|
+
|
|
118
|
+
#### Developer note: You can probably leave this out
|
|
119
|
+
## Optional debugging to explore things like docker issues
|
|
120
|
+
#- name: Docker debug tmate session
|
|
121
|
+
# #if: ${{ failure() && github.event.inputs.enable_tmate == 'true' }}
|
|
122
|
+
# uses: mxschmitt/action-tmate@v3
|
|
78
123
|
|
|
79
124
|
#### Developer note: You can probably leave this out
|
|
80
|
-
- name:
|
|
125
|
+
- name: "Create other env vars for our own tests from server output"
|
|
81
126
|
run: |
|
|
82
127
|
echo "USER1_EMAIL=${{ steps.github_server.outputs.DA_ADMIN_EMAIL }}" >> $GITHUB_ENV
|
|
83
128
|
echo "USER1_PASSWORD=${{ steps.github_server.outputs.DA_ADMIN_PASSWORD }}" >> $GITHUB_ENV
|
|
@@ -103,17 +148,17 @@ jobs:
|
|
|
103
148
|
# Internal: docassemble changes sometimes break @a11y tests. That is
|
|
104
149
|
# a da problem, not ours. We will trigger these tests manually when we
|
|
105
150
|
# want to check up on this.
|
|
106
|
-
ALKILN_TAG_EXPRESSION: "${{ (github.event.inputs.tags && format('{0}', github.event.inputs.tags)) || '(not @
|
|
151
|
+
ALKILN_TAG_EXPRESSION: "${{ (github.event.inputs.tags && format('{0}', github.event.inputs.tags)) || '(not @error) and (not @temp_error)' }}"
|
|
107
152
|
with:
|
|
108
153
|
#### Developer note: Required inputs. See
|
|
109
|
-
#### https://suffolklitlab.org/
|
|
154
|
+
#### https://assemblyline.suffolklitlab.org/docs/alkiln/writing/#sandbox-inputs
|
|
110
155
|
SERVER_URL: "${{ steps.github_server.outputs.SERVER_URL }}"
|
|
111
156
|
DOCASSEMBLE_DEVELOPER_API_KEY: "${{ steps.github_server.outputs.DOCASSEMBLE_DEVELOPER_API_KEY }}"
|
|
112
157
|
INSTALL_METHOD: "server"
|
|
113
158
|
#### Developer note: You can probably leave the rest out
|
|
114
|
-
#### To learn more, see https://suffolklitlab.org/
|
|
159
|
+
#### To learn more, see https://assemblyline.suffolklitlab.org/docs/alkiln/writing/#optional-inputs
|
|
115
160
|
ALKILN_TAG_EXPRESSION: "${{ env.ALKILN_TAG_EXPRESSION }}"
|
|
116
|
-
ALKILN_VERSION:
|
|
161
|
+
ALKILN_VERSION: delete
|
|
117
162
|
|
|
118
163
|
#### Developer note: Example of making an issue when tests fail
|
|
119
164
|
#### that includes the text of the failure output file
|
|
@@ -43,6 +43,11 @@ jobs:
|
|
|
43
43
|
PLAYGROUND_ID: ${{ secrets.PLAYGROUND_ID }}
|
|
44
44
|
USER1_EMAIL: ${{ secrets.USER1_EMAIL }}
|
|
45
45
|
USER1_PASSWORD: ${{ secrets.USER1_PASSWORD }}
|
|
46
|
+
USER_NO_PERMISSIONS_EMAIL: ${{ secrets.USER_NO_PERMISSIONS_EMAIL }}
|
|
47
|
+
USER_NO_PERMISSIONS_PASSWORD: ${{ secrets.USER_NO_PERMISSIONS_PASSWORD }}
|
|
48
|
+
USER_NO_PERMISSIONS_API_KEY: ${{ secrets.USER_NO_PERMISSIONS_API_KEY }}
|
|
49
|
+
INVALID_API_KEY: invalidAPIkey
|
|
50
|
+
EMPTY_STRING: ""
|
|
46
51
|
SECRET_VAR1: secret-var1-value
|
|
47
52
|
SECRET_VAR2: secret-var2-value
|
|
48
53
|
SECRET_FOR_MISSING_FIELD: secret for missing field
|
|
@@ -61,17 +66,17 @@ jobs:
|
|
|
61
66
|
- uses: ./
|
|
62
67
|
#### Developer note: You can probably leave this out
|
|
63
68
|
env:
|
|
64
|
-
ALKILN_TAG_EXPRESSION: "${{ (github.event.inputs.tags && format('{0}', github.event.inputs.tags)) || '(not @
|
|
69
|
+
ALKILN_TAG_EXPRESSION: "${{ (github.event.inputs.tags && format('{0}', github.event.inputs.tags)) || '(not @error) and (not @temp_error)' }}"
|
|
65
70
|
with:
|
|
66
71
|
#### Developer note: Required ALKiln inputs. You can read more at
|
|
67
|
-
#### https://suffolklitlab.org/
|
|
72
|
+
#### https://assemblyline.suffolklitlab.org/docs/alkiln/writing/#githubNyou-inputs
|
|
68
73
|
SERVER_URL: "${{ env.SERVER_URL }}"
|
|
69
74
|
DOCASSEMBLE_DEVELOPER_API_KEY: "${{ env.DOCASSEMBLE_DEVELOPER_API_KEY }}"
|
|
70
|
-
#### Developer note: See https://suffolklitlab.org/
|
|
75
|
+
#### Developer note: See https://assemblyline.suffolklitlab.org/docs/alkiln/writing/#optional-inputs
|
|
71
76
|
#### You can probably leave this out
|
|
72
77
|
# Internal: docassemble changes sometimes break @a11y tests. That is
|
|
73
78
|
# a da problem, not ours. We will trigger these tests manually when we
|
|
74
79
|
# want to check up on this.
|
|
75
80
|
ALKILN_TAG_EXPRESSION: "${{ env.ALKILN_TAG_EXPRESSION }}"
|
|
76
81
|
#### Developer note: You can probably leave this out
|
|
77
|
-
ALKILN_VERSION:
|
|
82
|
+
ALKILN_VERSION: delete
|
package/CHANGELOG.md
CHANGED
|
@@ -45,6 +45,28 @@ Format:
|
|
|
45
45
|
|
|
46
46
|
<!-- ## [Unreleased] -->
|
|
47
47
|
|
|
48
|
+
## [5.13.0] - 2024-07-11
|
|
49
|
+
|
|
50
|
+
## Added
|
|
51
|
+
|
|
52
|
+
- Authors can now make sure that ALKiln deletes the interviews it creates for an ALKiln testing account. They can add a new (optional) value, an API key, to [the "sign in" Step](https://assemblyline.suffolklitlab.org/docs/alkiln/writing/#sign-in) and use the ["start interview" Step](https://assemblyline.suffolklitlab.org/docs/alkiln/writing/#start-interview) to go to the interview. The API key must belong to the user who signs in. Example: `Given I sign in with the email "<username env var>", the password "<password env var>", and the API key "<user API key>`. Admins can create an API key for a regular user with https://docassemble.org/docs/api.html#api_user_user_id_api_post. We will add this to the documentation for the "sign in" Step.
|
|
53
|
+
|
|
54
|
+
### Changed
|
|
55
|
+
|
|
56
|
+
- Update links in logs to new Suffolk docs pages
|
|
57
|
+
|
|
58
|
+
### Fixed
|
|
59
|
+
|
|
60
|
+
- Now ALKiln deletes interviews upon interview completion for [signed in users](https://assemblyline.suffolklitlab.org/docs/alkiln/writing/#sign-in) as long as the user has an API key. Also, the author must have used the ["start interview" Step](https://assemblyline.suffolklitlab.org/docs/alkiln/writing/#start-interview) to get to the interview. Admins can give an API key to a regular user with https://docassemble.org/docs/api.html#api_user_user_id_api_post. See [#831](https://github.com/SuffolkLITLab/ALKiln/issues/831).
|
|
61
|
+
|
|
62
|
+
### Internal
|
|
63
|
+
|
|
64
|
+
- Tests deleting interviews upon interview completion for signed in users.
|
|
65
|
+
- Switches to mostly using a lowest-access-level user account for actually running interviews.
|
|
66
|
+
- Switches to adding our own `users[0]` question because of changes in Assembly Line.
|
|
67
|
+
- Adds @temp_error to neutralize tests that are getting undesired errors right now, but which we anticipate being fixed soon. We want to ignore them for a short while. For example, an upstream change temporarily breaking some tests.
|
|
68
|
+
- Excludes docassemble/ALKiln files from log code checks.
|
|
69
|
+
|
|
48
70
|
## [5.12.0] - 2024-06-06
|
|
49
71
|
|
|
50
72
|
### Added
|
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ https://github.com/18F/open-source-guide/blob/18f-pages/pages/making-readmes-rea
|
|
|
12
12
|
|
|
13
13
|
## Use ALKiln
|
|
14
14
|
|
|
15
|
-
Read about how to use this framework in
|
|
15
|
+
Read about how to use [this framework in Assembly Line's documentation](https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln/intro).
|
|
16
16
|
|
|
17
17
|
If you have questions or need to report a bug, [start a new issue](https://github.com/SuffolkLITLab/ALKiln/issues/new).
|
|
18
18
|
|
package/action.yml
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
name: "ALKiln: Automated docassemble tests"
|
|
2
|
-
description: "Automatically test any docassemble interview in a any branch on your GitHub repository whenever you commit or push. See https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/
|
|
2
|
+
description: "Automatically test any docassemble interview in a any branch on your GitHub repository whenever you commit or push. See https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln/setup/#types."
|
|
3
3
|
|
|
4
4
|
inputs:
|
|
5
5
|
SERVER_URL:
|
package/docs/story_tables.md
CHANGED
|
@@ -4,7 +4,7 @@ The Story Table is a very flexible tool. It lets the author answer interview (fo
|
|
|
4
4
|
|
|
5
5
|
When the robot arrives on each interview page, it finds all the fields on the page and loops through the whole Story Table trying to match rows with fields.
|
|
6
6
|
|
|
7
|
-
Here's some [documentation on the use of Story Table rows](https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln
|
|
7
|
+
Here's some [documentation on the use of Story Table rows](https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln/writing#story-table).
|
|
8
8
|
|
|
9
9
|
## Tracking use of rows
|
|
10
10
|
|
|
@@ -31,7 +31,28 @@ da.get_dev_id = async function ( timeout ) {
|
|
|
31
31
|
}; // Ends da.get_dev_id()
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
da.get_api_key_info = async function ( timeout, { api_key = null, server_url = null }) {
|
|
35
|
+
/** Get information about an API key owned by the user of the given API key. */
|
|
36
|
+
let options = {
|
|
37
|
+
method: 'GET',
|
|
38
|
+
headers: {'X-API-Key': api_key},
|
|
39
|
+
url: `${ server_url }/api/user/api`,
|
|
40
|
+
params: {
|
|
41
|
+
api_key: api_key,
|
|
42
|
+
},
|
|
43
|
+
timeout: timeout,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
return await axios.request( options );
|
|
48
|
+
} catch (error) {
|
|
49
|
+
let response = error.response || { data: null }
|
|
50
|
+
throw {...error.toJSON(), data: response.data};
|
|
51
|
+
}
|
|
52
|
+
}; // Ends da.get_api_key_info()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
// node -e 'require("./lib/docassemble/docassemble_api_REST.js").delete_project()'
|
|
35
56
|
da.delete_project = async function ( timeout ) {
|
|
36
57
|
/* Delete a project on the user's da server. */
|
|
37
58
|
let options = {
|
|
@@ -53,6 +74,51 @@ da.delete_project = async function ( timeout ) {
|
|
|
53
74
|
}; // Ends da.delete_project()
|
|
54
75
|
|
|
55
76
|
|
|
77
|
+
// node -e 'require("./lib/docassemble/docassemble_api_REST.js").delete_interview(...)'
|
|
78
|
+
da.delete_interview = async function ( timeout, { session = null, api_key = null }) {
|
|
79
|
+
/* Delete an interview with the given session id. */
|
|
80
|
+
let options = {
|
|
81
|
+
method: 'DELETE',
|
|
82
|
+
headers: { 'X-API-Key': api_key },
|
|
83
|
+
// Can't use `api/session` that uses the API key to identify the user's
|
|
84
|
+
// interview sessions. The test may have signed in with an account
|
|
85
|
+
// that's different than the ALKiln dev account that sends this request.
|
|
86
|
+
url: `${ session_vars.get_da_server_url() }/api/user/interviews`,
|
|
87
|
+
params: { session },
|
|
88
|
+
timeout: timeout,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
return await axios.request( options );
|
|
93
|
+
} catch (error) {
|
|
94
|
+
let response = error.response || {data: null}
|
|
95
|
+
throw {...error.toJSON(), data: response.data};
|
|
96
|
+
}
|
|
97
|
+
}; // Ends da.delete_interview()
|
|
98
|
+
|
|
99
|
+
// node -e 'require("./lib/docassemble/docassemble_api_REST.js").get_interview(...)'
|
|
100
|
+
da.get_interview = async function ( timeout, { session = null, api_key = null }) {
|
|
101
|
+
/* Get a specific interview. */
|
|
102
|
+
let options = {
|
|
103
|
+
method: 'GET',
|
|
104
|
+
headers: { 'X-API-Key': api_key },
|
|
105
|
+
// Can't use `/api/user/interviews` that uses the API key to identify the user's
|
|
106
|
+
// interview sessions. The test may have signed in with an account
|
|
107
|
+
// that's different than the ALKiln dev account that sends this request.
|
|
108
|
+
url: `${ session_vars.get_da_server_url() }/api/user/interviews`,
|
|
109
|
+
params: { session },
|
|
110
|
+
timeout: timeout,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
return await axios.request( options );
|
|
115
|
+
} catch ( error ) {
|
|
116
|
+
let response = error.response || { data: null }
|
|
117
|
+
throw { ...error.toJSON(), data: response.data };
|
|
118
|
+
}
|
|
119
|
+
}; // Ends da.get_interview()
|
|
120
|
+
|
|
121
|
+
|
|
56
122
|
// node -e 'require("./lib/docassemble/docassemble_api_REST.js").__delete_projects_starting_with( "testingdocassembleALAutomatedTestingTestsmain", 1, 1 )'
|
|
57
123
|
da.__delete_projects_starting_with = async function ( base_name, starting_num=1, ending_num ) {
|
|
58
124
|
/** FOR DEVELOPMENT ONLY. Deletes a set of Projects that start with a shared
|
|
@@ -15,6 +15,9 @@ const time = require('../utils/time');
|
|
|
15
15
|
// to specific functions, which can run in a very small
|
|
16
16
|
// window.
|
|
17
17
|
|
|
18
|
+
// TODO: Move get_dev_id() and such into here instead of in
|
|
19
|
+
// docassemble_REST - args should be the same vals as the request expects
|
|
20
|
+
|
|
18
21
|
/** Notes:
|
|
19
22
|
* - Checking for a busy server is vulnerable to race conditions, but
|
|
20
23
|
* there's nothing we can do about that.
|
|
@@ -174,13 +177,74 @@ da_i.get_dev_id = async function ( dev_id_options ) {
|
|
|
174
177
|
return dev_id;
|
|
175
178
|
}; // Ends da_i.get_dev_id();
|
|
176
179
|
|
|
180
|
+
da_i.get_api_key_access_status = async function ({ timeout = null, api_key = null }) {
|
|
181
|
+
/** Check if the API key has minimal authorization on the server.
|
|
182
|
+
* See https://docassemble.org/docs/api.html#api_user_api_get.
|
|
183
|
+
*
|
|
184
|
+
* @param options {obj} Options for getting authorization of the API key
|
|
185
|
+
* @param options.api_key {str | null} Required. The API key to test.
|
|
186
|
+
* @param options.timeout {int | null} Optional. Max MS
|
|
187
|
+
* to allow for server request.
|
|
188
|
+
*/
|
|
189
|
+
|
|
190
|
+
let auth_data = {
|
|
191
|
+
codes: [ 1 ],
|
|
192
|
+
has_auth: null,
|
|
193
|
+
messages: [`API key authorization is not yet determined.`],
|
|
194
|
+
error: null
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Skip this for tests run from ALKilnInThePlayground
|
|
198
|
+
if ( session_vars.get_origin() === 'playground' ) {
|
|
199
|
+
auth_data.codes.push( 2 );
|
|
200
|
+
auth_data.messages.push( `API keys are unnecessary for tests run in the Playground.` );
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if ( typeof api_key !== `string` ) {
|
|
204
|
+
auth_data.codes.push( 3 );
|
|
205
|
+
auth_data.messages.push( `The given API key is not a string. Type: ${ typeof api_key }`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if ( auth_data.codes.length === 1 ) {
|
|
209
|
+
// Default timeouts
|
|
210
|
+
timeout = timeout || DEFAULT_MAX_REQUEST_MS;
|
|
211
|
+
try {
|
|
212
|
+
let response = await _da_REST.get_api_key_info( timeout, {
|
|
213
|
+
api_key,
|
|
214
|
+
server_url: session_vars.get_da_server_url(),
|
|
215
|
+
});
|
|
216
|
+
auth_data.has_auth = true;
|
|
217
|
+
auth_data.codes = [ 6 ];
|
|
218
|
+
auth_data.messages = [ `This API key has at least basic authorization on this server.` ];
|
|
219
|
+
|
|
220
|
+
} catch ( error ) {
|
|
221
|
+
auth_data.error = error;
|
|
222
|
+
if ( error.status === 403 ) {
|
|
223
|
+
auth_data.has_auth = false;
|
|
224
|
+
auth_data.codes = [ 4 ];
|
|
225
|
+
auth_data.messages = [ `This API is unverified on this server. It is probably missing or has been deleted.` ];
|
|
226
|
+
} else {
|
|
227
|
+
// Note on 404, non-existent key: 404 == 403 - Since we're using the
|
|
228
|
+
// key itself to get its own info, if it doesn't exist, we'll get 403
|
|
229
|
+
auth_data.codes = [ 5 ];
|
|
230
|
+
auth_data.messages = [ `This API key threw a non-403 error.` ];
|
|
231
|
+
} // ends error status
|
|
232
|
+
} // ends try
|
|
233
|
+
} // ends auth_data.codes only has the first code
|
|
234
|
+
|
|
235
|
+
log.debug({ type: `da api info`, code: `ALK0202`,
|
|
236
|
+
pre: `API key authentication info`, data: auth_data });
|
|
237
|
+
|
|
238
|
+
return auth_data;
|
|
239
|
+
}; // Ends da_i.get_api_key_access_status()
|
|
240
|
+
|
|
177
241
|
// node -e 'require("./lib/docassemble/docassemble_api_interface.js").delete_project( "create1" )'
|
|
178
242
|
da_i.delete_project = async function ( delete_options ) {
|
|
179
243
|
/** Delete a project on the user's da server. Do we need to watch
|
|
180
244
|
* for a timeout? */
|
|
181
245
|
|
|
182
246
|
// Defaults
|
|
183
|
-
let { timeout } = delete_options ||
|
|
247
|
+
let { timeout } = delete_options || { timeout: DEFAULT_MAX_REQUEST_MS };
|
|
184
248
|
|
|
185
249
|
let project_name = session_vars.get_project_name()
|
|
186
250
|
log.info({ type: `da api`, code: `ALK0170`, pre: `Trying to delete Project ${ project_name }` });
|
|
@@ -196,3 +260,117 @@ da_i.delete_project = async function ( delete_options ) {
|
|
|
196
260
|
}
|
|
197
261
|
|
|
198
262
|
}; // Ends da_i.delete_project()
|
|
263
|
+
|
|
264
|
+
// node -e 'require("./lib/docassemble/docassemble_api_interface.js").quietly_try_to_delete_interviews(...)'
|
|
265
|
+
da_i.quietly_try_to_delete_interviews = async function ({ interviews = {}, api_keys = [], options = {} }) {
|
|
266
|
+
/** Try to delete the given interviews. Fail silently. Example failure: the test
|
|
267
|
+
* did not log in any users and the testing account's role prevents
|
|
268
|
+
* access to other user's interviews. Do we need to watch for a timeout? */
|
|
269
|
+
|
|
270
|
+
// Defaults
|
|
271
|
+
let { timeout } = options || { timeout: DEFAULT_MAX_REQUEST_MS };
|
|
272
|
+
|
|
273
|
+
let responses = [];
|
|
274
|
+
for ( let id in interviews ) {
|
|
275
|
+
let interview = interviews[ id ];
|
|
276
|
+
if ( interview && interview.uid ) {
|
|
277
|
+
let sub_responses = await da_i.quietly_delete_one_interview( timeout, {
|
|
278
|
+
interview,
|
|
279
|
+
api_keys
|
|
280
|
+
});
|
|
281
|
+
responses.push( sub_responses );
|
|
282
|
+
} else {
|
|
283
|
+
log.debug({ type: `da api info`, code: `ALK0191`, pre: `There is no interview to delete.` });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return responses;
|
|
287
|
+
}; // Ends da_i.quietly_try_to_delete_interviews()
|
|
288
|
+
|
|
289
|
+
// node -e 'require("./lib/docassemble/docassemble_api_interface.js").quietly_delete_one_interview(...)'
|
|
290
|
+
da_i.quietly_delete_one_interview = async function ( timeout, { interview = {}, api_keys = [] }) {
|
|
291
|
+
/** Try to delete an interview using all available API keys. Fail silently.
|
|
292
|
+
* Example failure: the test did not log in any users and the testing
|
|
293
|
+
* account's role prevents access to other user's interviews. */
|
|
294
|
+
|
|
295
|
+
let responses = [];
|
|
296
|
+
for ( let api_key of api_keys ) {
|
|
297
|
+
try {
|
|
298
|
+
log.debug({ type: `da api info`, code: `ALK0188`,
|
|
299
|
+
pre: `Trying to delete the interview with the id ${ interview.uid } and an API key.`
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Actual work:
|
|
303
|
+
let response = await _da_REST.delete_interview( timeout, {
|
|
304
|
+
session: interview.uid,
|
|
305
|
+
api_key
|
|
306
|
+
});
|
|
307
|
+
responses.push( response );
|
|
308
|
+
|
|
309
|
+
log.debug({ type: `da api info`, code: `ALK0189`,
|
|
310
|
+
pre: `No error when trying to delete interview ${ interview.uid } at ${ interview.i }.`
|
|
311
|
+
});
|
|
312
|
+
} catch ( error ) {
|
|
313
|
+
error.interview = interview;
|
|
314
|
+
let msg = `Error when deleting the interview. See https://docassemble.org/docs/api.html#session_delete.`
|
|
315
|
+
log.debug({ type: `da api info`, code: `ALK0190`, pre: msg, data: error });
|
|
316
|
+
}
|
|
317
|
+
} // ends for api keys
|
|
318
|
+
return responses;
|
|
319
|
+
}; // Ends da_i.quietly_delete_one_interview()
|
|
320
|
+
|
|
321
|
+
// node -e 'require("./lib/docassemble/docassemble_api_interface.js").get_interviews(...)'
|
|
322
|
+
da_i.get_interviews = async function ({ interviews = {}, api_keys = [], options = {} }) {
|
|
323
|
+
/** Return an object of interviews that exist on the server
|
|
324
|
+
* that match the given interviews. Keys are the session ids. */
|
|
325
|
+
|
|
326
|
+
let fetched_interviews = {};
|
|
327
|
+
if ( interviews ) {
|
|
328
|
+
for ( let id in interviews ) {
|
|
329
|
+
let interview = interviews[ id ];
|
|
330
|
+
let fetched = await da_i.get_interview({ interview, api_keys, options });
|
|
331
|
+
if ( fetched && fetched.session ) {
|
|
332
|
+
fetched_interviews[ fetched.session ] = fetched;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
} // ends if interviews
|
|
336
|
+
|
|
337
|
+
return fetched_interviews;
|
|
338
|
+
}; // Ends da_i.get_interviews()
|
|
339
|
+
|
|
340
|
+
// node -e 'require("./lib/docassemble/docassemble_api_interface.js").get_interview(...)'
|
|
341
|
+
da_i.get_interview = async function ({ interview = {}, api_keys = [], options = {} }) {
|
|
342
|
+
/** Try to get an interview from the server that matches the given interview data. */
|
|
343
|
+
|
|
344
|
+
// Defaults
|
|
345
|
+
let { timeout } = options || { timeout: DEFAULT_MAX_REQUEST_MS };
|
|
346
|
+
|
|
347
|
+
if ( interview && interview.uid ) {
|
|
348
|
+
for ( let api_key of api_keys ) {
|
|
349
|
+
try {
|
|
350
|
+
log.debug({ type: `da api info`, code: `ALK0192`, pre: `Trying to get the interview with the id ${ interview.uid } and an API key.` });
|
|
351
|
+
|
|
352
|
+
// Actual work:
|
|
353
|
+
let response = await _da_REST.get_interview( timeout, {
|
|
354
|
+
session: interview.uid,
|
|
355
|
+
api_key
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
if ( response && response.data && response.data.items && response.data.items[0] && response.data.items[0].session ) {
|
|
359
|
+
log.debug({ type: `da api info`, code: `ALK0193`, pre: `Status ${ response.status }. Got ${ response.data.items[0].session }.`});
|
|
360
|
+
return response.data.items[0];
|
|
361
|
+
} else {
|
|
362
|
+
log.debug({ type: `da api info`, code: `ALK0194`, pre: `Status ${ response.status }. Cannot find ${ interview.uid }`});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
} catch ( error ) {
|
|
366
|
+
error.interview = interview;
|
|
367
|
+
let msg = `${ error.status || `No error.status.` } ${ error.statusText || error.data || `No error.statusText.` } Ran into an error when getting the interview ${ interview.uid }. See https://docassemble.org/docs/api.html#interviews.`
|
|
368
|
+
log.debug({ type: `da api info`, code: `ALK0195`, pre: msg, data: error });
|
|
369
|
+
}
|
|
370
|
+
} // ends for api_keys
|
|
371
|
+
} else {
|
|
372
|
+
log.debug({ type: `da api info`, code: `ALK0196`, pre: `There is no interview to get.` });
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return null;
|
|
376
|
+
}; // Ends da_i.get_interview()
|
package/lib/run_cucumber.js
CHANGED
|
@@ -8,6 +8,7 @@ const session_vars = require('./utils/session_vars');
|
|
|
8
8
|
|
|
9
9
|
const SUMMARY_FILENAME = `cucumber-report.txt`;
|
|
10
10
|
|
|
11
|
+
|
|
11
12
|
function build_config( command_args ) {
|
|
12
13
|
// Turn all the paths to the sources folders into paths to the feature files
|
|
13
14
|
let sources_paths = session_vars.get_sources_paths();
|
|
@@ -124,7 +125,8 @@ function build_config( command_args ) {
|
|
|
124
125
|
let summary_file_name = SUMMARY_FILENAME;
|
|
125
126
|
fs.readFile(summary_file_name, 'utf8', (err, text) => {
|
|
126
127
|
if (err) {
|
|
127
|
-
|
|
128
|
+
// Something else will probably show an error that's more relevant
|
|
129
|
+
log.info({ type: `run`, code: `ALK0056`, pre: `Failed to read cucumber report`, data: err });
|
|
128
130
|
return;
|
|
129
131
|
}
|
|
130
132
|
|
|
@@ -137,7 +139,7 @@ function build_config( command_args ) {
|
|
|
137
139
|
try {
|
|
138
140
|
fs.appendFileSync( `${ artifacts_path }/${ log.debug_log_file }`, `\n\n${text}`);
|
|
139
141
|
} catch ( error ) {
|
|
140
|
-
log.info({ type: `
|
|
142
|
+
log.info({ type: `run`, code: `ALK0182`, pre: `Something probably went wrong earlier. Look above for more details, but here is more information just in case: ALKiln was unable to write to ${ artifacts_path }/${ log.debug_log_file }.`, data: error });
|
|
141
143
|
}
|
|
142
144
|
try {
|
|
143
145
|
// Add to a unexpected results file when tests themselves fail
|
|
@@ -145,7 +147,7 @@ function build_config( command_args ) {
|
|
|
145
147
|
fs.appendFileSync( `${ artifacts_path }/${ log.unexpected_filename }`, `\n\n${ text }` );
|
|
146
148
|
}
|
|
147
149
|
} catch ( error ) {
|
|
148
|
-
log.info({ type: `
|
|
150
|
+
log.info({ type: `run`, code: `ALK0181`, pre: `Something probably went wrong earlier. Look above for more details, but here is more information just in case: ALKiln was unable to write to ${ artifacts_path }/${ log.unexpected_filename }.`, data: error });
|
|
149
151
|
}
|
|
150
152
|
});
|
|
151
153
|
})();
|
package/lib/scope.js
CHANGED
|
@@ -16,7 +16,7 @@ const time = require('./utils/time');
|
|
|
16
16
|
const log = require('./utils/log');
|
|
17
17
|
const { waitForTimeout } = require('./utils/time');
|
|
18
18
|
const files = require('./utils/files' );
|
|
19
|
-
const
|
|
19
|
+
const da_api = require('./docassemble/docassemble_api_interface');
|
|
20
20
|
const wrapPromiseWithTimeout = require('./cucumber_8_shim');
|
|
21
21
|
const reports = require(`./utils/reports`);
|
|
22
22
|
|
|
@@ -339,7 +339,7 @@ module.exports = {
|
|
|
339
339
|
// This code assumes a reasonable response time would be n seconds if the
|
|
340
340
|
// server is up and that a server reload/restart won't take less than n seconds.
|
|
341
341
|
// Also, it's non-blocking, so it doesn't slow down tests
|
|
342
|
-
let is_up = await
|
|
342
|
+
let is_up = await da_api.is_server_responding({ timeout: scope.server_check_interval_ms });
|
|
343
343
|
// Add status to the beginning of the list to make truncating easier
|
|
344
344
|
scope.server_statuses.unshift( is_up );
|
|
345
345
|
|
|
@@ -627,7 +627,7 @@ module.exports = {
|
|
|
627
627
|
interview_url = `${ base_url }${ file_name }`;
|
|
628
628
|
}
|
|
629
629
|
|
|
630
|
-
reports.addToReport(scope,{
|
|
630
|
+
reports.addToReport(scope, {
|
|
631
631
|
code: `ALK0113`,
|
|
632
632
|
type: `row info`,
|
|
633
633
|
value: `Trying to load the interview at "${ interview_url }"`
|
|
@@ -638,7 +638,17 @@ module.exports = {
|
|
|
638
638
|
// TODO: implement and use scope.handle_possible_timeout()
|
|
639
639
|
try {
|
|
640
640
|
|
|
641
|
-
await scope.page.goto(interview_url, { waitUntil: 'domcontentloaded', timeout: scope.timeout })
|
|
641
|
+
await scope.page.goto(interview_url, { waitUntil: 'domcontentloaded', timeout: scope.timeout });
|
|
642
|
+
// Get interview session values, if possible, so we can delete the session later
|
|
643
|
+
let interview_vars = await scope.page.evaluate(async function() {
|
|
644
|
+
try {
|
|
645
|
+
// won't exist on non-da interview pages
|
|
646
|
+
return await get_interview_variables();
|
|
647
|
+
} catch (error) {
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
scope.set_interview_session_data( scope, { vars: interview_vars });
|
|
642
652
|
|
|
643
653
|
} catch ( error ) {
|
|
644
654
|
|
|
@@ -682,6 +692,36 @@ module.exports = {
|
|
|
682
692
|
return scope.page;
|
|
683
693
|
}, // Ends scope.load()
|
|
684
694
|
|
|
695
|
+
set_interview_session_data: function ( scope, { vars = null }) {
|
|
696
|
+
/** Sets this interview's session data for this scenario if it doesn't
|
|
697
|
+
* already exist.
|
|
698
|
+
*
|
|
699
|
+
* @returns final_vars { object | null }
|
|
700
|
+
* @returns final_vars.i { str } File path of interview
|
|
701
|
+
* @returns final_vars.uid { str } UID of interview
|
|
702
|
+
* */
|
|
703
|
+
if ( vars && vars.i ) {
|
|
704
|
+
let scenario = scope.scenarios.get( scope.scenario_id );
|
|
705
|
+
if ( !scenario.interviews ) { scenario.interviews = {}; }
|
|
706
|
+
if ( !scenario.interviews[ vars.uid ] ) {
|
|
707
|
+
scenario.interviews[ vars.uid ] = vars;
|
|
708
|
+
log.debug({
|
|
709
|
+
code: `ALK0185`, type: `info`,
|
|
710
|
+
pre: `Saved new interview data. File path: ${ vars.i }. Session id: ${ vars.uid }.`
|
|
711
|
+
});
|
|
712
|
+
} else {
|
|
713
|
+
log.debug({
|
|
714
|
+
code: `ALK0186`, type: `info`,
|
|
715
|
+
pre: `Interview data already existed. File path: ${ vars.i }. Session id: ${ vars.uid }.`
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
return vars;
|
|
719
|
+
} else {
|
|
720
|
+
log.debug({ code: `ALK0187`, type: `info`, pre: `Found no interview data.` });
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
723
|
+
}, // Ends scope.set_interview_session_data()
|
|
724
|
+
|
|
685
725
|
getLoadData: async function ( scope ) {
|
|
686
726
|
/* Return elements that could load on the page, like #daMainQuestion
|
|
687
727
|
* and #da-retry. If there's a system error on the page, return its main text.
|
|
@@ -771,7 +811,7 @@ module.exports = {
|
|
|
771
811
|
if ( row_raw.length > 3 ) { more_or_less = `less`; }
|
|
772
812
|
let bad_table_msg = `Your Story Table should have ${ more_or_less } than`
|
|
773
813
|
+ ` ${ row_raw.length } columns. Take another look at Story Table documentation at`
|
|
774
|
-
+ ` https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln
|
|
814
|
+
+ ` https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln/writing#story-table.`
|
|
775
815
|
reports.addToReport( scope, { type: `error`, code: `ALK0120`, value: bad_table_msg})
|
|
776
816
|
throw Error(bad_table_msg);
|
|
777
817
|
}
|
|
@@ -798,7 +838,7 @@ module.exports = {
|
|
|
798
838
|
let not_in_env_msg = `ALKiln could not find the GitHub secret "${ row.flags.secret_name }".`
|
|
799
839
|
+ ` Check for a typo and check your workflow file.`
|
|
800
840
|
+ ` Read more about using GitHub secrets in your workflow file at`
|
|
801
|
-
+ ` https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln
|
|
841
|
+
+ ` https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln/writing#secrets.`
|
|
802
842
|
reports.addToReport( scope, { type: `error`, code: `ALK0121`, value: not_in_env_msg });
|
|
803
843
|
throw ReferenceError(not_in_env_msg);
|
|
804
844
|
}
|
|
@@ -1317,6 +1357,7 @@ module.exports = {
|
|
|
1317
1357
|
* setting fields, and an object for a DOM field, returns list of the
|
|
1318
1358
|
* rows that match the DOM field. That list of story table rows may be empty.
|
|
1319
1359
|
*
|
|
1360
|
+
* Using a `trigger` column is deprecated, but still used in many tests in the wild.
|
|
1320
1361
|
* For notes on trigger var, see https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln/#trigger
|
|
1321
1362
|
*
|
|
1322
1363
|
* @param var_data {arr} - fields and the values they can be set to
|
|
@@ -1411,9 +1452,13 @@ module.exports = {
|
|
|
1411
1452
|
|
|
1412
1453
|
// field/page trigger name is needed but is missing.
|
|
1413
1454
|
if ( !page_trigger_name ) {
|
|
1414
|
-
let page_trigger_warning = `
|
|
1415
|
-
+ `
|
|
1416
|
-
+ `
|
|
1455
|
+
let page_trigger_warning = `You are using a 3-column Story Table. If possible, you should `
|
|
1456
|
+
+ `think about switching to the newer, easier, 2-column Story Table. That will probably `
|
|
1457
|
+
+ `get rid of this warning. Read more at `
|
|
1458
|
+
+ `https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln/trouble#deprecated-story-tables. `
|
|
1459
|
+
+ `If you still want to use this 3-column format, here is more info about the warning: `
|
|
1460
|
+
+ `A field on this page uses an index variable or a generic object in a 3-column `
|
|
1461
|
+
+ `Story Table, but the page is missing the ALKiln "trigger" variable's name. See `
|
|
1417
1462
|
+ `https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln/#a-missing-trigger-variable.`;
|
|
1418
1463
|
reports.addToReport(scope, { type: `warning`, code: `ALK0127`, value: page_trigger_warning });
|
|
1419
1464
|
}
|
|
@@ -1430,7 +1475,7 @@ module.exports = {
|
|
|
1430
1475
|
|
|
1431
1476
|
// First, make the row data more readable to humans.
|
|
1432
1477
|
let row_as_story_table = reports.convertToOriginalStoryTableRow( scope, { row_data: row_that_matches_this_field });
|
|
1433
|
-
let row_trigger_warning = `This Story Table row needs a value in the "trigger" `
|
|
1478
|
+
let row_trigger_warning = `This 3-column Story Table row needs a value in the "trigger" `
|
|
1434
1479
|
+ `column, but it is empty. The test might get unexpected results. See `
|
|
1435
1480
|
+ `https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln/#trigger.`
|
|
1436
1481
|
+ `\nThe rows:\n${ row_as_story_table }`;
|
|
@@ -1467,7 +1512,7 @@ module.exports = {
|
|
|
1467
1512
|
|
|
1468
1513
|
let all_trigger_names_conflict_msg = `${ rows_that_match_field_names.length } `
|
|
1469
1514
|
+ `Story Table row(s) matched a field name, but didn't match a trigger `
|
|
1470
|
-
+ `variable.
|
|
1515
|
+
+ `variable. This 3-column Story Table might be missing a row or a trigger variable. `
|
|
1471
1516
|
+ `The test will have to ignore this field. See `
|
|
1472
1517
|
+ `https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln/#trigger.`
|
|
1473
1518
|
+ `\nThe row(s) data:\n${ conflicts_as_story_table_rows.join( `\n` )}`
|
|
@@ -2893,7 +2938,9 @@ module.exports = {
|
|
|
2893
2938
|
await scope.afterStep(scope, {waitForShowIf: true});
|
|
2894
2939
|
}, // Ends scope.steps.set_al_address()
|
|
2895
2940
|
|
|
2896
|
-
sign_in: async function ( scope, {
|
|
2941
|
+
sign_in: async function ( scope, {
|
|
2942
|
+
email_secret_name, password_secret_name, api_key_secret_name
|
|
2943
|
+
}) {
|
|
2897
2944
|
/** Allow the developer to log a user into their server using GitHub
|
|
2898
2945
|
* secrets to authenticate.
|
|
2899
2946
|
*/
|
|
@@ -2946,6 +2993,26 @@ module.exports = {
|
|
|
2946
2993
|
if ( password === undefined ) {
|
|
2947
2994
|
reports.addToReport( scope, { type: `error`, code: `ALK0162`, value: password_msg })
|
|
2948
2995
|
}
|
|
2996
|
+
// Save API key to clean up interviews later. Nice to have, not critical
|
|
2997
|
+
if ( api_key_secret_name ) {
|
|
2998
|
+
let api_key = process.env[ api_key_secret_name ];
|
|
2999
|
+
let api_key_msg = `The API key in the variable "${ api_key_secret_name }" is missing. Check for a typo in your test, workflow file, GitHub secret, or server config.`
|
|
3000
|
+
if ( api_key === undefined ) {
|
|
3001
|
+
reports.addToReport( scope, { type: `warning`, code: `ALK0199`, value: api_key_msg });
|
|
3002
|
+
} else if ( api_key === `` ) {
|
|
3003
|
+
reports.addToReport( scope, { type: `warning`, code: `ALK0200`,
|
|
3004
|
+
value: `ALKiln found the API key in the variable "${ api_key_secret_name }", but it was empty. It had no text in it. Edit the value of the GitHub secret or server config variable called "${ api_key_secret_name }".` });
|
|
3005
|
+
} else {
|
|
3006
|
+
let access_info = await da_api.get_api_key_access_status({ api_key });
|
|
3007
|
+
if ( !access_info.has_auth ) {
|
|
3008
|
+
reports.addToReport( scope, { type: `warning`, code: `ALK0201`,
|
|
3009
|
+
value: `The server rejected the API key in the variable "${ api_key_secret_name }". Does it exist on this server? Was it deleted somehow? You can always make a new one.` });
|
|
3010
|
+
}
|
|
3011
|
+
// Even if it doesn't have access, give it a shot anyway.
|
|
3012
|
+
// Maybe we got something wrong.
|
|
3013
|
+
scope.scenarios.get( scope.scenario_id ).api_keys.push( api_key );
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
2949
3016
|
|
|
2950
3017
|
expect( email, email_msg ).to.not.equal( undefined );
|
|
2951
3018
|
expect( password, password_msg ).to.not.equal( undefined );
|
package/lib/steps.js
CHANGED
|
@@ -22,6 +22,7 @@ const log = require('./utils/log');
|
|
|
22
22
|
const session_vars = require('./utils/session_vars');
|
|
23
23
|
const files = require('./utils/files' );
|
|
24
24
|
const reports = require(`./utils/reports`);
|
|
25
|
+
const da_api = require('./docassemble/docassemble_api_interface');
|
|
25
26
|
|
|
26
27
|
/* Of Note:
|
|
27
28
|
- We're using `*=` for selectors because sometimes da text has funny characters in it that are hard to anticipate
|
|
@@ -83,6 +84,9 @@ let click_with = {
|
|
|
83
84
|
|
|
84
85
|
BeforeAll(async () => {
|
|
85
86
|
scope.showif_timeout = 600;
|
|
87
|
+
// TODO: Do these really need to be maps? As sets, they may make the output
|
|
88
|
+
// more predictable to the user, but they add complexity.
|
|
89
|
+
scope.scenarios = new Map();
|
|
86
90
|
scope.report = new Map();
|
|
87
91
|
|
|
88
92
|
// Start tracking whether server is up and running
|
|
@@ -93,7 +97,7 @@ BeforeAll(async () => {
|
|
|
93
97
|
|
|
94
98
|
// TODO: figure out how to test file and folder creation stuff,
|
|
95
99
|
// especially when setup is excluded from the flow.
|
|
96
|
-
|
|
100
|
+
|
|
97
101
|
// Make the folder to store all the files for this run of the tests
|
|
98
102
|
scope.paths.artifacts = files.make_artifacts_folder();
|
|
99
103
|
// Save its name in a file. `session_vars` can get it back later.
|
|
@@ -127,13 +131,16 @@ Before(async (scenario) => {
|
|
|
127
131
|
}
|
|
128
132
|
}
|
|
129
133
|
// Clean up all previously existing pages
|
|
130
|
-
for (const page of await scope.browser.pages()) {
|
|
134
|
+
for (const page of await scope.browser.pages()) {
|
|
131
135
|
await page.close();
|
|
132
136
|
}
|
|
133
137
|
// Make a new page
|
|
134
138
|
scope.page = await scope.browser.newPage()
|
|
135
139
|
|
|
136
140
|
scope.scenario_id = uuidv4();
|
|
141
|
+
scope.scenarios.set( scope.scenario_id, {} );
|
|
142
|
+
scope.scenarios.get( scope.scenario_id ).interviews = {};
|
|
143
|
+
scope.scenarios.get( scope.scenario_id ).api_keys = [];
|
|
137
144
|
scope.expected_status = null;
|
|
138
145
|
scope.expected_in_report = null;
|
|
139
146
|
scope.expected_not_in_report = null;
|
|
@@ -179,7 +186,7 @@ Given(/I start the interview at "([^"]+)"/, {timeout: -1}, async (file_name) =>
|
|
|
179
186
|
|
|
180
187
|
// Load the right interview page
|
|
181
188
|
await scope.load( scope, file_name );
|
|
182
|
-
|
|
189
|
+
|
|
183
190
|
// The page timeout is set in here too in case someone set a custom timeout
|
|
184
191
|
// just before a page was loaded for the Scenario. Move this to just after page creation?
|
|
185
192
|
// The function itself won't really timeout here, so we don't need to wrap it in
|
|
@@ -274,21 +281,27 @@ Given(/I go to "([^"]+)"/i, {timeout: -1}, async ( url ) => {
|
|
|
274
281
|
return result;
|
|
275
282
|
});
|
|
276
283
|
|
|
277
|
-
Given(
|
|
284
|
+
Given(
|
|
285
|
+
/I (?:sign|log) ?(?:in)?(?:on)?(?:to the server)? with(?: the email)? "([^"]+)",?(?: and)?(?: the password)? "([^"]+)"(?: SECRETs)?(?:,?(?: and)?(?: the API key)? "([^"]+)")?/i,
|
|
286
|
+
{ timeout: -1 },
|
|
287
|
+
async ( email, password, api_key ) => {
|
|
278
288
|
/** Uses the names of environment variables (most often GitHub SECRETs) to
|
|
279
|
-
* log into an account on the user's server. Must be
|
|
289
|
+
* log into an account on the user's server. Must be secure.
|
|
280
290
|
*
|
|
281
291
|
* Variations:
|
|
282
|
-
* I sign into the server with the email "
|
|
283
|
-
* I sign on with the email "
|
|
284
|
-
* I log in with "
|
|
292
|
+
* I sign into the server with the email "USER2_EMAIL" and the password "USER2_PASSWORD"
|
|
293
|
+
* I sign on with the email "USER2_EMAIL" and the password "USER2_PASSWORD"
|
|
294
|
+
* I log in with "USER2_EMAIL" and "USER2_PASSWORD"
|
|
295
|
+
* I sign in with the email "USER2_EMAIL", the password "USER2_PASSWORD", and the API key "USER2_API_KEY"
|
|
296
|
+
* I sign in with "USER2_EMAIL", "USER2_PASSWORD", "USER2_API_KEY"
|
|
285
297
|
*/
|
|
286
298
|
// `page` timeout will deal with custom timeout
|
|
287
299
|
// Couldn't test the timeout for tapping the button because there's not
|
|
288
300
|
// enough of a pause between initial navigation and pressing button.
|
|
289
301
|
await scope.steps.sign_in( scope, {
|
|
290
302
|
email_secret_name: email,
|
|
291
|
-
password_secret_name: password
|
|
303
|
+
password_secret_name: password,
|
|
304
|
+
api_key_secret_name: api_key
|
|
292
305
|
});
|
|
293
306
|
});
|
|
294
307
|
|
|
@@ -313,7 +326,7 @@ Given(/the max(?:imum)? sec(?:ond)?s for each step (?:in this scenario )?is (\d+
|
|
|
313
326
|
/* At any time, set a custom timeout for the scenario. */
|
|
314
327
|
// TODO: Add 1 second to this to allow other steps to not fail, like "I wait n seconds"
|
|
315
328
|
scope.timeout = parseInt( seconds ) * 1000;
|
|
316
|
-
|
|
329
|
+
|
|
317
330
|
// Cucumber timeouts are made custom for each step that needs it
|
|
318
331
|
|
|
319
332
|
// If there is no page yet, we've ensured custom timeout will still be set
|
|
@@ -653,7 +666,7 @@ Then('I should see the link to {string}', async ( url ) => { // √
|
|
|
653
666
|
let msg = `Cannot find a link to "${ url }".`;
|
|
654
667
|
if ( !link ) { reports.addToReport(scope, { type: `error`, code: `ALK0080`, value: msg }); }
|
|
655
668
|
expect(link, msg ).to.exist;
|
|
656
|
-
|
|
669
|
+
|
|
657
670
|
await scope.afterStep(scope);
|
|
658
671
|
});
|
|
659
672
|
|
|
@@ -662,7 +675,7 @@ Then(/the "([^"]+)" link leads to "([^"]+)"/, async (linkText, expected_url) =>
|
|
|
662
675
|
let [link] = await scope.page.$x(`//a[contains(text(), "${linkText}")]`);
|
|
663
676
|
// TODO: Possibly add a check here for the link text, though note this
|
|
664
677
|
// Step is not currently supported.
|
|
665
|
-
|
|
678
|
+
|
|
666
679
|
let prop_obj = await link.getProperty('href');
|
|
667
680
|
let actual_url = await prop_obj.jsonValue();
|
|
668
681
|
|
|
@@ -756,7 +769,7 @@ Then(/I expect the baseline PDF "([^"]+)" and the new PDF "([^"]+)" to be the sa
|
|
|
756
769
|
* Variations:
|
|
757
770
|
* Then I expect the baseline PDF "simple-doc-Baseline.pdf" and the new PDF "simple-doc.pdf" to be the same
|
|
758
771
|
*/
|
|
759
|
-
|
|
772
|
+
|
|
760
773
|
return await wrapPromiseWithTimeout(
|
|
761
774
|
scope.steps.compare_pdfs(scope, {existing_pdf_path, new_pdf_path}),
|
|
762
775
|
scope.timeout
|
|
@@ -880,7 +893,7 @@ When(/I answer randomly for at most ([0-9]+) (?:page(?:s)?|screen(?:s)?)/, { tim
|
|
|
880
893
|
|
|
881
894
|
// Set up timeout to stop infinite loops
|
|
882
895
|
let page_timeout_id = setTimeout(async function() {
|
|
883
|
-
|
|
896
|
+
|
|
884
897
|
// Make sure to only throw once
|
|
885
898
|
if ( has_thrown ) { return; }
|
|
886
899
|
else { has_thrown = true; }
|
|
@@ -1178,6 +1191,71 @@ Given(/the final Scenario status should be "([^"]+)"/i, async ( expected_status
|
|
|
1178
1191
|
scope.expected_status = expected_status.toUpperCase();
|
|
1179
1192
|
});
|
|
1180
1193
|
|
|
1194
|
+
Then(/of two interviews, I delete one __internal__/i, async () => {
|
|
1195
|
+
/** Test that an interview does get successfully deleted. This tests whether
|
|
1196
|
+
* the API code is working correctly.
|
|
1197
|
+
*/
|
|
1198
|
+
// Make sure we have two interviews in `scope.scenario.interviews` (two session ids)
|
|
1199
|
+
let interviews = scope.scenarios.get( scope.scenario_id ).interviews;
|
|
1200
|
+
let interview_keys = Object.keys( interviews );
|
|
1201
|
+
expect(
|
|
1202
|
+
interview_keys.length,
|
|
1203
|
+
`Of 2 interview sessions, ALKiln only saved ${ interview_keys.length }`
|
|
1204
|
+
).to.equal( 2 );
|
|
1205
|
+
|
|
1206
|
+
// Check that both interviews exist on the server. 1 √ 2 √
|
|
1207
|
+
let api_keys = scope.scenarios.get( scope.scenario_id ).api_keys;
|
|
1208
|
+
let visible_interviews = await da_api.get_interviews({ interviews, api_keys });
|
|
1209
|
+
let visible_keys = Object.keys( visible_interviews );
|
|
1210
|
+
expect(
|
|
1211
|
+
visible_keys.length,
|
|
1212
|
+
`2 interviews should be on the server. Instead: "${ visible_keys.join(', ') }"`
|
|
1213
|
+
).to.equal( 2 );
|
|
1214
|
+
|
|
1215
|
+
// Delete one. 1 x 2 √
|
|
1216
|
+
let to_delete = interviews[ interview_keys[ 0 ]];
|
|
1217
|
+
await da_api.quietly_try_to_delete_interviews({
|
|
1218
|
+
interviews: { [interview_keys[ 0 ]]: to_delete },
|
|
1219
|
+
api_keys
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
// Check that only one session exists on the server. This tests that
|
|
1223
|
+
// 1. An interview has been deleted and
|
|
1224
|
+
// 2. We are fetching interviews correctly because we are getting one
|
|
1225
|
+
// back (e.g. not passing null into the api function)
|
|
1226
|
+
// 1 x 2 √
|
|
1227
|
+
let after_deletion = await da_api.get_interviews({ interviews, api_keys });
|
|
1228
|
+
let keys_after_deletion = Object.keys( after_deletion );
|
|
1229
|
+
expect(
|
|
1230
|
+
keys_after_deletion.length,
|
|
1231
|
+
`1 interviews should be on the server: ${ keys_after_deletion.join(', ') }`
|
|
1232
|
+
).to.equal( 1 )
|
|
1233
|
+
// In After(), the other session should get deleted
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
Then(/I fail to delete (\d) detected interview(?:s)? and get no error __internal__/i, async ( num ) => {
|
|
1237
|
+
/** Test that there is no error when ALKiln is unable to delete an interview. */
|
|
1238
|
+
num = parseInt( num );
|
|
1239
|
+
// Make sure we started an interview
|
|
1240
|
+
let interviews = scope.scenarios.get( scope.scenario_id ).interviews;
|
|
1241
|
+
expect(
|
|
1242
|
+
Object.keys( interviews ).length,
|
|
1243
|
+
`The test should have saved ${ num } interview session(s)`
|
|
1244
|
+
).to.equal( num );
|
|
1245
|
+
|
|
1246
|
+
// There should be no error about an unauthorized user
|
|
1247
|
+
let api_keys = scope.scenarios.get( scope.scenario_id ).api_keys;
|
|
1248
|
+
let visible_interviews = await da_api.get_interviews({ interviews, api_keys });
|
|
1249
|
+
let visible_keys = Object.keys( visible_interviews );
|
|
1250
|
+
expect(
|
|
1251
|
+
visible_keys.length,
|
|
1252
|
+
`User should detect 0 interviews on the server. Instead: "${ visible_keys.join(', ') }"`
|
|
1253
|
+
).to.equal( 0 );
|
|
1254
|
+
|
|
1255
|
+
// Deleting with an anonymous user should fail silently
|
|
1256
|
+
await da_api.quietly_try_to_delete_interviews({ interviews, api_keys });
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1181
1259
|
|
|
1182
1260
|
//#####################################
|
|
1183
1261
|
//#####################################
|
|
@@ -1314,8 +1392,8 @@ After(async function(scenario) {
|
|
|
1314
1392
|
// it as failed too.
|
|
1315
1393
|
expected_status_was_met = false;
|
|
1316
1394
|
// Keep it general. Users may see this message, e.g. for a11y tests.
|
|
1317
|
-
let msg = `Test
|
|
1318
|
-
+ `
|
|
1395
|
+
let msg = `Test ${ changeable_test_status }. `
|
|
1396
|
+
+ `Expected status: ${scope.expected_status}. `
|
|
1319
1397
|
+ `See the report for details.`;
|
|
1320
1398
|
let code = `ALK0099`;
|
|
1321
1399
|
log.debug({ type: `error`, code: code, pre: msg, })
|
|
@@ -1372,7 +1450,7 @@ After(async function(scenario) {
|
|
|
1372
1450
|
code: code,
|
|
1373
1451
|
message: non_reload_report_msg,
|
|
1374
1452
|
}, error });
|
|
1375
|
-
} catch {
|
|
1453
|
+
} catch ( error ) {
|
|
1376
1454
|
signout_error_msg = `🤕 ${ code } ERROR: ${ non_reload_report_msg }`;
|
|
1377
1455
|
}
|
|
1378
1456
|
} else {
|
|
@@ -1387,10 +1465,43 @@ After(async function(scenario) {
|
|
|
1387
1465
|
|
|
1388
1466
|
} // ends if scope.page
|
|
1389
1467
|
|
|
1468
|
+
// Now that we're no longer on the interview page, one way or
|
|
1469
|
+
// another, try to delete the interviews created during the test
|
|
1470
|
+
// Authors running this manually get to choose what to do with
|
|
1471
|
+
// the interviews they're creating unless we hear otherwise.
|
|
1472
|
+
if ( session_vars.get_origin() !== `playground` ) {
|
|
1473
|
+
try {
|
|
1474
|
+
let interviews = scope.scenarios.get( scope.scenario_id ).interviews;
|
|
1475
|
+
let api_keys = scope.scenarios.get( scope.scenario_id ).api_keys;
|
|
1476
|
+
await da_api.quietly_try_to_delete_interviews({ interviews, api_keys });
|
|
1477
|
+
let visible_interviews = await da_api.get_interviews({ interviews, api_keys });
|
|
1478
|
+
let visible_keys = Object.keys( visible_interviews );
|
|
1479
|
+
// Deleting interviews is nice, but not non-critical. Don't bother the
|
|
1480
|
+
// user with it. Don't want to cover up a more meaningful failure (like
|
|
1481
|
+
// going to the wrong file). If it becomes a problem we can track it down later.
|
|
1482
|
+
if ( visible_keys.length > 0 ) {
|
|
1483
|
+
// Can an API key ever see an interview that it can't delete?
|
|
1484
|
+
log.info({ type: `internal warning`, code: `ALK0197`,
|
|
1485
|
+
pre: `The API key can see ${ visible_keys.length } interviews that were not deleted: ${ visible_keys.join( ', ' ) }`
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1488
|
+
} catch ( error ) {
|
|
1489
|
+
// Deleting interviews is nice, but not non-critical. Don't bother the
|
|
1490
|
+
// user with it. Don't want to cover up a more meaningful failure (like
|
|
1491
|
+
// going to the wrong file). Example of OK failure: the user owns the
|
|
1492
|
+
// interview is different than the API key that's trying to delete the
|
|
1493
|
+
// interview. If it becomes a problem we can track it down later.
|
|
1494
|
+
// Consider add "internal" to docs: "If you see this, make an
|
|
1495
|
+
// issue in ALKiln with your debug/verbose log"
|
|
1496
|
+
log.info({ type: `internal warning`, code: `ALK0198`, pre: `Error while cleaning up interviews.`, data: error });
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
// Handle all the accumulated errors
|
|
1390
1501
|
let errors = []
|
|
1391
1502
|
if ( !expected_status_was_met ) { errors.push( expected_status_err_msg ); }
|
|
1392
1503
|
if ( !signout_succeeded ) { errors.push( signout_error_msg ); }
|
|
1393
|
-
if (
|
|
1504
|
+
if ( errors.length > 0 ) {
|
|
1394
1505
|
throw new Error( `${errors.join( errors, '\n')}` );
|
|
1395
1506
|
}
|
|
1396
1507
|
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suffolklitlab/alkiln",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.13.0",
|
|
4
4
|
"description": "Integrated automated end-to-end testing with docassemble, puppeteer, and cucumber.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "npm run unit && npm run codes && npm run cucumber",
|
|
8
8
|
"cucumber": "./lib/run_cucumber.js",
|
|
9
9
|
"local": "npm run setup && npm run cucumber; npm run takedown",
|
|
10
|
-
"pass": "npm run cucumber 'not @error'",
|
|
10
|
+
"pass": "npm run cucumber 'not @error and not @temp_error'",
|
|
11
11
|
"fail": "npm run cucumber '@error'",
|
|
12
12
|
"setup": "./lib/docassemble/setup.js",
|
|
13
13
|
"takedown": "./lib/docassemble/takedown.js",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"bugs": {
|
|
33
33
|
"url": "https://github.com/SuffolkLITLab/ALKiln/issues"
|
|
34
34
|
},
|
|
35
|
-
"homepage": "https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/
|
|
35
|
+
"homepage": "https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln/intro",
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@axe-core/puppeteer": "4.7.3",
|
|
38
38
|
"@cucumber/cucumber": "9.3.0",
|
package/setup.py
CHANGED
|
@@ -50,7 +50,7 @@ setup(name='docassemble.ALKilnTests',
|
|
|
50
50
|
author='plocket',
|
|
51
51
|
author_email='example@example.com',
|
|
52
52
|
license='The MIT License (MIT)',
|
|
53
|
-
url='https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln',
|
|
53
|
+
url='https://suffolklitlab.org/docassemble-AssemblyLine-documentation/docs/alkiln/intro',
|
|
54
54
|
packages=find_packages(),
|
|
55
55
|
namespace_packages=['docassemble'],
|
|
56
56
|
install_requires=['docassemble.ALToolbox>=0.8.2', 'docassemble.AssemblyLine>=2.24.0'],
|
|
@@ -16,7 +16,7 @@ fi
|
|
|
16
16
|
# The grepped files will include the list of all the codes that have been
|
|
17
17
|
# removed and are no longer used. Syntax used works with GitHub cli ([[:digit:]])
|
|
18
18
|
# https://stackoverflow.com/a/6901221
|
|
19
|
-
lines=$(grep -roh --exclude="tests/log_codes/check_codes.sh" --exclude="debug_log.txt" --exclude="cucumber-report.txt" --exclude-dir="node_modules" --exclude-dir='alkiln-*' --exclude-dir='_alkiln*' 'ALK[[:digit:]][[:digit:]][[:digit:]][[:digit:]]' "$directory")
|
|
19
|
+
lines=$(grep -roh --exclude="tests/log_codes/check_codes.sh" --exclude="debug_log.txt" --exclude="cucumber-report.txt" --exclude-dir="ALKilnTests" --exclude-dir="node_modules" --exclude-dir='alkiln-*' --exclude-dir='_alkiln*' 'ALK[[:digit:]][[:digit:]][[:digit:]][[:digit:]]' "$directory")
|
|
20
20
|
sorted=$(echo "$lines" | sort -n)
|
|
21
21
|
|
|
22
22
|
|