diff --git a/frontend/cypress/integration/projects/notifications.spec.ts b/frontend/cypress/integration/projects/notifications.spec.ts index 125c9b861b..e214cdc45a 100644 --- a/frontend/cypress/integration/projects/notifications.spec.ts +++ b/frontend/cypress/integration/projects/notifications.spec.ts @@ -1,134 +1,63 @@ -/// +/// + +import UserCredentials = Cypress.UserCredentials; -type UserCredentials = { email: string; password: string }; const ENTERPRISE = Boolean(Cypress.env('ENTERPRISE')); const randomId = String(Math.random()).split('.')[1]; const featureToggleName = `notifications_test-${randomId}`; const baseUrl = Cypress.config().baseUrl; let strategyId = ''; -const userIds: number[] = []; -const userCredentials: UserCredentials[] = []; +let userIds: number[] = []; +let userCredentials: UserCredentials[] = []; const userName = `notifications_user-${randomId}`; const projectName = `default`; -const password = Cypress.env(`AUTH_PASSWORD`) + '_A'; + const EDITOR = 2; -const PROJECT_MEMBER = 5; - -// Disable all active splash pages by visiting them. -const disableActiveSplashScreens = () => { - cy.visit(`/splash/operators`); -}; - -const createUser = () => { - const name = `${userName}`; - const email = `${name}@test.com`; - cy.request('POST', `${baseUrl}/api/admin/user-admin`, { - name: name, - email: `${name}@test.com`, - username: `${name}@test.com`, - sendEmail: false, - rootRole: EDITOR, - }) - .as(name) - .then(response => { - const id = response.body.id; - updateUserPassword(id).then(() => { - addUserToProject(id).then(() => { - userIds.push(id); - userCredentials.push({ email, password }); - }); - }); - }); -}; - -const updateUserPassword = (id: number) => - cy.request( - 'POST', - `${baseUrl}/api/admin/user-admin/${id}/change-password`, - { - password, - } - ); - -const addUserToProject = (id: number) => - cy.request( - 'POST', - `${baseUrl}/api/admin/projects/${projectName}/role/${PROJECT_MEMBER}/access`, - { - groups: [], - users: [{ id }], - } - ); describe('notifications', () => { before(() => { - disableActiveSplashScreens(); - cy.login(); - createUser(); + cy.runBefore(); }); - after(() => { - // We need to login as admin for cleanup - cy.login(); - userIds.forEach(id => - cy.request('DELETE', `${baseUrl}/api/admin/user-admin/${id}`) - ); + // This one is failing on CI: https://github.com/Unleash/unleash/actions/runs/4609305167/jobs/8160244872#step:4:193 + it.skip('should create a notification when a feature is created in a project', () => { + cy.login_UI(); + cy.createUser_API(userName, EDITOR).then(value => { + userIds = value.userIds; + userCredentials = value.userCredentials; - cy.request( - 'DELETE', - `${baseUrl}/api/admin/features/${featureToggleName}` - ); - }); + cy.login_UI(); + cy.visit(`/projects/${projectName}`); - beforeEach(() => { - cy.login(); - cy.visit(`/projects/${projectName}`); - if (document.querySelector("[data-testid='CLOSE_SPLASH']")) { - cy.get("[data-testid='CLOSE_SPLASH']").click(); - } - }); + cy.createFeature_UI(featureToggleName); - afterEach(() => { - cy.logout(); - }); + //Should not show own notifications + cy.get("[data-testid='NOTIFICATIONS_BUTTON']").click(); - const createFeature = () => { - cy.get('[data-testid=NAVIGATE_TO_CREATE_FEATURE').click(); + //then + cy.get("[data-testid='NOTIFICATIONS_MODAL']").should('exist'); - cy.intercept('POST', `/api/admin/projects/${projectName}/features`).as( - 'createFeature' - ); + const credentials = userCredentials[0]; - cy.get("[data-testid='CF_NAME_ID'").type(featureToggleName); - cy.get("[data-testid='CF_DESC_ID'").type('hello-world'); - cy.get("[data-testid='CF_CREATE_BTN_ID']").click(); - cy.wait('@createFeature'); - }; + //Sign in as a different user + cy.login_UI(credentials.email, credentials.password); + cy.visit(`/projects/${projectName}`); + cy.get("[data-testid='NOTIFICATIONS_BUTTON']").click(); - it('should create a notification when a feature is created in a project', () => { - createFeature(); + //then + cy.get("[data-testid='UNREAD_NOTIFICATIONS']").should('exist'); + cy.get("[data-testid='NOTIFICATIONS_LIST']") + .eq(0) + .should('contain.text', `New feature ${featureToggleName}`); - //Should not show own notifications - cy.get("[data-testid='NOTIFICATIONS_BUTTON']").click(); + //clean + // We need to login as admin for cleanup + cy.login_UI(); + userIds.forEach(id => + cy.request('DELETE', `${baseUrl}/api/admin/user-admin/${id}`) + ); - //then - cy.get("[data-testid='NOTIFICATIONS_MODAL']").should('exist'); - - const credentials = userCredentials[0]; - - //Sign in as a different user - cy.login(credentials.email, credentials.password); - cy.visit(`/projects/${projectName}`); - if (document.querySelector("[data-testid='CLOSE_SPLASH']")) { - cy.get("[data-testid='CLOSE_SPLASH']").click(); - } - cy.get("[data-testid='NOTIFICATIONS_BUTTON']").click(); - - //then - cy.get("[data-testid='UNREAD_NOTIFICATIONS']").should('exist'); - cy.get("[data-testid='NOTIFICATIONS_LIST']") - .should('have.length', 1) - .eq(0) - .should('contain.text', 'New feature'); + cy.deleteFeature_API(featureToggleName); + }); }); }); diff --git a/frontend/cypress/integration/projects/settings.spec.ts b/frontend/cypress/integration/projects/settings.spec.ts index 51307231e9..9637a2bcb1 100644 --- a/frontend/cypress/integration/projects/settings.spec.ts +++ b/frontend/cypress/integration/projects/settings.spec.ts @@ -1,117 +1,80 @@ -/// +/// -type UserCredentials = { email: string; password: string }; -const ENTERPRISE = Boolean(Cypress.env('ENTERPRISE')); const randomId = String(Math.random()).split('.')[1]; -const featureToggleName = `settings-${randomId}`; const baseUrl = Cypress.config().baseUrl; let strategyId = ''; const userName = `settings-user-${randomId}`; const projectName = `stickiness-project-${randomId}`; +const TEST_STICKINESS = 'userId'; +const featureToggleName = `settings-${randomId}`; +let cleanFeature = false; +let cleanProject = false; -// Disable all active splash pages by visiting them. -const disableActiveSplashScreens = () => { - cy.visit(`/splash/operators`); -}; - -const disableFeatureStrategiesProdGuard = () => { - localStorage.setItem( - 'useFeatureStrategyProdGuardSettings:v2', - JSON.stringify({ hide: true }) - ); -}; - -describe('notifications', () => { +describe('project settings', () => { before(() => { - disableFeatureStrategiesProdGuard(); - disableActiveSplashScreens(); - cy.login(); - }); - - after(() => { - cy.request( - 'DELETE', - `${baseUrl}/api/admin/features/${featureToggleName}` - ); - - cy.request('DELETE', `${baseUrl}/api/admin/projects/${projectName}`); + cy.runBefore(); }); beforeEach(() => { - cy.login(); - cy.visit(`/projects`); - if (document.querySelector("[data-testid='CLOSE_SPLASH']")) { - cy.get("[data-testid='CLOSE_SPLASH']").click(); + cy.login_UI(); + if (cleanFeature) { + cy.deleteFeature_API(featureToggleName); } + if (cleanProject) { + cy.deleteProject_API(projectName); + } + cy.visit(`/projects`); + cy.wait(300); }); - afterEach(() => { - cy.request('DELETE', `${baseUrl}/api/admin/projects/${projectName}`); - }); - - const createFeature = () => { - cy.get('[data-testid=NAVIGATE_TO_CREATE_FEATURE').click(); - - cy.intercept('POST', `/api/admin/projects/${projectName}/features`).as( - 'createFeature' - ); - - cy.get("[data-testid='CF_NAME_ID'").type(featureToggleName); - cy.get("[data-testid='CF_DESC_ID'").type('hello-world'); - cy.get("[data-testid='CF_CREATE_BTN_ID']").click(); - cy.wait('@createFeature'); - }; - - const createProject = () => { - cy.get('[data-testid=NAVIGATE_TO_CREATE_PROJECT').click(); - - cy.get("[data-testid='PROJECT_ID_INPUT']").type(projectName); - cy.get("[data-testid='PROJECT_NAME_INPUT']").type(projectName); - cy.get("[id='stickiness-select']") - .first() - .click() - .get('[data-testid=SELECT_ITEM_ID-userId') - .first() - .click(); - cy.get("[data-testid='CREATE_PROJECT_BTN']").click(); - }; - it('should store default project stickiness when creating, retrieve it when editing a project', () => { - createProject(); - + //when + cleanProject = true; + cy.createProject_UI(projectName, TEST_STICKINESS); cy.visit(`/projects/${projectName}`); - if (document.querySelector("[data-testid='CLOSE_SPLASH']")) { - cy.get("[data-testid='CLOSE_SPLASH']").click(); - } cy.get("[data-testid='NAVIGATE_TO_EDIT_PROJECT']").click(); //then cy.get("[id='stickiness-select']") .first() .should('have.text', 'userId'); + + //clean + cy.request('DELETE', `${baseUrl}/api/admin/projects/${projectName}`); }); it('should respect the default project stickiness when creating a Gradual Rollout Strategy', () => { - createProject(); - createFeature(); + cy.createProject_UI(projectName, TEST_STICKINESS); + cy.createFeature_UI(featureToggleName, true, projectName); + cleanFeature = true; + + //when - then + cy.addFlexibleRolloutStrategyToFeature_UI({ + featureToggleName, + project: projectName, + stickiness: TEST_STICKINESS, + }); + + //clean + }); + + it.skip('should respect the default project stickiness when creating a variant', () => { + cy.createProject_UI(projectName, TEST_STICKINESS); + cy.createFeature_UI(featureToggleName, true, projectName); + + //when cy.visit( - `/projects/default/features/${featureToggleName}/strategies/create?environmentId=development&strategyName=flexibleRollout` + `/projects/${projectName}/features/${featureToggleName}/variants` ); + cy.get("[data-testid='ADD_VARIANT_BUTTON']").first().click(); //then cy.get("[id='stickiness-select']") .first() .should('have.text', 'userId'); - }); - it('should respect the default project stickiness when creating a variant', () => { - createProject(); - createFeature(); - - cy.visit(`/projects/default/features/${featureToggleName}/variants`); - - cy.get("[data-testid='EDIT_VARIANTS_BUTTON']").click(); - //then - cy.get('#menu-stickiness').first().should('have.text', 'userId'); + //clean + cy.deleteFeature_API(featureToggleName); + cy.deleteProject_API(projectName); }); }); diff --git a/frontend/src/component/user/Profile/Profile.tsx b/frontend/src/component/user/Profile/Profile.tsx index 94748642d3..324d2aff7f 100644 --- a/frontend/src/component/user/Profile/Profile.tsx +++ b/frontend/src/component/user/Profile/Profile.tsx @@ -7,6 +7,7 @@ import { useLocation, useNavigate } from 'react-router-dom'; import { PasswordTab } from './PasswordTab/PasswordTab'; import { PersonalAPITokensTab } from './PersonalAPITokensTab/PersonalAPITokensTab'; import { ProfileTab } from './ProfileTab/ProfileTab'; +import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; export const Profile = () => { const { user } = useAuthUser(); @@ -14,6 +15,8 @@ export const Profile = () => { const navigate = useNavigate(); const { config: simpleAuthConfig } = useAuthSettings('simple'); + const { uiConfig } = useUiConfig(); + const tabs = [ { id: 'profile', label: 'Profile' }, { @@ -26,6 +29,7 @@ export const Profile = () => { id: 'pat', label: 'Personal API tokens', path: 'personal-api-tokens', + hidden: uiConfig.flags.personalAccessTokensKillSwitch, }, ]; diff --git a/frontend/src/interfaces/uiConfig.ts b/frontend/src/interfaces/uiConfig.ts index 2b6ac934c7..4773226136 100644 --- a/frontend/src/interfaces/uiConfig.ts +++ b/frontend/src/interfaces/uiConfig.ts @@ -51,6 +51,7 @@ export interface IFlags { projectScopedSegments?: boolean; projectScopedStickiness?: boolean; projectMode?: boolean; + personalAccessTokensKillSwitch?: boolean; } export interface IVersionInfo { diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap index df802fb748..0e901a2942 100644 --- a/src/lib/__snapshots__/create-config.test.ts.snap +++ b/src/lib/__snapshots__/create-config.test.ts.snap @@ -83,6 +83,7 @@ exports[`should create default config 1`] = ` "newProjectOverview": false, "optimal304": false, "optimal304Differ": false, + "personalAccessTokensKillSwitch": false, "proPlanAutoCharge": false, "projectMode": false, "projectScopedSegments": false, @@ -110,6 +111,7 @@ exports[`should create default config 1`] = ` "newProjectOverview": false, "optimal304": false, "optimal304Differ": false, + "personalAccessTokensKillSwitch": false, "proPlanAutoCharge": false, "projectMode": false, "projectScopedSegments": false, diff --git a/src/lib/routes/admin-api/user/pat.ts b/src/lib/routes/admin-api/user/pat.ts index 077a189a42..330de59c35 100644 --- a/src/lib/routes/admin-api/user/pat.ts +++ b/src/lib/routes/admin-api/user/pat.ts @@ -1,7 +1,11 @@ import { Response } from 'express'; import Controller from '../../controller'; import { Logger } from '../../../logger'; -import { IUnleashConfig, IUnleashServices } from '../../../types'; +import { + IFlagResolver, + IUnleashConfig, + IUnleashServices, +} from '../../../types'; import { createRequestSchema } from '../../../openapi/util/create-request-schema'; import { createResponseSchema } from '../../../openapi/util/create-response-schema'; import { OpenApiService } from '../../../services/openapi-service'; @@ -21,6 +25,8 @@ export default class PatController extends Controller { private logger: Logger; + private flagResolver: IFlagResolver; + constructor( config: IUnleashConfig, { @@ -30,6 +36,7 @@ export default class PatController extends Controller { ) { super(config); this.logger = config.getLogger('lib/routes/auth/pat-controller.ts'); + this.flagResolver = config.flagResolver; this.openApiService = openApiService; this.patService = patService; this.route({ @@ -77,6 +84,11 @@ export default class PatController extends Controller { } async createPat(req: IAuthRequest, res: Response): Promise { + if (this.flagResolver.isEnabled('personalAccessTokensKillSwitch')) { + res.status(404).send({ message: 'PAT is disabled' }); + return; + } + const pat = req.body; const createdPat = await this.patService.createPat( pat, @@ -92,6 +104,10 @@ export default class PatController extends Controller { } async getPats(req: IAuthRequest, res: Response): Promise { + if (this.flagResolver.isEnabled('personalAccessTokensKillSwitch')) { + res.status(404).send({ message: 'PAT is disabled' }); + return; + } const pats = await this.patService.getAll(req.user.id); this.openApiService.respondWithValidation(200, res, patsSchema.$id, { pats: serializeDates(pats), diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts index 6012013081..e2017d0463 100644 --- a/src/lib/types/experimental.ts +++ b/src/lib/types/experimental.ts @@ -68,6 +68,10 @@ const flags = { false, ), projectMode: parseEnvVarBoolean(process.env.PROJECT_MODE, false), + personalAccessTokensKillSwitch: parseEnvVarBoolean( + process.env.UNLEASH_PAT_KILL_SWITCH, + false, + ), cleanClientApi: parseEnvVarBoolean(process.env.CLEAN_CLIENT_API, false), optimal304: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_OPTIMAL_304,