Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions cypress/Shared/AdminUserProfilePage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@ export class AdminUserProfilePage {
public static readonly compareVersionsButton = MeasuresPage.compareVersionsBtn
public static readonly transferButton = '[data-testid="transfer-action-btn"]'
public static readonly shareButton = '[data-testid="share-action-btn"]'
public static readonly deleteButton = '[data-testid="delete-action-btn"]'

public static readonly exportTooltip = '[data-testid="export-action-tooltip"]'
public static readonly humanReadableTooltip = '[data-testid="view-hr-action-tooltip"]'
public static readonly historyTooltip = '[data-testid="history-action-tooltip"]'
public static readonly compareVersionsTooltip = '[data-testid="compare-versions-action-tooltip"]'
public static readonly transferTooltip = '[data-testid="transfer-action-tooltip"]'
public static readonly shareTooltip = '[data-testid="share-action-tooltip"]'
public static readonly deleteTooltip = '[data-testid="delete-action-tooltip"]'

public static openAdminWorkspace(): void {
cy.visit('/admin')
Expand Down Expand Up @@ -142,6 +144,13 @@ export class AdminUserProfilePage {
.closest('tr')
}

public static selectLibraryByName(libraryName: string): Cypress.Chainable<JQuery<HTMLElement>> {
return this.findLibraryRow(libraryName)
.find('input[type="checkbox"]')
.should('be.visible')
.check()
}

public static expandLibrarySet(libraryName: string): Cypress.Chainable<JQuery<HTMLElement>> {
this.findLibraryRow(libraryName)
.find('[data-testid^="expand-library-toggle-"]')
Expand Down
16 changes: 16 additions & 0 deletions cypress/Shared/TestData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,22 @@ export class TestData {
})
}

public static requestAdminCqlLibraryDeleteById<T = CqlLibraryBody>(
libraryId: string,
harpId: string,
options: Partial<Cypress.RequestOptions> = {}
): Cypress.Chainable<Cypress.Response<T>> {
return this.requestWithAccessToken<T>({
...options,
url: `/api/cql-libraries/admin/${libraryId}`,
method: 'DELETE',
headers: {
...options.headers,
harpId
}
})
}

public static updateCqlLibrary<T = CqlLibraryBody>(
body: CqlLibraryBody,
options: Partial<Cypress.RequestOptions> = {}
Expand Down
79 changes: 79 additions & 0 deletions cypress/e2e/Services/CQL Library Service/CQLLibraryDelete.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,29 @@ const draftCurrentCqlLibrary = (): Cypress.Chainable<Cypress.Response<CqlLibrary
}))
}

const createSecondVersion = (): Cypress.Chainable<{
historicalLibraryId: string
latestLibraryId: string
}> => {
return TestData.versionCqlLibrary<CqlLibraryBody>(versionNumber).then((historicalResponse) => {
const historicalLibraryId = historicalResponse.body.id
expect(historicalLibraryId, 'historical library id').to.be.a('string').and.not.be.empty

return draftCurrentCqlLibrary().then((draftResponse) => {
expect(draftResponse.status).to.eq(201)
expect(draftResponse.body.draft).to.eq(true)
TestData.writeCqlLibraryId(draftResponse.body.id, 1)

return TestData.versionCqlLibrary<CqlLibraryBody>('2.0.000', 1).then((latestResponse) => {
const latestLibraryId = latestResponse.body.id
expect(latestLibraryId, 'latest library id').to.be.a('string').and.not.be.empty

return cy.wrap({ historicalLibraryId, latestLibraryId })
})
})
})
}

const deleteAllCqlLibraryVersions = (
harpId: string,
options: Partial<Cypress.RequestOptions> = {}
Expand Down Expand Up @@ -132,6 +155,62 @@ describe('Delete CQL Library', () => {
})
})

// MAT-9892: Proven in DEV on 2026-08-06. Keep as regression coverage without rerunning by default.
it.skip('Admin deletes only the selected latest CQL Library version by document ID', () => {
const harpUser = OktaLogin.setupUserSession(false)

createSecondVersion().then(({ historicalLibraryId, latestLibraryId }) => {
OktaLogin.setupAdminSession()
TestData.requestAdminCqlLibraryDeleteById<CqlLibraryBody>(
latestLibraryId,
harpUser
).then((response) => {
expect(response.status).to.eq(200)
expect(response.body.id).to.eq(latestLibraryId)
expect(response.body.version).to.eq('2.0.000')
})

OktaLogin.setupUserSession(false)
TestData.requestCqlLibraryById<CqlLibraryBody>('GET', latestLibraryId, {
failOnStatusCode: false
}).then((response) => {
expect(response.status).to.eq(404)
})
TestData.requestCqlLibraryById<CqlLibraryBody>('GET', historicalLibraryId).then(
(response) => {
expect(response.status).to.eq(200)
expect(response.body.id).to.eq(historicalLibraryId)
expect(response.body.version).to.eq(versionNumber)
}
)
})
})

// MAT-9892: Proven in DEV on 2026-08-06. Keep as regression coverage without rerunning by default.
it.skip('Admin cannot delete a CQL Library version when harpId does not match the owner', () => {
const harpUserALT = OktaLogin.getUser(true)

OktaLogin.setupUserSession(false)
TestData.versionCqlLibrary<CqlLibraryBody>(versionNumber).then((versionResponse) => {
const libraryId = versionResponse.body.id

OktaLogin.setupAdminSession()
TestData.requestAdminCqlLibraryDeleteById<CqlLibraryBody>(libraryId, harpUserALT, {
failOnStatusCode: false
}).then((response) => {
expect(response.status).to.eq(409)
expect(response.body).to.have.property('message').and.contain(harpUserALT)
expect(response.body).to.have.property('message').and.contain(libraryId)
})

OktaLogin.setupUserSession(false)
TestData.requestCqlLibraryById<CqlLibraryBody>('GET', libraryId).then((response) => {
expect(response.status).to.eq(200)
expect(response.body.id).to.eq(libraryId)
})
})
})

it('Delete all Versions of the CQL Library - user is the owner of the Library', () => {
const harpUser = OktaLogin.setupUserSession(false)
CQLLibraryPage.versionLibraryAPI(versionNumber)
Expand Down
142 changes: 142 additions & 0 deletions cypress/e2e/WebInterface/Admin/AdminUserProfileLibraryDelete.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { AdminUserProfilePage } from '../../../Shared/AdminUserProfilePage'
import { CQLLibraryPage } from '../../../Shared/CQLLibraryPage'
import { CQLEditorPage } from '../../../Shared/CQLEditorPage'
import { Environment } from '../../../Shared/Environment'
import { LibraryCQL } from '../../../Shared/LibraryCQL'
import { OktaLogin } from '../../../Shared/OktaLogin'
import { SupportedModels } from '../../../Shared/CreateMeasurePage'
import { TestData } from '../../../Shared/TestData'

const describeAdminUserProfile = Cypress.env('environment') === 'test' ? describe.skip : describe

const assertDeleteDialog = (message: string): void => {
cy.get(CQLLibraryPage.cqlLibraryDeleteDialog)
.should('be.visible')
.within(() => {
cy.contains('h2', 'Delete Library').should('be.visible')
cy.contains(message).should('be.visible')
cy.get(CQLEditorPage.modalActionWarning)
.should('be.visible')
.and('contain.text', 'This action cannot be undone.')
cy.get('hr').should('have.length.at.least', 2)
cy.contains('button', 'Cancel').should('be.enabled')
cy.get(CQLEditorPage.deleteContinueButton)
.should('be.enabled')
.and('contain.text', 'Yes, Delete')
.should(($button) => {
const [red, green, blue] =
getComputedStyle($button[0]).backgroundColor.match(/\d+/g)?.map(Number) ?? []
expect(red, 'destructive button red channel').to.be.greaterThan(green)
expect(red, 'destructive button red channel').to.be.greaterThan(blue)
})
})
}

// MAT-9816: Run in DEV until the AdminUserProfile feature and delete flows are proven.
describeAdminUserProfile('Admin user profile library deletion', () => {
let libraryName = ''
let libraryOwner = ''
let profileUser = ''

beforeEach(() => {
const credentials = Environment.credentials()
libraryName = `AdminProfileLibraryDelete${Date.now()}`
libraryOwner = OktaLogin.getUser(false)
profileUser = credentials.altHarpUser?.toLowerCase() ?? ''
expect(profileUser, 'shared profile user').not.to.be.empty
expect(profileUser, 'shared profile user differs from owner').not.to.eq(libraryOwner)
})

afterEach(() => {
OktaLogin.setupAdminSession()
TestData.readCqlLibraryId().then((libraryId) => {
TestData.requestAdminCqlLibraryDeleteById(libraryId, libraryOwner, {
failOnStatusCode: false
})
})
})

const openOwnedDraftDeleteDialog = (): void => {
CQLLibraryPage.createLibraryAPI(libraryName, SupportedModels.qiCore4, {
cql: LibraryCQL.validCQL4QICORELib
})

OktaLogin.AdminLogin()
AdminUserProfilePage.openUserProfile(libraryOwner)
AdminUserProfilePage.openLibrariesTab(AdminUserProfilePage.ownedLibrariesTab)
AdminUserProfilePage.submitLibrarySearch(libraryName)
AdminUserProfilePage.selectLibraryByName(libraryName)
AdminUserProfilePage.assertEnabledAction(
AdminUserProfilePage.deleteButton,
AdminUserProfilePage.deleteTooltip,
'Delete library'
)

cy.get(AdminUserProfilePage.deleteButton).click()
assertDeleteDialog(`Are you sure you want to delete draft of ${libraryName}`)
}

it('deletes an Owned draft library after showing the draft confirmation', () => {
openOwnedDraftDeleteDialog()
cy.intercept('DELETE', '**/api/cql-libraries/*').as('deleteDraft')
cy.get(CQLEditorPage.deleteContinueButton).click()
cy.wait('@deleteDraft').its('response.statusCode').should('eq', 200)
cy.get(AdminUserProfilePage.librariesTable).should('not.contain.text', libraryName)
})

// MAT-9816: Proven in DEV on 2026-08-06. Keep as regression coverage without rerunning by default.
it.skip('cancels an Owned draft deletion without sending a delete request', () => {
openOwnedDraftDeleteDialog()
cy.intercept('DELETE', '**/api/cql-libraries/**').as('deleteLibrary')

cy.get(CQLLibraryPage.cqlLibraryDeleteDialog).contains('button', 'Cancel').click()
cy.get(CQLLibraryPage.cqlLibraryDeleteDialog).should('not.exist')
cy.get('@deleteLibrary.all').should('have.length', 0)
AdminUserProfilePage.findLibraryRow(libraryName).should('be.visible')
})

// MAT-9816: Proven in DEV on 2026-08-06. Keep as regression coverage without rerunning by default.
it.skip('closes an Owned draft deletion with the dialog X without sending a delete request', () => {
openOwnedDraftDeleteDialog()
cy.intercept('DELETE', '**/api/cql-libraries/**').as('deleteLibrary')

cy.get(CQLLibraryPage.cqlLibraryDeleteDialog).find(CQLEditorPage.modalXButton).click()
cy.get(CQLLibraryPage.cqlLibraryDeleteDialog).should('not.exist')
cy.get('@deleteLibrary.all').should('have.length', 0)
AdminUserProfilePage.findLibraryRow(libraryName).should('be.visible')
})

it('deletes a Shared version library through the admin single-instance endpoint', () => {
CQLLibraryPage.createLibraryAPI(libraryName, SupportedModels.QDM, {
cql: LibraryCQL.validCQL4QDMLib
})
TestData.versionCqlLibrary('1.0.000').then((versionResponse) => {
const versionId = versionResponse.body.id
expect(versionId, 'version id').to.be.a('string').and.not.be.empty
TestData.requestSharePermissions('library', 'GRANT', versionId, profileUser).then((response) => {
expect(response.status).to.eq(200)
})
})

OktaLogin.AdminLogin()
AdminUserProfilePage.openUserProfile(profileUser)
AdminUserProfilePage.openLibrariesTab(AdminUserProfilePage.sharedLibrariesTab)
AdminUserProfilePage.submitLibrarySearch(libraryName)
AdminUserProfilePage.selectLibraryByName(libraryName)
AdminUserProfilePage.assertEnabledAction(
AdminUserProfilePage.deleteButton,
AdminUserProfilePage.deleteTooltip,
'Delete library'
)

cy.get(AdminUserProfilePage.deleteButton).click()
assertDeleteDialog(`Are you sure you want to delete version 1.0.000 of ${libraryName}`)
cy.intercept('DELETE', '**/api/cql-libraries/admin/*').as('deleteVersion')
cy.get(CQLEditorPage.deleteContinueButton).click()
cy.wait('@deleteVersion').then(({ request, response }) => {
expect(request.headers.harpid).to.eq(libraryOwner)
expect(response?.statusCode).to.eq(200)
})
cy.get(AdminUserProfilePage.librariesTable).should('not.contain.text', libraryName)
})
})
Loading
Loading