@suffolklitlab/alkiln 5.12.0-delete1 → 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
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
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. */
@@ -54,16 +75,16 @@ da.delete_project = async function ( timeout ) {
54
75
 
55
76
 
56
77
  // node -e 'require("./lib/docassemble/docassemble_api_REST.js").delete_interview(...)'
57
- da.delete_interview = async function ( timeout, { uid = null }) {
78
+ da.delete_interview = async function ( timeout, { session = null, api_key = null }) {
58
79
  /* Delete an interview with the given session id. */
59
80
  let options = {
60
81
  method: 'DELETE',
61
- headers: { 'X-API-Key': session_vars.get_dev_api_key() },
82
+ headers: { 'X-API-Key': api_key },
62
83
  // Can't use `api/session` that uses the API key to identify the user's
63
84
  // interview sessions. The test may have signed in with an account
64
85
  // that's different than the ALKiln dev account that sends this request.
65
- url: `${ session_vars.get_da_server_url() }/api/interviews`,
66
- params: { session: uid },
86
+ url: `${ session_vars.get_da_server_url() }/api/user/interviews`,
87
+ params: { session },
67
88
  timeout: timeout,
68
89
  };
69
90
 
@@ -76,16 +97,16 @@ da.delete_interview = async function ( timeout, { uid = null }) {
76
97
  }; // Ends da.delete_interview()
77
98
 
78
99
  // node -e 'require("./lib/docassemble/docassemble_api_REST.js").get_interview(...)'
79
- da.get_interview = async function ( timeout, { uid = null }) {
100
+ da.get_interview = async function ( timeout, { session = null, api_key = null }) {
80
101
  /* Get a specific interview. */
81
102
  let options = {
82
103
  method: 'GET',
83
- headers: { 'X-API-Key': session_vars.get_dev_api_key() },
104
+ headers: { 'X-API-Key': api_key },
84
105
  // Can't use `/api/user/interviews` that uses the API key to identify the user's
85
106
  // interview sessions. The test may have signed in with an account
86
107
  // that's different than the ALKiln dev account that sends this request.
87
- url: `${ session_vars.get_da_server_url() }/api/interviews`,
88
- params: { session: uid },
108
+ url: `${ session_vars.get_da_server_url() }/api/user/interviews`,
109
+ params: { session },
89
110
  timeout: timeout,
90
111
  };
91
112
 
@@ -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
@@ -201,48 +262,75 @@ da_i.delete_project = async function ( delete_options ) {
201
262
  }; // Ends da_i.delete_project()
202
263
 
203
264
  // node -e 'require("./lib/docassemble/docassemble_api_interface.js").quietly_try_to_delete_interviews(...)'
204
- da_i.quietly_try_to_delete_interviews = async function ({ interviews = {}, options = {} }) {
205
- /** Delete a user's interview. Do we need to watch for a timeout? */
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? */
206
269
 
207
270
  // Defaults
208
271
  let { timeout } = options || { timeout: DEFAULT_MAX_REQUEST_MS };
209
272
 
273
+ let responses = [];
210
274
  for ( let id in interviews ) {
211
- let interview_vars = interviews[ id ];
212
- if ( interview_vars && interview_vars.uid ) {
213
- try {
214
- log.debug({ type: `da api info`, code: `ALK0188`, pre: `Trying to delete the interview with the id ${ interview_vars.uid }.` });
215
-
216
- // Actual work:
217
- let response = await _da_REST.delete_interview( timeout, interview_vars );
218
- log.debug({ type: `da api info`, code: `ALK0189`,
219
- pre: `No error when trying to delete interview ${ interview_vars.uid } at ${ interview_vars.i }.`
220
- });
221
-
222
- } catch ( error ) {
223
- error.interview_vars = interview_vars;
224
- let msg = `Error when deleting the interview. See https://docassemble.org/docs/api.html#session_delete.`
225
- log.debug({ type: `da api info`, code: `ALK0190`, pre: msg, data: error });
226
- }
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 );
227
282
  } else {
228
283
  log.debug({ type: `da api info`, code: `ALK0191`, pre: `There is no interview to delete.` });
229
284
  }
230
285
  }
286
+ return responses;
231
287
 
232
288
  }; // Ends da_i.quietly_try_to_delete_interviews()
233
289
 
290
+ // node -e 'require("./lib/docassemble/docassemble_api_interface.js").quietly_delete_one_interview(...)'
291
+ da_i.quietly_delete_one_interview = async function ( timeout, { interview = {}, api_keys = [] }) {
292
+ /** Try to delete an interview using all available API keys. Fail silently.
293
+ * Example failure: the test did not log in any users and the testing
294
+ * account's role prevents access to other user's interviews. */
295
+ api_keys = normalize_api_keys( api_keys );
296
+ let responses = [];
297
+ for ( let api_key of api_keys ) {
298
+ try {
299
+ log.debug({ type: `da api info`, code: `ALK0188`,
300
+ pre: `Trying to delete the interview with the id ${ interview.uid } and an API key.`
301
+ });
302
+
303
+ // Actual work:
304
+ let response = await _da_REST.delete_interview( timeout, {
305
+ session: interview.uid,
306
+ api_key
307
+ });
308
+ responses.push( response );
309
+
310
+ log.debug({ type: `da api info`, code: `ALK0189`,
311
+ pre: `No error when trying to delete interview ${ interview.uid } at ${ interview.i }.`
312
+ });
313
+ } catch ( error ) {
314
+ error.interview = interview;
315
+ let msg = `Error when deleting the interview. See https://docassemble.org/docs/api.html#session_delete.`
316
+ log.debug({ type: `da api info`, code: `ALK0190`, pre: msg, data: error });
317
+ }
318
+ } // ends for api keys
319
+ return responses;
320
+ }; // Ends da_i.quietly_delete_one_interview()
321
+
234
322
  // node -e 'require("./lib/docassemble/docassemble_api_interface.js").get_interviews(...)'
235
- da_i.get_interviews = async function ({ interviews = {}, options = {} }) {
323
+ da_i.get_interviews = async function ({ interviews = {}, api_keys = [], options = {} }) {
236
324
  /** Return an object of interviews that exist on the server
237
325
  * that match the given interviews. Keys are the session ids. */
238
- // Defaults
239
- let { timeout } = options || { timeout: DEFAULT_MAX_REQUEST_MS };
326
+
327
+ api_keys = normalize_api_keys( api_keys );
240
328
 
241
329
  let fetched_interviews = {};
242
330
  if ( interviews ) {
243
331
  for ( let id in interviews ) {
244
- let interview_vars = interviews[ id ];
245
- let fetched = await da_i.get_interview({ interview_vars });
332
+ let interview = interviews[ id ];
333
+ let fetched = await da_i.get_interview({ interview, api_keys, options });
246
334
  if ( fetched && fetched.session ) {
247
335
  fetched_interviews[ fetched.session ] = fetched;
248
336
  }
@@ -253,30 +341,37 @@ da_i.get_interviews = async function ({ interviews = {}, options = {} }) {
253
341
  }; // Ends da_i.get_interviews()
254
342
 
255
343
  // node -e 'require("./lib/docassemble/docassemble_api_interface.js").get_interview(...)'
256
- da_i.get_interview = async function ({ interview_vars, options = {} }) {
344
+ da_i.get_interview = async function ({ interview = {}, api_keys = [], options = {} }) {
257
345
  /** Try to get an interview from the server that matches the given interview data. */
258
346
 
259
347
  // Defaults
260
348
  let { timeout } = options || { timeout: DEFAULT_MAX_REQUEST_MS };
349
+ api_keys = normalize_api_keys( api_keys );
261
350
 
262
- if ( interview_vars && interview_vars.uid ) {
263
- try {
264
- log.debug({ type: `da api info`, code: `ALK0192`, pre: `Trying to get the interview with the id ${ interview_vars.uid }.` });
351
+ if ( interview && interview.uid ) {
352
+ for ( let api_key of api_keys ) {
353
+ try {
354
+ log.debug({ type: `da api info`, code: `ALK0192`, pre: `Trying to get the interview with the id ${ interview.uid } and an API key.` });
265
355
 
266
- // Actual work:
267
- let response = await _da_REST.get_interview( timeout, interview_vars );
268
- if ( response && response.data && response.data.items && response.data.items[0] && response.data.items[0].session ) {
269
- log.debug({ type: `da api info`, code: `ALK0193`, pre: `Status ${ response.status }. Got ${ response.data.items[0].session }.`});
270
- return response.data.items[0];
271
- } else {
272
- log.debug({ type: `da api info`, code: `ALK0194`, pre: `Status ${ response.status }. Cannot find ${ interview_vars.uid }`});
273
- }
356
+ // Actual work:
357
+ let response = await _da_REST.get_interview( timeout, {
358
+ session: interview.uid,
359
+ api_key
360
+ });
274
361
 
275
- } catch ( error ) {
276
- error.interview_vars = interview_vars;
277
- let msg = `${ error.status } "${ error.statusText }" Ran into an error when getting the interview ${ interview_vars.uid }. See https://docassemble.org/docs/api.html#interviews.`
278
- log.debug({ type: `da api info`, code: `ALK0195`, pre: msg, data: error });
279
- }
362
+ if ( response && response.data && response.data.items && response.data.items[0] && response.data.items[0].session ) {
363
+ log.debug({ type: `da api info`, code: `ALK0193`, pre: `Status ${ response.status }. Got ${ response.data.items[0].session }.`});
364
+ return response.data.items[0];
365
+ } else {
366
+ log.debug({ type: `da api info`, code: `ALK0194`, pre: `Status ${ response.status }. Cannot find ${ interview.uid }`});
367
+ }
368
+
369
+ } catch ( error ) {
370
+ error.interview = interview;
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.`
372
+ log.debug({ type: `da api info`, code: `ALK0195`, pre: msg, data: error });
373
+ }
374
+ } // ends for api_keys
280
375
  } else {
281
376
  log.debug({ type: `da api info`, code: `ALK0196`, pre: `There is no interview to get.` });
282
377
  }
@@ -284,3 +379,11 @@ da_i.get_interview = async function ({ interview_vars, options = {} }) {
284
379
  return null;
285
380
 
286
381
  }; // Ends da_i.get_interview()
382
+
383
+ let normalize_api_keys = function ( api_keys = [] ) {
384
+ /** Ensure the dev API key is in the api_keys list. */
385
+ let dev_key = session_vars.get_dev_api_key();
386
+ if ( !api_keys ) { api_keys = [ dev_key ]; }
387
+ if ( !api_keys.includes( dev_key ) ) { api_keys.unshift( dev_key ); }
388
+ return api_keys;
389
+ };
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
 
@@ -2938,7 +2938,9 @@ module.exports = {
2938
2938
  await scope.afterStep(scope, {waitForShowIf: true});
2939
2939
  }, // Ends scope.steps.set_al_address()
2940
2940
 
2941
- sign_in: async function ( scope, { email_secret_name, password_secret_name }) {
2941
+ sign_in: async function ( scope, {
2942
+ email_secret_name, password_secret_name, api_key_secret_name
2943
+ }) {
2942
2944
  /** Allow the developer to log a user into their server using GitHub
2943
2945
  * secrets to authenticate.
2944
2946
  */
@@ -2991,6 +2993,26 @@ module.exports = {
2991
2993
  if ( password === undefined ) {
2992
2994
  reports.addToReport( scope, { type: `error`, code: `ALK0162`, value: password_msg })
2993
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 called "${ 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 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 }".` });
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.
3013
+ scope.scenarios.get( scope.scenario_id ).api_keys.push( api_key );
3014
+ }
3015
+ }
2994
3016
 
2995
3017
  expect( email, email_msg ).to.not.equal( undefined );
2996
3018
  expect( password, password_msg ).to.not.equal( undefined );
package/lib/steps.js CHANGED
@@ -140,6 +140,7 @@ Before(async (scenario) => {
140
140
  scope.scenario_id = uuidv4();
141
141
  scope.scenarios.set( scope.scenario_id, {} );
142
142
  scope.scenarios.get( scope.scenario_id ).interviews = {};
143
+ scope.scenarios.get( scope.scenario_id ).api_keys = [ session_vars.get_dev_api_key() ];
143
144
  scope.expected_status = null;
144
145
  scope.expected_in_report = null;
145
146
  scope.expected_not_in_report = null;
@@ -280,21 +281,27 @@ Given(/I go to "([^"]+)"/i, {timeout: -1}, async ( url ) => {
280
281
  return result;
281
282
  });
282
283
 
283
- Given(/I (?:sign|log) ?(?:in)?(?:on)?(?:to the server)? with(?: the email)? "([^"]+)" and(?: the password)? "([^"]+)"(?: SECRETs)?/i, {timeout: -1}, async ( email, password ) => {
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 ) => {
284
288
  /** Uses the names of environment variables (most often GitHub SECRETs) to
285
- * 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.
286
290
  *
287
291
  * Variations:
288
- * I sign into the server with the email "USER1_EMAIL" and the password "USER1_PASSWORD" SECRETs
289
- * I sign on with the email "USER1_EMAIL" and the password "USER1_PASSWORD" SECRETs
290
- * I log in with "USER1_EMAIL" and "USER1_PASSWORD"
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"
291
297
  */
292
298
  // `page` timeout will deal with custom timeout
293
299
  // Couldn't test the timeout for tapping the button because there's not
294
300
  // enough of a pause between initial navigation and pressing button.
295
301
  await scope.steps.sign_in( scope, {
296
302
  email_secret_name: email,
297
- password_secret_name: password
303
+ password_secret_name: password,
304
+ api_key_secret_name: api_key
298
305
  });
299
306
  });
300
307
 
@@ -1190,24 +1197,26 @@ Then(/of two interviews, I delete one __internal__/i, async () => {
1190
1197
  */
1191
1198
  // Make sure we have two interviews in `scope.scenario.interviews` (two session ids)
1192
1199
  let interviews = scope.scenarios.get( scope.scenario_id ).interviews;
1193
- let internal_keys = Object.keys( interviews );
1200
+ let interview_keys = Object.keys( interviews );
1194
1201
  expect(
1195
- internal_keys.length,
1196
- `Of 2 interview sessions, ALKiln only saved ${ internal_keys.length }`
1202
+ interview_keys.length,
1203
+ `Of 2 interview sessions, ALKiln only saved ${ interview_keys.length }`
1197
1204
  ).to.equal( 2 );
1198
1205
 
1199
1206
  // Check that both interviews exist on the server. 1 √ 2 √
1200
- let existing_interviews = await da_api.get_interviews({ interviews });
1201
- let existing_keys = Object.keys( existing_interviews );
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 );
1202
1210
  expect(
1203
- existing_keys.length,
1204
- `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(', ') }"`
1205
1213
  ).to.equal( 2 );
1206
1214
 
1207
1215
  // Delete one. 1 x 2 √
1208
- let to_delete = interviews[ internal_keys[ 0 ]];
1216
+ let to_delete = interviews[ interview_keys[ 0 ]];
1209
1217
  await da_api.quietly_try_to_delete_interviews({
1210
- interviews: { [internal_keys[ 0 ]]: to_delete }
1218
+ interviews: { [interview_keys[ 0 ]]: to_delete },
1219
+ api_keys
1211
1220
  });
1212
1221
 
1213
1222
  // Check that only one session exists on the server. This tests that
@@ -1215,7 +1224,7 @@ Then(/of two interviews, I delete one __internal__/i, async () => {
1215
1224
  // 2. We are fetching interviews correctly because we are getting one
1216
1225
  // back (e.g. not passing null into the api function)
1217
1226
  // 1 x 2 √
1218
- let after_deletion = await da_api.get_interviews({ interviews });
1227
+ let after_deletion = await da_api.get_interviews({ interviews, api_keys });
1219
1228
  let keys_after_deletion = Object.keys( after_deletion );
1220
1229
  expect(
1221
1230
  keys_after_deletion.length,
@@ -1224,16 +1233,28 @@ Then(/of two interviews, I delete one __internal__/i, async () => {
1224
1233
  // In After(), the other session should get deleted
1225
1234
  });
1226
1235
 
1227
- // Then(/there are no interview_vars saved __internal__/i, async () => {
1228
- // /** Test that no interviews were saved internally */
1229
- // // Check the number of interviews
1230
- // let interviews = scope.scenarios.get( scope.scenario_id ).interviews;
1231
- // let keys = Object.keys( interviews );
1232
- // expect(
1233
- // keys.length,
1234
- // `0 interviews should exist, but at least one does: ${ keys.join(', ') }`
1235
- // ).to.equal( 0 );
1236
- // });
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
+ });
1237
1258
 
1238
1259
 
1239
1260
  //#####################################
@@ -1446,25 +1467,34 @@ After(async function(scenario) {
1446
1467
 
1447
1468
  // Now that we're no longer on the interview page, one way or
1448
1469
  // another, try to delete the interviews created during the test
1449
- try {
1450
- let interviews = scope.scenarios.get( scope.scenario_id ).interviews;
1451
- await da_api.quietly_try_to_delete_interviews({ interviews });
1452
- let existing_interviews = await da_api.get_interviews({ interviews });
1453
- let existing_keys = Object.keys( existing_interviews );
1454
- // Deleting interviews is nice, but not non-critical. Don't bother the
1455
- // user with it. Don't want to cover up a more meaningful failure (like
1456
- // going to the wrong file). If it becomes a problem we can track it down later.
1457
- if ( existing_keys.length > 0 ) {
1458
- log.debug({ type: `internal warning`, code: `ALK0197`,
1459
- pre: `${ existing_keys.length } interviews were not deleted: ${ existing_keys.join( ', ' ) }`
1460
- });
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.debug({ 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.debug({ type: `internal warning`, code: `ALK0198`, pre: `Error while cleaning up interviews.`, data: error });
1461
1497
  }
1462
- } catch ( error ) {
1463
- // Deleting interviews is nice, but not non-critical. Don't bother the
1464
- // user with it. Don't want to cover up a more meaningful failure (like
1465
- // going to the wrong file). If it becomes a problem we can track it down later.
1466
- // Consider add "internal" to docs: "If you see this, make an issue in ALKiln with your debug/verbose log"
1467
- log.debug({ type: `internal warning`, code: `ALK0198`, pre: `Error while cleaning up interviews.`, data: error });
1468
1498
  }
1469
1499
 
1470
1500
  // Handle all the accumulated errors
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suffolklitlab/alkiln",
3
- "version": "5.12.0-delete1",
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