@suffolklitlab/alkiln 5.12.0-delete1 → 5.12.0-delete2

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/CHANGELOG.md CHANGED
@@ -51,11 +51,11 @@ Format:
51
51
 
52
52
  ### Fixed
53
53
 
54
- - Now delete interviews upon interview completion
54
+ - Now delete interviews upon interview completion for signed in users
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
59
  - 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
60
 
61
61
  ## [5.12.0] - 2024-06-06
@@ -54,16 +54,16 @@ da.delete_project = async function ( timeout ) {
54
54
 
55
55
 
56
56
  // node -e 'require("./lib/docassemble/docassemble_api_REST.js").delete_interview(...)'
57
- da.delete_interview = async function ( timeout, { uid = null }) {
57
+ da.delete_interview = async function ( timeout, { session = null, api_key = null }) {
58
58
  /* Delete an interview with the given session id. */
59
59
  let options = {
60
60
  method: 'DELETE',
61
- headers: { 'X-API-Key': session_vars.get_dev_api_key() },
61
+ headers: { 'X-API-Key': api_key },
62
62
  // Can't use `api/session` that uses the API key to identify the user's
63
63
  // interview sessions. The test may have signed in with an account
64
64
  // 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 },
65
+ url: `${ session_vars.get_da_server_url() }/api/user/interviews`,
66
+ params: { session },
67
67
  timeout: timeout,
68
68
  };
69
69
 
@@ -76,16 +76,16 @@ da.delete_interview = async function ( timeout, { uid = null }) {
76
76
  }; // Ends da.delete_interview()
77
77
 
78
78
  // node -e 'require("./lib/docassemble/docassemble_api_REST.js").get_interview(...)'
79
- da.get_interview = async function ( timeout, { uid = null }) {
79
+ da.get_interview = async function ( timeout, { session = null, api_key = null }) {
80
80
  /* Get a specific interview. */
81
81
  let options = {
82
82
  method: 'GET',
83
- headers: { 'X-API-Key': session_vars.get_dev_api_key() },
83
+ headers: { 'X-API-Key': api_key },
84
84
  // Can't use `/api/user/interviews` that uses the API key to identify the user's
85
85
  // interview sessions. The test may have signed in with an account
86
86
  // 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 },
87
+ url: `${ session_vars.get_da_server_url() }/api/user/interviews`,
88
+ params: { session },
89
89
  timeout: timeout,
90
90
  };
91
91
 
@@ -201,48 +201,75 @@ da_i.delete_project = async function ( delete_options ) {
201
201
  }; // Ends da_i.delete_project()
202
202
 
203
203
  // 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? */
204
+ da_i.quietly_try_to_delete_interviews = async function ({ interviews = {}, api_keys = [], options = {} }) {
205
+ /** Try to delete the given interviews. Fail silently. Example failure: the test
206
+ * did not log in any users and the testing account's role prevents
207
+ * access to other user's interviews. Do we need to watch for a timeout? */
206
208
 
207
209
  // Defaults
208
210
  let { timeout } = options || { timeout: DEFAULT_MAX_REQUEST_MS };
209
211
 
212
+ let responses = [];
210
213
  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
- }
214
+ let interview = interviews[ id ];
215
+ if ( interview && interview.uid ) {
216
+ let sub_responses = await da_i.quietly_delete_one_interview( timeout, {
217
+ interview,
218
+ api_keys
219
+ });
220
+ responses.push( sub_responses );
227
221
  } else {
228
222
  log.debug({ type: `da api info`, code: `ALK0191`, pre: `There is no interview to delete.` });
229
223
  }
230
224
  }
225
+ return responses;
231
226
 
232
227
  }; // Ends da_i.quietly_try_to_delete_interviews()
233
228
 
229
+ // node -e 'require("./lib/docassemble/docassemble_api_interface.js").quietly_delete_one_interview(...)'
230
+ da_i.quietly_delete_one_interview = async function ( timeout, { interview = {}, api_keys = [] }) {
231
+ /** Try to delete an interview using all available API keys. Fail silently.
232
+ * Example failure: the test did not log in any users and the testing
233
+ * account's role prevents access to other user's interviews. */
234
+ api_keys = normalize_api_keys( api_keys );
235
+ let responses = [];
236
+ for ( let api_key of api_keys ) {
237
+ try {
238
+ log.debug({ type: `da api info`, code: `ALK0188`,
239
+ pre: `Trying to delete the interview with the id ${ interview.uid } and an API key.`
240
+ });
241
+
242
+ // Actual work:
243
+ let response = await _da_REST.delete_interview( timeout, {
244
+ session: interview.uid,
245
+ api_key
246
+ });
247
+ responses.push( response );
248
+
249
+ log.debug({ type: `da api info`, code: `ALK0189`,
250
+ pre: `No error when trying to delete interview ${ interview.uid } at ${ interview.i }.`
251
+ });
252
+ } catch ( error ) {
253
+ error.interview = interview;
254
+ let msg = `Error when deleting the interview. See https://docassemble.org/docs/api.html#session_delete.`
255
+ log.debug({ type: `da api info`, code: `ALK0190`, pre: msg, data: error });
256
+ }
257
+ } // ends for api keys
258
+ return responses;
259
+ }; // Ends da_i.quietly_delete_one_interview()
260
+
234
261
  // node -e 'require("./lib/docassemble/docassemble_api_interface.js").get_interviews(...)'
235
- da_i.get_interviews = async function ({ interviews = {}, options = {} }) {
262
+ da_i.get_interviews = async function ({ interviews = {}, api_keys = [], options = {} }) {
236
263
  /** Return an object of interviews that exist on the server
237
264
  * that match the given interviews. Keys are the session ids. */
238
- // Defaults
239
- let { timeout } = options || { timeout: DEFAULT_MAX_REQUEST_MS };
265
+
266
+ api_keys = normalize_api_keys( api_keys );
240
267
 
241
268
  let fetched_interviews = {};
242
269
  if ( interviews ) {
243
270
  for ( let id in interviews ) {
244
- let interview_vars = interviews[ id ];
245
- let fetched = await da_i.get_interview({ interview_vars });
271
+ let interview = interviews[ id ];
272
+ let fetched = await da_i.get_interview({ interview, api_keys, options });
246
273
  if ( fetched && fetched.session ) {
247
274
  fetched_interviews[ fetched.session ] = fetched;
248
275
  }
@@ -253,30 +280,36 @@ da_i.get_interviews = async function ({ interviews = {}, options = {} }) {
253
280
  }; // Ends da_i.get_interviews()
254
281
 
255
282
  // node -e 'require("./lib/docassemble/docassemble_api_interface.js").get_interview(...)'
256
- da_i.get_interview = async function ({ interview_vars, options = {} }) {
283
+ da_i.get_interview = async function ({ interview = {}, api_keys = [], options = {} }) {
257
284
  /** Try to get an interview from the server that matches the given interview data. */
258
285
 
259
286
  // Defaults
260
287
  let { timeout } = options || { timeout: DEFAULT_MAX_REQUEST_MS };
288
+ api_keys = normalize_api_keys( api_keys );
261
289
 
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 }.` });
290
+ if ( interview && interview.uid ) {
291
+ for ( let api_key of api_keys ) {
292
+ try {
293
+ log.debug({ type: `da api info`, code: `ALK0192`, pre: `Trying to get the interview with the id ${ interview.uid } and an API key.` });
265
294
 
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
- }
295
+ // Actual work:
296
+ let response = await _da_REST.get_interview( timeout, {
297
+ session: interview.uid,
298
+ api_key
299
+ });
274
300
 
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
- }
301
+ if ( response && response.data && response.data.items && response.data.items[0] && response.data.items[0].session ) {
302
+ log.debug({ type: `da api info`, code: `ALK0193`, pre: `Status ${ response.status }. Got ${ response.data.items[0].session }.`});
303
+ return response.data.items[0];
304
+ } else {
305
+ log.debug({ type: `da api info`, code: `ALK0194`, pre: `Status ${ response.status }. Cannot find ${ interview.uid }`});
306
+ }
307
+ } catch ( error ) {
308
+ error.interview = interview;
309
+ 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.`
310
+ log.debug({ type: `da api info`, code: `ALK0195`, pre: msg, data: error });
311
+ }
312
+ } // ends for api_keys
280
313
  } else {
281
314
  log.debug({ type: `da api info`, code: `ALK0196`, pre: `There is no interview to get.` });
282
315
  }
@@ -284,3 +317,11 @@ da_i.get_interview = async function ({ interview_vars, options = {} }) {
284
317
  return null;
285
318
 
286
319
  }; // Ends da_i.get_interview()
320
+
321
+ let normalize_api_keys = function ( api_keys = [] ) {
322
+ /** Ensure the dev API key is in the api_keys list. */
323
+ let dev_key = session_vars.get_dev_api_key();
324
+ if ( !api_keys ) { api_keys = [ dev_key ]; }
325
+ if ( !api_keys.includes( dev_key ) ) { api_keys.unshift( dev_key ); }
326
+ return api_keys;
327
+ };
package/lib/scope.js CHANGED
@@ -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,16 @@ 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 GitHub SECRET "${ api_key_secret_name }" doesn't exist. Check for a typo and check your workflow file.`
3000
+ if ( api_key === undefined ) {
3001
+ reports.addToReport( scope, { type: `warning`, code: `ALK0199`, value: api_key_msg });
3002
+ } else {
3003
+ scope.scenarios.get( scope.scenario_id ).api_keys.push( api_key );
3004
+ }
3005
+ }
2994
3006
 
2995
3007
  expect( email, email_msg ).to.not.equal( undefined );
2996
3008
  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,7 +281,10 @@ 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
289
  * log into an account on the user's server. Must be SECRETs to stay secure.
286
290
  *
@@ -288,13 +292,16 @@ Given(/I (?:sign|log) ?(?:in)?(?:on)?(?:to the server)? with(?: the email)? "([^
288
292
  * I sign into the server with the email "USER1_EMAIL" and the password "USER1_PASSWORD" SECRETs
289
293
  * I sign on with the email "USER1_EMAIL" and the password "USER1_PASSWORD" SECRETs
290
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"
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 });
1207
+ let api_keys = scope.scenarios.get( scope.scenario_id ).api_keys;
1208
+ let existing_interviews = await da_api.get_interviews({ interviews, api_keys });
1201
1209
  let existing_keys = Object.keys( existing_interviews );
1202
1210
  expect(
1203
1211
  existing_keys.length,
1204
- `2 interviews should be on the server. Instead: ${ existing_keys.join(', ') }`
1212
+ `2 interviews should be on the server. Instead: "${ existing_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,29 @@ 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(/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
+ */
1240
+ // Make sure we started an interview
1241
+ let interviews = scope.scenarios.get( scope.scenario_id ).interviews;
1242
+ expect(
1243
+ Object.keys( interviews ).length,
1244
+ `The test should have saved one interview session`
1245
+ ).to.equal( 1 );
1246
+
1247
+ // There should be no error about an unauthorized user
1248
+ 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 );
1251
+ expect(
1252
+ existing_keys.length,
1253
+ `Anonymous user should detect 0 interviews on the server. Instead: "${ existing_keys.join(', ') }"`
1254
+ ).to.equal( 0 );
1255
+
1256
+ // Deleting with an anonymous user should fail silently
1257
+ await da_api.quietly_try_to_delete_interviews({ interviews, api_keys });
1258
+ });
1237
1259
 
1238
1260
 
1239
1261
  //#####################################
@@ -1446,25 +1468,33 @@ After(async function(scenario) {
1446
1468
 
1447
1469
  // Now that we're no longer on the interview page, one way or
1448
1470
  // 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
- });
1471
+ // Authors running this manually get to choose what to do with
1472
+ // the interviews they're creating unless we hear otherwise.
1473
+ if ( session_vars.get_origin() !== `playground` ) {
1474
+ try {
1475
+ let interviews = scope.scenarios.get( scope.scenario_id ).interviews;
1476
+ let api_keys = scope.scenarios.get( scope.scenario_id ).api_keys;
1477
+ 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 );
1480
+ // Deleting interviews is nice, but not non-critical. Don't bother the
1481
+ // user with it. Don't want to cover up a more meaningful failure (like
1482
+ // going to the wrong file). If it becomes a problem we can track it down later.
1483
+ if ( existing_keys.length > 0 ) {
1484
+ log.debug({ type: `internal warning`, code: `ALK0197`,
1485
+ pre: `${ existing_keys.length } interviews were not deleted: ${ existing_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-delete2",
4
4
  "description": "Integrated automated end-to-end testing with docassemble, puppeteer, and cucumber.",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {