@suffolklitlab/alkiln 5.12.0-delete2 → 5.12.0-delete3

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.
@@ -81,6 +81,11 @@ jobs:
81
81
  run: |
82
82
  echo "USER1_EMAIL=${{ steps.github_server.outputs.DA_ADMIN_EMAIL }}" >> $GITHUB_ENV
83
83
  echo "USER1_PASSWORD=${{ steps.github_server.outputs.DA_ADMIN_PASSWORD }}" >> $GITHUB_ENV
84
+ echo "USER_NO_PERMISSIONS_EMAIL=${{ steps.github_server.outputs.DA_ADMIN_EMAIL }}" >> $GITHUB_ENV
85
+ echo "USER_NO_PERMISSIONS_PASSWORD=${{ steps.github_server.outputs.DA_ADMIN_PASSWORD }}" >> $GITHUB_ENV
86
+ echo "USER_NO_PERMISSIONS_API_KEY=${{ steps.github_server.outputs.DOCASSEMBLE_DEVELOPER_API_KEY }}" >> $GITHUB_ENV
87
+ echo "INVALID_API_KEY=invalidAPIkey" >> $GITHUB_ENV
88
+ echo "EMPTY_STRING=''" >> $GITHUB_ENV
84
89
 
85
90
  #### Developer note:
86
91
  #### Example of working with the docker container after it's been
@@ -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
package/CHANGELOG.md CHANGED
@@ -51,11 +51,12 @@ Format:
51
51
 
52
52
  ### Fixed
53
53
 
54
- - Now delete interviews upon interview completion for signed in users
54
+ - 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 a regular user an API key with https://docassemble.org/docs/api.html#api_user_user_id_api_post. See [#831](https://github.com/SuffolkLITLab/ALKiln/issues/831).
55
55
 
56
56
  ### Internal
57
57
 
58
- - Test deleting interviews upon interview completion for signed in users
58
+ - Test deleting interviews upon interview completion for signed in users.
59
+ - Switch to mostly using a lowest-access-level user account for actually running interviews.
59
60
  - Added @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.
60
61
 
61
62
  ## [5.12.0] - 2024-06-06
@@ -31,6 +31,27 @@ da.get_dev_id = async function ( timeout ) {
31
31
  }; // Ends da.get_dev_id()
32
32
 
33
33
 
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
+
34
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. */
@@ -177,6 +177,67 @@ da_i.get_dev_id = async function ( dev_id_options ) {
177
177
  return dev_id;
178
178
  }; // Ends da_i.get_dev_id();
179
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
+
180
241
  // node -e 'require("./lib/docassemble/docassemble_api_interface.js").delete_project( "create1" )'
181
242
  da_i.delete_project = async function ( delete_options ) {
182
243
  /** Delete a project on the user's da server. Do we need to watch
@@ -304,6 +365,7 @@ da_i.get_interview = async function ({ interview = {}, api_keys = [], options =
304
365
  } else {
305
366
  log.debug({ type: `da api info`, code: `ALK0194`, pre: `Status ${ response.status }. Cannot find ${ interview.uid }`});
306
367
  }
368
+
307
369
  } catch ( error ) {
308
370
  error.interview = interview;
309
371
  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.`
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 server = require('./docassemble/docassemble_api_interface');
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 server.is_server_responding({ timeout: scope.server_check_interval_ms });
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
 
@@ -2996,10 +2996,20 @@ module.exports = {
2996
2996
  // Save API key to clean up interviews later. Nice to have, not critical
2997
2997
  if ( api_key_secret_name ) {
2998
2998
  let api_key = process.env[ api_key_secret_name ];
2999
- let api_key_msg = `The api_key GitHub SECRET "${ api_key_secret_name }" doesn't exist. Check for a typo and check your workflow file.`
2999
+ let api_key_msg = `The API key called "${ api_key_secret_name }" is missing. Check for a typo in your test, workflow file, GitHub secret, or server config.`
3000
3000
  if ( api_key === undefined ) {
3001
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 called "${ 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 }".` });
3002
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 called "${ 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.
3003
3013
  scope.scenarios.get( scope.scenario_id ).api_keys.push( api_key );
3004
3014
  }
3005
3015
  }
package/lib/steps.js CHANGED
@@ -286,14 +286,14 @@ Given(
286
286
  { timeout: -1 },
287
287
  async ( email, password, api_key ) => {
288
288
  /** Uses the names of environment variables (most often GitHub SECRETs) to
289
- * log into an account on the user's server. Must be SECRETs to stay secure.
289
+ * log into an account on the user's server. Must be secure.
290
290
  *
291
291
  * Variations:
292
- * I sign into the server with the email "USER1_EMAIL" and the password "USER1_PASSWORD" SECRETs
293
- * I sign on with the email "USER1_EMAIL" and the password "USER1_PASSWORD" SECRETs
294
- * I log in with "USER1_EMAIL" and "USER1_PASSWORD"
295
- * I sign in with the email "USER1_EMAIL", the password "USER1_PASSWORD", and the API key "USER1_API_KEY"
296
- * I sign in with "USER1_EMAIL", "USER1_PASSWORD", "USER1_API_KEY"
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"
297
297
  */
298
298
  // `page` timeout will deal with custom timeout
299
299
  // Couldn't test the timeout for tapping the button because there's not
@@ -1205,11 +1205,11 @@ Then(/of two interviews, I delete one __internal__/i, async () => {
1205
1205
 
1206
1206
  // Check that both interviews exist on the server. 1 √ 2 √
1207
1207
  let api_keys = scope.scenarios.get( scope.scenario_id ).api_keys;
1208
- let existing_interviews = await da_api.get_interviews({ interviews, api_keys });
1209
- let existing_keys = Object.keys( existing_interviews );
1208
+ let visible_interviews = await da_api.get_interviews({ interviews, api_keys });
1209
+ let visible_keys = Object.keys( visible_interviews );
1210
1210
  expect(
1211
- existing_keys.length,
1212
- `2 interviews should be on the server. Instead: "${ existing_keys.join(', ') }"`
1211
+ visible_keys.length,
1212
+ `2 interviews should be on the server. Instead: "${ visible_keys.join(', ') }"`
1213
1213
  ).to.equal( 2 );
1214
1214
 
1215
1215
  // Delete one. 1 x 2 √
@@ -1233,24 +1233,23 @@ Then(/of two interviews, I delete one __internal__/i, async () => {
1233
1233
  // In After(), the other session should get deleted
1234
1234
  });
1235
1235
 
1236
- Then(/as an anonymous user, I fail to delete an interview and get no error __internal__/i, async () => {
1237
- /** Test that an interview passes even when unable to delete an interview.
1238
- * An API test
1239
- */
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 );
1240
1239
  // Make sure we started an interview
1241
1240
  let interviews = scope.scenarios.get( scope.scenario_id ).interviews;
1242
1241
  expect(
1243
1242
  Object.keys( interviews ).length,
1244
- `The test should have saved one interview session`
1245
- ).to.equal( 1 );
1243
+ `The test should have saved ${ num } interview session(s)`
1244
+ ).to.equal( num );
1246
1245
 
1247
1246
  // There should be no error about an unauthorized user
1248
1247
  let api_keys = scope.scenarios.get( scope.scenario_id ).api_keys;
1249
- let existing_interviews = await da_api.get_interviews({ interviews, api_keys });
1250
- let existing_keys = Object.keys( existing_interviews );
1248
+ let visible_interviews = await da_api.get_interviews({ interviews, api_keys });
1249
+ let visible_keys = Object.keys( visible_interviews );
1251
1250
  expect(
1252
- existing_keys.length,
1253
- `Anonymous user should detect 0 interviews on the server. Instead: "${ existing_keys.join(', ') }"`
1251
+ visible_keys.length,
1252
+ `User should detect 0 interviews on the server. Instead: "${ visible_keys.join(', ') }"`
1254
1253
  ).to.equal( 0 );
1255
1254
 
1256
1255
  // Deleting with an anonymous user should fail silently
@@ -1475,14 +1474,15 @@ After(async function(scenario) {
1475
1474
  let interviews = scope.scenarios.get( scope.scenario_id ).interviews;
1476
1475
  let api_keys = scope.scenarios.get( scope.scenario_id ).api_keys;
1477
1476
  await da_api.quietly_try_to_delete_interviews({ interviews, api_keys });
1478
- let existing_interviews = await da_api.get_interviews({ interviews, api_keys });
1479
- let existing_keys = Object.keys( existing_interviews );
1477
+ let visible_interviews = await da_api.get_interviews({ interviews, api_keys });
1478
+ let visible_keys = Object.keys( visible_interviews );
1480
1479
  // Deleting interviews is nice, but not non-critical. Don't bother the
1481
1480
  // user with it. Don't want to cover up a more meaningful failure (like
1482
1481
  // going to the wrong file). If it becomes a problem we can track it down later.
1483
- if ( existing_keys.length > 0 ) {
1482
+ if ( visible_keys.length > 0 ) {
1483
+ // Can an API key ever see an interview that it can't delete?
1484
1484
  log.debug({ type: `internal warning`, code: `ALK0197`,
1485
- pre: `${ existing_keys.length } interviews were not deleted: ${ existing_keys.join( ', ' ) }`
1485
+ pre: `The API key can see ${ visible_keys.length } interviews that were not deleted: ${ visible_keys.join( ', ' ) }`
1486
1486
  });
1487
1487
  }
1488
1488
  } catch ( error ) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suffolklitlab/alkiln",
3
- "version": "5.12.0-delete2",
3
+ "version": "5.12.0-delete3",
4
4
  "description": "Integrated automated end-to-end testing with docassemble, puppeteer, and cucumber.",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -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="./docassemble" --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