44 */
55
66import { MiscUtils } from '@opensearch-dashboards-test/opensearch-dashboards-test-library' ;
7+ import { BASE_PATH } from '../../../base_constants' ;
78
89export const WorkspaceDeleteTestCases = ( ) => {
910 const miscUtils = new MiscUtils ( cy ) ;
@@ -87,7 +88,7 @@ export const WorkspaceDeleteTestCases = () => {
8788 expect ( interception . response . statusCode ) . to . equal ( 200 ) ;
8889 } ) ;
8990 cy . location ( 'pathname' ) . should ( 'include' , 'app/workspace_list' ) ;
90- cy . contains ( 'Delete workspace successfully' ) . should ( 'be.visible' ) ;
91+ cy . contains ( 'workspaces deleted successfully' ) . should ( 'be.visible' ) ;
9192 cy . contains ( workspace1Name ) . should ( 'not.exist' ) ;
9293 } ) ;
9394 } ) ;
@@ -109,7 +110,7 @@ export const WorkspaceDeleteTestCases = () => {
109110 expect ( interception . response . statusCode ) . to . equal ( 200 ) ;
110111 } ) ;
111112 cy . location ( 'pathname' ) . should ( 'include' , 'app/workspace_list' ) ;
112- cy . contains ( 'Delete workspace successfully' ) . should ( 'be.visible' ) ;
113+ cy . contains ( 'workspaces deleted successfully' ) . should ( 'be.visible' ) ;
113114 cy . contains ( workspace1Name ) . should ( 'not.exist' ) ;
114115 } ) ;
115116
@@ -131,7 +132,7 @@ export const WorkspaceDeleteTestCases = () => {
131132 expect ( interception . response . statusCode ) . to . equal ( 200 ) ;
132133 } ) ;
133134 cy . location ( 'pathname' ) . should ( 'include' , 'app/workspace_list' ) ;
134- cy . contains ( 'Delete workspace successfully' ) . should ( 'be.visible' ) ;
135+ cy . contains ( 'workspaces deleted successfully' ) . should ( 'be.visible' ) ;
135136 cy . contains ( workspace1Name ) . should ( 'not.exist' ) ;
136137 cy . contains ( workspace2Name ) . should ( 'not.exist' ) ;
137138 } ) ;
@@ -176,10 +177,322 @@ export const WorkspaceDeleteTestCases = () => {
176177 cy . wait ( '@deleteWorkspace1Request' ) . then ( ( interception ) => {
177178 expect ( interception . response . statusCode ) . to . equal ( 200 ) ;
178179 } ) ;
179- cy . contains ( 'Delete workspace successfully' ) . should ( 'be.visible' ) ;
180+ cy . contains ( 'workspaces deleted successfully' ) . should ( 'be.visible' ) ;
180181 cy . location ( 'pathname' ) . should ( 'include' , 'app/workspace_list' ) ;
181182 cy . contains ( workspace1Name ) . should ( 'not.exist' ) ;
182183 } ) ;
183184 } ) ;
185+
186+ describe ( 'Delete workspace with saved objects cleanup verification' , ( ) => {
187+ const testWorkspaceName = 'test_workspace_saved_objects_cleanup' ;
188+ let testWorkspaceId ;
189+ let datasourceId = '' ;
190+
191+ before ( ( ) => {
192+ cy . deleteWorkspaceByName ( testWorkspaceName ) ;
193+
194+ if ( Cypress . env ( 'DATASOURCE_MANAGEMENT_ENABLED' ) ) {
195+ cy . createDataSourceNoAuth ( ) . then ( ( result ) => {
196+ datasourceId = result [ 0 ] ;
197+ expect ( datasourceId ) . to . be . a ( 'string' ) . that . is . not . empty ;
198+ } ) ;
199+ }
200+ } ) ;
201+
202+ beforeEach ( ( ) => {
203+ cy . createWorkspace ( {
204+ name : testWorkspaceName ,
205+ description : 'Test workspace for saved objects cleanup verification' ,
206+ features : [ 'use-case-observability' ] ,
207+ settings : {
208+ permissions : {
209+ library_write : { users : [ '%me%' ] } ,
210+ write : { users : [ '%me%' ] } ,
211+ } ,
212+ ...( datasourceId ? { dataSources : [ datasourceId ] } : { } ) ,
213+ } ,
214+ } ) . then ( ( value ) => {
215+ testWorkspaceId = value ;
216+ } ) ;
217+ } ) ;
218+
219+ afterEach ( ( ) => {
220+ cy . deleteWorkspaceByName ( testWorkspaceName ) ;
221+ } ) ;
222+
223+ after ( ( ) => {
224+ if ( Cypress . env ( 'DATASOURCE_MANAGEMENT_ENABLED' ) && datasourceId ) {
225+ cy . deleteDataSource ( datasourceId ) ;
226+ }
227+ } ) ;
228+
229+ it ( 'should automatically delete saved objects when workspace is deleted' , ( ) => {
230+ // Add sample data to create saved objects in the workspace
231+ cy . loadSampleDataForWorkspace (
232+ 'ecommerce' ,
233+ testWorkspaceId ,
234+ datasourceId
235+ ) ;
236+
237+ // Verify saved objects exist in the workspace
238+ cy . visit ( `${ BASE_PATH } /w/${ testWorkspaceId } /app/objects` ) ;
239+ cy . getElementByTestId ( 'savedObjectsTableRowTitle' ) . should ( 'exist' ) ;
240+
241+ // Search for ecommerce sample data objects
242+ cy . getElementByTestId ( 'savedObjectSearchBar' )
243+ . type ( 'opensearch_dashboards_sample_data_ecommerce{enter}' )
244+ . trigger ( 'search' ) ;
245+
246+ // Verify we have saved objects
247+ cy . getElementByTestId ( 'savedObjectsTableRowTitle' ) . should (
248+ 'have.length.greaterThan' ,
249+ 0
250+ ) ;
251+
252+ // Get the saved object IDs that are associated with this workspace
253+ cy . request ( {
254+ url : `${ BASE_PATH } /api/opensearch-dashboards/management/saved_objects/_find?workspaces=${ testWorkspaceId } &page=1&perPage=100&type=index-pattern&type=visualization&type=dashboard&type=search&type=config` ,
255+ headers : {
256+ 'Osd-Xsrf' : 'osd-fetch' ,
257+ } ,
258+ } ) . then ( ( resp ) => {
259+ const savedObjectsInWorkspace = resp . body . saved_objects ;
260+ expect ( savedObjectsInWorkspace ) . to . have . length . greaterThan ( 0 ) ;
261+
262+ // Store the saved object IDs for verification after deletion
263+ const savedObjectIds = savedObjectsInWorkspace . map ( ( obj ) => ( {
264+ type : obj . type ,
265+ id : obj . id ,
266+ } ) ) ;
267+
268+ // Delete the workspace
269+ cy . visit ( `${ BASE_PATH } /app/workspace_list` ) ;
270+ cy . contains ( testWorkspaceName ) . should ( 'be.visible' ) ;
271+
272+ // Find and click the delete button for this workspace
273+ cy . getElementByTestId ( `checkboxSelectRow-${ testWorkspaceId } ` )
274+ . parents ( 'tr' )
275+ . within ( ( ) => {
276+ cy . getElementByTestId ( 'euiCollapsedItemActionsButton' ) . click ( ) ;
277+ } ) ;
278+
279+ cy . getElementByTestId ( 'workspace-list-delete-icon' ) . should (
280+ 'be.visible'
281+ ) ;
282+ cy . getElementByTestId ( 'workspace-list-delete-icon' ) . click ( ) ;
283+
284+ // Confirm deletion
285+ cy . contains ( 'Delete workspace' ) . should ( 'be.visible' ) ;
286+ cy . getElementByTestId ( 'delete-workspace-modal-input' ) . type ( 'delete' ) ;
287+ cy . getElementByTestId ( 'delete-workspace-modal-confirm' ) . click ( ) ;
288+
289+ // Wait for deletion to complete
290+ cy . contains ( 'workspaces deleted successfully' ) . should ( 'be.visible' ) ;
291+ cy . contains ( testWorkspaceName ) . should ( 'not.exist' ) ;
292+
293+ // Check each saved object to ensure it no longer exists
294+ savedObjectIds . forEach ( ( { type, id } ) => {
295+ cy . request ( {
296+ method : 'GET' ,
297+ url : `${ BASE_PATH } /api/saved_objects/${ type } /${ id } ` ,
298+ failOnStatusCode : false ,
299+ headers : {
300+ 'Osd-Xsrf' : 'osd-fetch' ,
301+ } ,
302+ } ) . then ( ( response ) => {
303+ // The saved object should be deleted (404) or not accessible
304+ expect ( response . status ) . to . be . oneOf ( [ 404 , 403 ] ) ;
305+ } ) ;
306+ } ) ;
307+
308+ // Verify saved objects are not visible in global saved objects management
309+ cy . visit ( `${ BASE_PATH } /app/objects` ) ;
310+
311+ // Search for the ecommerce sample data that was in the deleted workspace
312+ cy . getElementByTestId ( 'savedObjectSearchBar' )
313+ . clear ( )
314+ . type ( 'opensearch_dashboards_sample_data_ecommerce{enter}' )
315+ . trigger ( 'search' ) ;
316+
317+ // Should not find any saved objects from the deleted workspace
318+ // (or significantly fewer if some were shared with other workspaces)
319+ cy . get ( 'body' ) . then ( ( $body ) => {
320+ if (
321+ $body . find ( '[data-test-subj="savedObjectsTableRowTitle"]' )
322+ . length > 0
323+ ) {
324+ // If there are still some objects, they should not belong to the deleted workspace
325+ cy . getElementByTestId ( 'savedObjectsTableRowTitle' ) . each ( ( $el ) => {
326+ // Check that none of these objects have the deleted workspace ID
327+ cy . wrap ( $el ) . should ( 'not.contain' , testWorkspaceId ) ;
328+ } ) ;
329+ } else {
330+ // No objects found, which is expected
331+ cy . get ( '[data-test-subj="savedObjectsTableRowTitle"]' ) . should (
332+ 'not.exist'
333+ ) ;
334+ }
335+ } ) ;
336+ } ) ;
337+ } ) ;
338+
339+ it ( 'should delete workspace with multiple types of saved objects' , ( ) => {
340+ // Create an index pattern
341+ cy . request ( {
342+ method : 'POST' ,
343+ url : `${ BASE_PATH } /w/${ testWorkspaceId } /api/saved_objects/index-pattern` ,
344+ headers : {
345+ 'content-type' : 'application/json;charset=UTF-8' ,
346+ 'osd-xsrf' : true ,
347+ } ,
348+ body : JSON . stringify ( {
349+ attributes : {
350+ title : 'test-index-pattern-*' ,
351+ timeFieldName : '@timestamp' ,
352+ } ,
353+ references : [ ] ,
354+ } ) ,
355+ } ) . then ( ( indexPatternResp ) => {
356+ const indexPatternId = indexPatternResp . body . id ;
357+
358+ // Create a visualization
359+ cy . request ( {
360+ method : 'POST' ,
361+ url : `${ BASE_PATH } /w/${ testWorkspaceId } /api/saved_objects/visualization` ,
362+ headers : {
363+ 'content-type' : 'application/json;charset=UTF-8' ,
364+ 'osd-xsrf' : true ,
365+ } ,
366+ body : JSON . stringify ( {
367+ attributes : {
368+ title : 'Test Visualization' ,
369+ visState : JSON . stringify ( {
370+ title : 'Test Visualization' ,
371+ type : 'histogram' ,
372+ params : { } ,
373+ aggs : [ ] ,
374+ } ) ,
375+ uiStateJSON : '{}' ,
376+ description : '' ,
377+ version : 1 ,
378+ kibanaSavedObjectMeta : {
379+ searchSourceJSON : JSON . stringify ( {
380+ index : indexPatternId ,
381+ query : { match_all : { } } ,
382+ } ) ,
383+ } ,
384+ } ,
385+ references : [
386+ {
387+ name : 'kibanaSavedObjectMeta.searchSourceJSON.index' ,
388+ type : 'index-pattern' ,
389+ id : indexPatternId ,
390+ } ,
391+ ] ,
392+ } ) ,
393+ } ) . then ( ( visualizationResp ) => {
394+ const visualizationId = visualizationResp . body . id ;
395+
396+ // Create a dashboard
397+ cy . request ( {
398+ method : 'POST' ,
399+ url : `${ BASE_PATH } /w/${ testWorkspaceId } /api/saved_objects/dashboard` ,
400+ headers : {
401+ 'content-type' : 'application/json;charset=UTF-8' ,
402+ 'osd-xsrf' : true ,
403+ } ,
404+ body : JSON . stringify ( {
405+ attributes : {
406+ title : 'Test Dashboard' ,
407+ hits : 0 ,
408+ description : 'Test dashboard for workspace deletion' ,
409+ panelsJSON : JSON . stringify ( [
410+ {
411+ version : '7.9.0' ,
412+ gridData : { x : 0 , y : 0 , w : 24 , h : 15 , i : '1' } ,
413+ panelIndex : '1' ,
414+ embeddableConfig : { } ,
415+ panelRefName : 'panel_1' ,
416+ } ,
417+ ] ) ,
418+ optionsJSON : JSON . stringify ( {
419+ useMargins : true ,
420+ syncColors : false ,
421+ hidePanelTitles : false ,
422+ } ) ,
423+ version : 1 ,
424+ timeRestore : false ,
425+ kibanaSavedObjectMeta : {
426+ searchSourceJSON : JSON . stringify ( {
427+ query : { query : '' , language : 'kuery' } ,
428+ filter : [ ] ,
429+ } ) ,
430+ } ,
431+ } ,
432+ references : [
433+ {
434+ name : 'panel_1' ,
435+ type : 'visualization' ,
436+ id : visualizationId ,
437+ } ,
438+ ] ,
439+ } ) ,
440+ } ) . then ( ( dashboardResp ) => {
441+ const dashboardId = dashboardResp . body . id ;
442+
443+ // Verify all saved objects exist in the workspace
444+ cy . visit ( `${ BASE_PATH } /w/${ testWorkspaceId } /app/objects` ) ;
445+ cy . getElementByTestId ( 'savedObjectsTableRowTitle' ) . should (
446+ 'have.length.greaterThan' ,
447+ 2
448+ ) ;
449+
450+ // Delete the workspace
451+ cy . visit ( `${ BASE_PATH } /app/workspace_list` ) ;
452+ cy . contains ( testWorkspaceName ) . should ( 'be.visible' ) ;
453+
454+ cy . getElementByTestId ( `checkboxSelectRow-${ testWorkspaceId } ` )
455+ . parents ( 'tr' )
456+ . within ( ( ) => {
457+ cy . getElementByTestId (
458+ 'euiCollapsedItemActionsButton'
459+ ) . click ( ) ;
460+ } ) ;
461+
462+ cy . getElementByTestId ( 'workspace-list-delete-icon' ) . click ( ) ;
463+ cy . contains ( 'Delete workspace' ) . should ( 'be.visible' ) ;
464+ cy . getElementByTestId ( 'delete-workspace-modal-input' ) . type (
465+ 'delete'
466+ ) ;
467+ cy . getElementByTestId ( 'delete-workspace-modal-confirm' ) . click ( ) ;
468+
469+ cy . contains ( 'workspaces deleted successfully' ) . should (
470+ 'be.visible'
471+ ) ;
472+
473+ // Verify all saved objects are deleted
474+ const savedObjectsToCheck = [
475+ { type : 'index-pattern' , id : indexPatternId } ,
476+ { type : 'visualization' , id : visualizationId } ,
477+ { type : 'dashboard' , id : dashboardId } ,
478+ ] ;
479+
480+ savedObjectsToCheck . forEach ( ( { type, id } ) => {
481+ cy . request ( {
482+ method : 'GET' ,
483+ url : `${ BASE_PATH } /api/saved_objects/${ type } /${ id } ` ,
484+ failOnStatusCode : false ,
485+ headers : {
486+ 'Osd-Xsrf' : 'osd-fetch' ,
487+ } ,
488+ } ) . then ( ( response ) => {
489+ expect ( response . status ) . to . be . oneOf ( [ 404 , 403 ] ) ;
490+ } ) ;
491+ } ) ;
492+ } ) ;
493+ } ) ;
494+ } ) ;
495+ } ) ;
496+ } ) ;
184497 }
185498} ;
0 commit comments