diff --git a/addons/api/addon/adapters/session-recording.js b/addons/api/addon/adapters/session-recording.js new file mode 100644 index 000000000..0531baa3a --- /dev/null +++ b/addons/api/addon/adapters/session-recording.js @@ -0,0 +1,55 @@ +import ApplicationAdapter from './application'; + +export default class SessionRecordingAdapter extends ApplicationAdapter { + /** + * Overrides the default `buildURL` such that for custom download methods, + * URLs will get the query parameters. All other request types behave as normal. + * + * @param modelName {string} + * @param id {string} + * @param snapshot {object} + * @param requestType {string} + * @param query {object} + * @returns {string} + */ + buildURL(modelName, id, snapshot, requestType, query) { + let url = super.buildURL(...arguments); + const method = snapshot?.adapterOptions?.method; + if (method === 'download') { + const queryParams = new URLSearchParams(query).toString(); + if (queryParams) url = `${url}?${queryParams}`; + } + return url; + } + + /** + * Handles the download recording for recordings. Takes in a mime type for the + * type or recording to return. + * The ID can be the session ID, session recording ID, + * connection recording ID, or the channel recording ID. + * @param id {string} + * @param mimeType {string} + * @returns {Promise} + */ + async getRecordingDownload({ id, mimeType }) { + const options = { adapterOptions: { method: 'download' } }; + const url = this.buildURL('session-recording', id, options, 'findRecord', { + mime_type: mimeType, + }); + + const response = await fetch(url); + return response.text(); + } + + /** + * Returns an ascii cast recording. + * @param id + * @returns {Promise} + */ + getAsciicast(id) { + return this.getRecordingDownload({ + id, + mimeType: 'application/x-asciicast', + }); + } +} diff --git a/addons/api/addon/models/channel-recording.js b/addons/api/addon/models/channel-recording.js index 6652b11fe..42c459170 100644 --- a/addons/api/addon/models/channel-recording.js +++ b/addons/api/addon/models/channel-recording.js @@ -24,4 +24,13 @@ export default class ChannelRecordingModel extends GeneratedChannelRecordingMode get isUnknown() { return !TYPES_CHANNEL_RECORDING.includes(this.type); } + + /** + * Download the ascii cast recording for this channel. + * @returns {Promise} + */ + getAsciicast() { + const adapter = this.store.adapterFor('session-recording'); + return adapter.getAsciicast(this.id); + } } diff --git a/addons/api/addon/models/session-recording.js b/addons/api/addon/models/session-recording.js index c1d8a4d83..db0714b51 100644 --- a/addons/api/addon/models/session-recording.js +++ b/addons/api/addon/models/session-recording.js @@ -27,26 +27,4 @@ export default class SessionRecordingModel extends GeneratedSessionRecordingMode get isUnknown() { return !TYPES_SESSION_RECORDING.includes(this.type); } - - /** - * Download the recording for either session, connection, or channels. - * id can be the session ID, session recording ID, connection recording ID, - * or the channel recording ID. - * @param {string} id - * @param {string} mimeType - * @returns {Promise} - */ - getAsciiCast(id) { - const adapter = this.store.adapterFor('application'); - const options = { adapterOptions: { method: 'download' } }; - const url = adapter.buildURL( - 'session-recording', - id, - options, - 'findRecord' - ); - return adapter.ajax(url, 'GET', { - data: { mimeType: 'application/x-asciicast' }, - }); - } } diff --git a/addons/api/app/adapters/session-recording.js b/addons/api/app/adapters/session-recording.js new file mode 100644 index 000000000..b8c3a26ce --- /dev/null +++ b/addons/api/app/adapters/session-recording.js @@ -0,0 +1 @@ +export { default } from 'api/adapters/session-recording'; diff --git a/addons/api/mirage/config.js b/addons/api/mirage/config.js index 69eb82f7a..e2a31192f 100644 --- a/addons/api/mirage/config.js +++ b/addons/api/mirage/config.js @@ -15,6 +15,7 @@ import { pickRandomStatusString } from './factories/session'; import initializeMockIPC from './scenarios/ipc'; import makeBooleanFilter from './helpers/bexpr-filter'; import { faker } from '@faker-js/faker'; +import { asciiCasts } from './data/asciicasts'; const isTesting = environmentConfig.environment === 'test'; @@ -688,7 +689,19 @@ function routes() { return sessionRecordings.where({ scopeId }); } ); - this.get('/session-recordings/:id'); + this.get( + '/session-recordings/:idMethod', + async ({ sessionRecordings }, { params: { idMethod } }) => { + const id = idMethod.split(':')[0]; + const method = idMethod.split(':')[1]; + + if (method === 'download') { + return faker.helpers.arrayElement(asciiCasts); + } else { + return sessionRecordings.find(id); + } + } + ); /* Uncomment the following line and the Response import above * Then change the response code to simulate error responses. diff --git a/addons/api/mirage/data/asciicasts.js b/addons/api/mirage/data/asciicasts.js new file mode 100644 index 000000000..99ce39286 --- /dev/null +++ b/addons/api/mirage/data/asciicasts.js @@ -0,0 +1,151 @@ +export const asciiCast1 = `{"version": 2, "width": 124, "height": 36, "timestamp": 1670352594, "env": {"SHELL": "/bin/zsh", "TERM": "xterm-256color"}} +[2.138652, "o", "\\u001b[1m\\u001b[7m%\\u001b[27m\\u001b[1m\\u001b[0m \\r \\r\\u001b]2;ember-asciinema-player\\u0007\\u001b]1;..iinema-player\\u0007"] +[2.220877, "o", "\\r\\u001b[0m\\u001b[27m\\u001b[24m\\u001b[J\\u001b[33member-asciinema-player\\u001b[00m \\u001b[32mgit\\u001b[00m:\\u001b[36m(main*)\\u001b[00m $ \\u001b[K"] +[2.221019, "o", "\\u001b[?1h\\u001b="] +[2.221076, "o", "\\u001b[?2004h"] +[2.676444, "o", "y"] +[2.788653, "o", "\\bya"] +[2.868652, "o", "r"] +[3.001764, "o", "n"] +[3.167636, "o", " "] +[3.313668, "o", "t"] +[3.460527, "o", "e"] +[3.561983, "o", "s"] +[3.63678, "o", "t"] +[3.853437, "o", "\\u001b[?1l\\u001b>"] +[3.853473, "o", "\\u001b[?2004l\\r\\r\\n"] +[3.854604, "o", "\\u001b]2;yarn test\\u0007\\u001b]1;yarn\\u0007"] +[4.489178, "o", "\\u001b[2K"] +[4.48982, "o", "\\u001b[1G\\u001b[1myarn run v1.22.19\\u001b[22m\\r\\n"] +[4.555618, "o", "\\u001b[2K"] +[4.555649, "o", "\\u001b[1G\\u001b[2m$ npm-run-all lint\\u001b[22m\\r\\n"] +[5.345253, "o", "\\u001b[2K"] +[5.345509, "o", "\\u001b[1G\\u001b[2m$ npm-run-all --print-name --aggregate-output --continue-on-error --parallel \\"lint:!(fix)\\"\\u001b[22m\\r\\n"] +[7.225926, "o", "\\r\\n> ember-asciinema-player@0.0.0 lint:js ember-asciinema-player/package.json\\r\\n> eslint . --cache \\r\\n\\r\\n$ eslint . --cache\\r\\n"] +[8.398641, "o", "\\r\\n> ember-asciinema-player@0.0.0 lint:hbs ember-asciinema-player/package.json\\r\\n> ember-template-lint . \\r\\n\\r\\n$ ember-template-lint .\\r\\n\\r\\n"] +[8.418537, "o", "\\u001b[2K\\u001b[1G\\u001b[2m$ ember test\\u001b[22m\\r\\n"] +[12.256156, "o", "\\u001b[?25l"] +[12.256464, "o", "\\u001b[32m⠋\\u001b[39m \\u001b[32mBuilding\\u001b[39m"] +[12.2573, "o", "\\u001b[2K"] +[12.257386, "o", "\\u001b[1G\\u001b[?25h"] +[12.257689, "o", "Environment: test\\r\\n"] +[12.257857, "o", "\\u001b[?25l\\u001b[32m⠋\\u001b[39m \\u001b[32mBuilding\\u001b[39m"] +[12.539035, "o", "\\u001b[2K"] +[12.539144, "o", "\\u001b[1G"] +[12.539219, "o", "\\u001b[32m⠙\\u001b[39m \\u001b[32mbuilding... \\u001b[39m"] +[12.745202, "o", "\\u001b[2K\\u001b[1G"] +[12.745343, "o", "\\u001b[32m⠹\\u001b[39m \\u001b[32mbuilding... \\u001b[39m[Babel: @ember/test-helpers > applyPatches]"] +[12.825004, "o", "\\u001b[2K"] +[12.825031, "o", "\\u001b[1G"] +[12.825122, "o", "\\u001b[32m⠸\\u001b[39m \\u001b[32mbuilding... \\u001b[39m[Babel: ember-qunit > applyPatches]"] +[12.933588, "o", "\\u001b[2K\\u001b[1G"] +[12.933624, "o", "\\u001b[32m⠼\\u001b[39m \\u001b[32mbuilding... \\u001b[39m[Babel: ember-load-initializers > applyPatches]"] +[13.194738, "o", "\\u001b[2K\\u001b[1G"] +[13.195009, "o", "\\u001b[32m⠴\\u001b[39m \\u001b[32mbuilding... \\u001b[39m[Babel: ember-asciinema-player > applyPatches]"] +[13.307878, "o", "\\u001b[2K\\u001b[1G\\u001b[32m⠦\\u001b[39m \\u001b[32mbuilding... \\u001b[39m[Package /assets/vendor.js]"] +[13.750717, "o", "\\u001b[2K\\u001b[1G"] +[13.750755, "o", "\\u001b[32m⠧\\u001b[39m \\u001b[32mbuilding... \\u001b[39m[Concat: Test Support JS]"] +[13.869411, "o", "\\u001b[2K\\u001b[1G"] +[13.869438, "o", "\\u001b[32m⠇\\u001b[39m \\u001b[32mbuilding... \\u001b[39m[ember-auto-import-webpack]"] +[14.655374, "o", "\\u001b[2K\\u001b[1G"] +[14.655415, "o", "\\u001b[32m⠏\\u001b[39m \\u001b[32mbuilding... \\u001b[39m[ember-auto-import-webpack]"] +[14.73686, "o", "\\u001b[2K\\u001b[1G\\u001b[?25h"] +[14.738533, "o", "\\u001b[?25h"] +[14.738818, "o", "\\u001b[?25l\\u001b[32m⠋\\u001b[39m cleaning up"] +[14.739061, "o", "\\u001b[2K\\u001b[1G\\u001b[?25hcleaning up...\\r\\n"] +[14.739197, "o", "\\u001b[?25l\\u001b[32m⠋\\u001b[39m cleaning up"] +[14.907985, "o", "\\u001b[2K"] +[14.90808, "o", "\\u001b[1G\\u001b[?25h"] +[14.908128, "o", "\\u001b[32mBuilt project successfully.\\u001b[39m\\r\\n"] +[16.224632, "o", "ok 1 Chrome 108.0 - [2 ms] - ember-qunit: Ember.onerror validation: Ember.onerror is functioning properly\\r\\n"] +[16.346568, "o", "\\r\\n1..1\\r\\n# tests 1\\r\\n# pass 1\\r\\n# skip 0\\r\\n# todo 0\\r\\n# fail 0\\r\\n\\r\\n# ok\\r\\n"] +[16.403167, "o", "\\u001b[2K"] +[16.403388, "o", "\\u001b[1G✨ Done in 11.92s.\\r\\n"] +[16.413967, "o", "\\u001b[1m\\u001b[7m%\\u001b[27m\\u001b[1m\\u001b[0m \\r \\r"] +[16.414147, "o", "\\u001b]2;ember-asciinema-player\\u0007\\u001b]1;..iinema-player\\u0007"] +[16.528341, "o", "\\r\\u001b[0m\\u001b[27m\\u001b[24m\\u001b[J\\u001b[33member-asciinema-player\\u001b[00m \\u001b[32mgit\\u001b[00m:\\u001b[36m(main*)\\u001b[00m $ \\u001b[K"] +[16.528501, "o", "\\u001b[?1h\\u001b="] +[16.52863, "o", "\\u001b[?2004h"] +[19.078794, "o", "\\u001b[?2004l\\r\\r\\n"]`; + +export const asciiCast2 = `{"version": 2, "width": 112, "height": 30, "timestamp": 1672793720, "env": {"SHELL": "/bin/bash", "TERM": "xterm-256color"}} +[0.036928, "o", "\\u001b[?2004h\\u001b[1;32m[\\u001b]0;johan@nixos: ~/src/hashicorp/boundary-enterprise\\u0007johan@nixos:~/src/hashicorp/boundary-enterprise]$\\u001b[0m "] +[2.827983, "o", "b"] +[2.9386, "o", "o"] +[2.97036, "o", "u"] +[3.032542, "o", "n"] +[3.108687, "o", "d"] +[3.246045, "o", "a"] +[3.340117, "o", "r"] +[3.841216, "o", "y"] +[4.314958, "o", " "] +[6.052102, "o", "u"] +[6.146486, "o", "s"] +[6.207341, "o", "e"] +[6.273087, "o", "r"] +[6.378873, "o", "s"] +[6.799687, "o", " "] +[7.08347, "o", "l"] +[7.159196, "o", "i"] +[7.249656, "o", "s"] +[7.352251, "o", "t"] +[7.450038, "o", " "] +[7.688195, "o", "-"] +[7.931818, "o", "r"] +[8.001542, "o", "e"] +[8.387771, "o", "c"] +[8.526339, "o", "u"] +[8.631438, "o", "r"] +[8.81899, "o", "s"] +[8.958184, "o", "i"] +[9.085688, "o", "v"] +[9.134885, "o", "e"] +[9.786868, "o", "\\r\\n"] +[9.788828, "o", "\\u001b[?2004l\\r"] +[10.086691, "o", "\\u001b[0m\\r\\nUser information:\\r\\n ID: u_anon\\r\\n Scope ID: global\\r\\n Version: 1\\r\\n Name: anonymous\\r\\n Description: The anonymous user matches any request, whether authenticated or not\\r\\n Authorized Actions:\\r\\n no-op\\r\\n read\\r\\n update\\r\\n delete\\r\\n add-accounts\\r\\n set-accounts\\r\\n remove-accounts\\r\\n\\r\\n ID: u_auth\\r\\n Scope ID: global\\r\\n Version: 1\\r\\n Name: authenticated\\r\\n Description: The authenticated user matches any user that has a valid token\\r\\n Authorized Actions:\\r\\n no-op\\r\\n read\\r\\n update\\r\\n delete\\r\\n add-accounts\\r\\n set-accounts\\r\\n remove-accounts\\r\\n\\r\\n ID: u_recovery\\r\\n Scope ID: global\\r\\n Version: 1\\r\\n Name: recovery\\r\\n Description: The recovery user is used for any request that was performed with the recovery KMS\\r\\n workflow\\r\\n Autho"] +[10.086736, "o", "rized Actions:\\r\\n no-op\\r\\n read\\r\\n update\\r\\n delete\\r\\n add-accounts\\r\\n set-accounts\\r\\n remove-accounts\\r\\n\\r\\n ID: u_1234567890\\r\\n Scope ID: global\\r\\n Version: 3\\r\\n Name: admin\\r\\n Description: Initial admin user within the \\"global\\" scope\\r\\n Primary Account ID: acctpw_1234567890\\r\\n Login Name: admin\\r\\n Authorized Actions:\\r\\n no-op\\r\\n read\\r\\n update\\r\\n delete\\r\\n add-accounts\\r\\n set-accounts\\r\\n remove-accounts\\r\\n\\r\\n ID: u_0987654321\\r\\n Scope ID: global\\r\\n Version: 3\\r\\n Name: user\\r\\n Description: Initial unprivileged user\\r\\n Primary Account ID: acctpw_0987654321\\r\\n Login Name: user\\r\\n Authorized Actions:\\r\\n no-op\\r\\n read\\r\\n update\\r\\n delete\\r\\n add-accounts\\r\\n set-accounts\\r\\n remove-accounts\\u001b[0m\\r\\n"] +[10.106453, "o", "\\u001b[?2004h\\u001b[1;32m[\\u001b]0;johan@nixos: ~/src/hashicorp/boundary-enterprise\\u0007johan@nixos:~/src/hashicorp/boundary-enterprise]$\\u001b[0m "] +[12.501496, "o", "b"] +[12.703134, "o", "o"] +[12.752198, "o", "u"] +[12.775329, "o", "n"] +[12.905261, "o", "d"] +[13.002373, "o", "a"] +[13.100111, "o", "r"] +[13.354779, "o", "y"] +[13.699827, "o", " "] +[13.998849, "o", "t"] +[14.139192, "o", "a"] +[14.19663, "o", "r"] +[14.392075, "o", "g"] +[14.489938, "o", "e"] +[14.579533, "o", "t"] +[14.674628, "o", "s"] +[14.830951, "o", " "] +[15.015311, "o", "l"] +[15.077147, "o", "i"] +[15.262221, "o", "s"] +[15.355571, "o", "t"] +[15.456597, "o", " "] +[15.91612, "o", "-"] +[16.264491, "o", "r"] +[16.351595, "o", "e"] +[16.798456, "o", "c"] +[16.891689, "o", "u"] +[16.997975, "o", "r"] +[17.176378, "o", "s"] +[17.290104, "o", "i"] +[17.434691, "o", "v"] +[17.482502, "o", "e"] +[18.393541, "o", "\\r\\n"] +[18.39558, "o", "\\u001b[?2004l\\r"] +[18.673452, "o", "\\u001b[0m\\r\\nTarget information:\\r\\n ID: ttcp_1234567890\\r\\n Scope ID: p_1234567890\\r\\n Version: 2\\r\\n Type: tcp\\r\\n Name: Generated target\\r\\n Description: Provides an initial target in Boundary\\r\\n Authorized Actions:\\r\\n no-op\\r\\n read\\r\\n update\\r\\n delete\\r\\n add-host-sources\\r\\n set-host-sources\\r\\n remove-host-sources\\r\\n add-credential-sources\\r\\n set-credential-sources\\r\\n remove-credential-sources\\r\\n authorize-session\\u001b[0m\\r\\n"] +[18.692528, "o", "\\u001b[?2004h\\u001b[1;32m[\\u001b]0;johan@nixos: ~/src/hashicorp/boundary-enterprise\\u0007johan@nixos:~/src/hashicorp/boundary-enterprise]$\\u001b[0m "] +[20.083966, "o", "e"] +[20.37301, "o", "x"] +[20.51445, "o", "i"] +[20.612885, "o", "t"] +[21.303896, "o", "\\r\\n"] +[21.307902, "o", "\\u001b[?2004l\\rexit\\r\\n"]`; + +export const asciiCasts = [asciiCast1, asciiCast2]; diff --git a/addons/api/tests/unit/adapters/session-recording-test.js b/addons/api/tests/unit/adapters/session-recording-test.js new file mode 100644 index 000000000..14696834e --- /dev/null +++ b/addons/api/tests/unit/adapters/session-recording-test.js @@ -0,0 +1,42 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Adapter | session recording', function (hooks) { + setupTest(hooks); + + test('it generates correct URLs for downloading asciicast', function (assert) { + assert.expect(1); + const mockSnapshot = { + adapterOptions: { + method: 'download', + }, + }; + const queryObject = { + mime_type: 'mimeType', + }; + const adapter = this.owner.lookup('adapter:session-recording'); + const createRecordURL = adapter.buildURL( + 'session-recording', + 'sr_id', + mockSnapshot, + 'findRecord', + queryObject + ); + assert.strictEqual( + createRecordURL, + '/v1/session-recordings/sr_id:download?mime_type=mimeType' + ); + }); + + test('it generates correct URLs for retrieving session-recordings', function (assert) { + assert.expect(1); + const adapter = this.owner.lookup('adapter:session-recording'); + const createRecordURL = adapter.buildURL( + 'session-recording', + 'sr_id', + {}, + 'findRecord' + ); + assert.strictEqual(createRecordURL, '/v1/session-recordings/sr_id'); + }); +});