diff --git a/redisinsight/api/src/constants/agreements-spec.json b/redisinsight/api/src/constants/agreements-spec.json index e99bc0549b..001c218789 100644 --- a/redisinsight/api/src/constants/agreements-spec.json +++ b/redisinsight/api/src/constants/agreements-spec.json @@ -7,6 +7,7 @@ "required": false, "editable": true, "disabled": false, + "linkToPrivacyPolicy": true, "category": "privacy", "since": "1.0.1", "title": "Usage Data", @@ -19,6 +20,7 @@ "required": false, "editable": true, "disabled": false, + "linkToPrivacyPolicy": false, "category": "notifications", "since": "1.0.6", "title": "Notification", @@ -37,6 +39,7 @@ "required": false, "editable": true, "disabled": false, + "linkToPrivacyPolicy": false, "category": "privacy", "since": "1.0.3", "title": "Encryption", @@ -49,6 +52,7 @@ "required": false, "editable": true, "disabled": true, + "linkToPrivacyPolicy": false, "category": "privacy", "since": "1.0.3", "title": "Encryption", @@ -61,6 +65,7 @@ "required": false, "editable": true, "disabled": true, + "linkToPrivacyPolicy": false, "category": "privacy", "since": "1.0.5", "title": "Encryption", @@ -75,6 +80,7 @@ "required": true, "editable": false, "disabled": false, + "linkToPrivacyPolicy": false, "since": "1.0.4", "title": "Server Side Public License", "label": "I have read and understood the Terms", diff --git a/redisinsight/api/src/modules/database-discovery/local.database-discovery.service.ts b/redisinsight/api/src/modules/database-discovery/local.database-discovery.service.ts index aa10e73787..3f5aec0cac 100644 --- a/redisinsight/api/src/modules/database-discovery/local.database-discovery.service.ts +++ b/redisinsight/api/src/modules/database-discovery/local.database-discovery.service.ts @@ -26,7 +26,7 @@ export class LocalDatabaseDiscoveryService extends DatabaseDiscoveryService { firstRun?: boolean, ): Promise { try { - // no need to auto discover for Redis Stack + // No need to auto discover for Redis Stack - quick check if (SERVER_CONFIG.buildType === 'REDIS_STACK') { return; } diff --git a/redisinsight/api/src/modules/encryption/encryption.service.spec.ts b/redisinsight/api/src/modules/encryption/encryption.service.spec.ts index bed86e9db3..f163134213 100644 --- a/redisinsight/api/src/modules/encryption/encryption.service.spec.ts +++ b/redisinsight/api/src/modules/encryption/encryption.service.spec.ts @@ -103,6 +103,44 @@ describe('EncryptionService', () => { }); }); + describe('isEncryptionAvailable', () => { + it('should return true when multiple strategies are available (KEYTAR and PLAIN)', async () => { + keytarEncryptionStrategy.isAvailable.mockResolvedValueOnce(true); + keyEncryptionStrategy.isAvailable.mockResolvedValueOnce(false); + + const result = await service.isEncryptionAvailable(); + + expect(result).toBe(true); + }); + + it('should return true when multiple strategies are available (KEY and PLAIN)', async () => { + keytarEncryptionStrategy.isAvailable.mockResolvedValueOnce(false); + keyEncryptionStrategy.isAvailable.mockResolvedValueOnce(true); + + const result = await service.isEncryptionAvailable(); + + expect(result).toBe(true); + }); + + it('should return true when all strategies are available (KEY, KEYTAR and PLAIN)', async () => { + keytarEncryptionStrategy.isAvailable.mockResolvedValueOnce(true); + keyEncryptionStrategy.isAvailable.mockResolvedValueOnce(true); + + const result = await service.isEncryptionAvailable(); + + expect(result).toBe(true); + }); + + it('should return false when only PLAIN strategy is available', async () => { + keytarEncryptionStrategy.isAvailable.mockResolvedValueOnce(false); + keyEncryptionStrategy.isAvailable.mockResolvedValueOnce(false); + + const result = await service.isEncryptionAvailable(); + + expect(result).toBe(false); + }); + }); + describe('getEncryptionStrategy', () => { it('Should return KEYTAR strategy based on app agreements', async () => { expect(await service.getEncryptionStrategy()).toEqual( diff --git a/redisinsight/api/src/modules/init/local.init.service.ts b/redisinsight/api/src/modules/init/local.init.service.ts index 199244d147..ef1d77b139 100644 --- a/redisinsight/api/src/modules/init/local.init.service.ts +++ b/redisinsight/api/src/modules/init/local.init.service.ts @@ -32,7 +32,7 @@ export class LocalInitService extends InitService { await this.initAnalytics(firstStart); await this.featureService.recalculateFeatureFlags(sessionMetadata); await this.redisClientFactory.init(); - await this.databaseDiscoveryService.discover(sessionMetadata); + await this.databaseDiscoveryService.discover(sessionMetadata, firstStart); } async initAnalytics(firstStart: boolean) { diff --git a/redisinsight/api/src/modules/settings/repositories/local.agreements.repository.spec.ts b/redisinsight/api/src/modules/settings/repositories/local.agreements.repository.spec.ts index 11d48623ed..3feab3361f 100644 --- a/redisinsight/api/src/modules/settings/repositories/local.agreements.repository.spec.ts +++ b/redisinsight/api/src/modules/settings/repositories/local.agreements.repository.spec.ts @@ -57,6 +57,54 @@ describe('LocalAgreementsRepository', () => { data: undefined, }); }); + it('should create new agreements when entity exists but has no data', async () => { + // Mock an entity that exists but has no data property + const entityWithoutData = Object.assign(new AgreementsEntity(), { + id: mockUserId, + version: '1.0.0', + data: undefined, // This should trigger the !entity?.data check + }); + + repository.findOneBy.mockResolvedValueOnce(entityWithoutData); + + const result = await service.getOrCreate(mockSessionMetadata); + + // Verify that save was called to create a new entity + expect(repository.save).toHaveBeenCalledWith({ + id: 1, + data: undefined, + }); + + expect(result).toEqual({ + ...mockAgreements, + version: undefined, + data: undefined, + }); + }); + it('should create new agreements when entity exists but has empty string data', async () => { + // Mock an entity that exists but has empty string data + const entityWithEmptyData = Object.assign(new AgreementsEntity(), { + id: mockUserId, + version: '1.0.0', + data: '', // This should also trigger the !entity?.data check + }); + + repository.findOneBy.mockResolvedValueOnce(entityWithEmptyData); + + const result = await service.getOrCreate(mockSessionMetadata); + + // Verify that save was called to create a new entity + expect(repository.save).toHaveBeenCalledWith({ + id: 1, + data: undefined, + }); + + expect(result).toEqual({ + ...mockAgreements, + version: undefined, + data: undefined, + }); + }); it('should fail to create with unique constraint and return existing', async () => { repository.findOneBy.mockResolvedValueOnce(null); repository.findOneBy.mockResolvedValueOnce(mockAgreements); diff --git a/redisinsight/api/src/modules/settings/repositories/local.agreements.repository.ts b/redisinsight/api/src/modules/settings/repositories/local.agreements.repository.ts index a828db01c7..12dce2b4fe 100644 --- a/redisinsight/api/src/modules/settings/repositories/local.agreements.repository.ts +++ b/redisinsight/api/src/modules/settings/repositories/local.agreements.repository.ts @@ -17,10 +17,10 @@ export class LocalAgreementsRepository extends AgreementsRepository { async getOrCreate( sessionMetadata: SessionMetadata, - defaultOptions: DefaultAgreementsOptions = {} + defaultOptions: DefaultAgreementsOptions = {}, ): Promise { let entity = await this.repository.findOneBy({}); - if (!entity) { + if (!entity?.data) { try { entity = await this.repository.save( classToClass(AgreementsEntity, plainToInstance(Agreements, { diff --git a/redisinsight/api/src/modules/settings/settings.service.ts b/redisinsight/api/src/modules/settings/settings.service.ts index 94e89fd453..040127fb0c 100644 --- a/redisinsight/api/src/modules/settings/settings.service.ts +++ b/redisinsight/api/src/modules/settings/settings.service.ts @@ -64,7 +64,7 @@ export class SettingsService { let defaultOptions: object; if (SERVER_CONFIG.acceptTermsAndConditions) { - const isEncryptionAvailable = await this.encryptionService.isEncryptionAvailable(); + const isEncryptionAvailable = await this.encryptionService.isEncryptionAvailable(); defaultOptions = { data: { @@ -84,7 +84,6 @@ export class SettingsService { sessionMetadata, ); - return classToClass(GetAppSettingsResponse, { ...settings?.data, acceptTermsAndConditionsOverwritten: SERVER_CONFIG.acceptTermsAndConditions, diff --git a/redisinsight/api/test/api/settings/GET-settings-agreements-spec.test.ts b/redisinsight/api/test/api/settings/GET-settings-agreements-spec.test.ts index 7a23bbd783..96e64e9276 100644 --- a/redisinsight/api/test/api/settings/GET-settings-agreements-spec.test.ts +++ b/redisinsight/api/test/api/settings/GET-settings-agreements-spec.test.ts @@ -17,6 +17,7 @@ const agreementItemSchema = Joi.object().keys({ category: Joi.string().optional(), description: Joi.string().optional(), requiredText: Joi.string().optional(), + linkToPrivacyPolicy: Joi.boolean().required(), }); const responseSchema = Joi.object() diff --git a/redisinsight/ui/src/components/consents-settings/ConsentOption/ConsentOption.spec.tsx b/redisinsight/ui/src/components/consents-settings/ConsentOption/ConsentOption.spec.tsx new file mode 100644 index 0000000000..ddab11b853 --- /dev/null +++ b/redisinsight/ui/src/components/consents-settings/ConsentOption/ConsentOption.spec.tsx @@ -0,0 +1,126 @@ +import React from 'react' +import { render, screen, fireEvent } from 'uiSrc/utils/test-utils' +import ConsentOption from './ConsentOption' +import { IConsent } from '../ConsentsSettings' + +const mockConsent: IConsent = { + agreementName: 'analytics', + title: 'Analytics', + label: 'Share usage data', + description: 'Help us improve Redis Insight by sharing usage data.', + required: false, + editable: true, + disabled: false, + defaultValue: false, + displayInSetting: true, + since: '1.0.0', + linkToPrivacyPolicy: false, +} + +const mockOnChangeAgreement = jest.fn() + +const defaultProps = { + consent: mockConsent, + onChangeAgreement: mockOnChangeAgreement, + checked: false, +} + +describe('ConsentOption', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should render switch with correct test id', () => { + render() + expect(screen.getByTestId('switch-option-analytics')).toBeInTheDocument() + }) + + it('should call onChangeAgreement when switch is clicked', () => { + render() + + fireEvent.click(screen.getByTestId('switch-option-analytics')) + + expect(mockOnChangeAgreement).toHaveBeenCalledWith(true, 'analytics') + }) + + it('should render description without privacy policy link when linkToPrivacyPolicy is false', () => { + const consentWithDescription = { + ...mockConsent, + description: 'Help us improve Redis Insight by sharing usage data.', + linkToPrivacyPolicy: false, + } + + render() + + expect(screen.getByText('Help us improve Redis Insight by sharing usage data.')).toBeInTheDocument() + expect(screen.queryByText('Privacy Policy')).not.toBeInTheDocument() + }) + + it('should render description with privacy policy link when linkToPrivacyPolicy is true', () => { + const consentWithPrivacyLink = { + ...mockConsent, + description: 'Help us improve Redis Insight by sharing usage data.', + linkToPrivacyPolicy: true, + } + + render() + + // Verify that the Privacy Policy link is rendered + expect(screen.getByText('Privacy Policy')).toBeInTheDocument() + + const privacyPolicyLink = screen.getByText('Privacy Policy') + expect(privacyPolicyLink.closest('a')).toHaveAttribute( + 'href', + 'https://redis.io/legal/privacy-policy/?utm_source=redisinsight&utm_medium=app&utm_campaign=telemetry' + ) + }) + + it('should render description with privacy policy link on settings page when linkToPrivacyPolicy is true', () => { + const consentWithPrivacyLink = { + ...mockConsent, + description: 'Help us improve Redis Insight by sharing usage data.', + linkToPrivacyPolicy: true, + } + + render() + + // Verify that the Privacy Policy link is rendered + expect(screen.getByText('Privacy Policy')).toBeInTheDocument() + }) + + it('should not render privacy policy link on settings page when linkToPrivacyPolicy is false', () => { + const consentWithoutPrivacyLink = { + ...mockConsent, + description: 'Help us improve Redis Insight by sharing usage data.', + linkToPrivacyPolicy: false, + } + + render() + + expect(screen.getByText('Help us improve Redis Insight by sharing usage data.')).toBeInTheDocument() + expect(screen.queryByText('Privacy Policy')).not.toBeInTheDocument() + }) + + it('should render disabled switch when consent is disabled', () => { + const disabledConsent = { + ...mockConsent, + disabled: true, + } + + render() + + const switchElement = screen.getByTestId('switch-option-analytics') + expect(switchElement).toBeDisabled() + }) + + it('should render checked switch when checked prop is true', () => { + render() + + const switchElement = screen.getByTestId('switch-option-analytics') + expect(switchElement).toBeChecked() + }) +}) \ No newline at end of file diff --git a/redisinsight/ui/src/components/consents-settings/ConsentOption/ConsentOption.tsx b/redisinsight/ui/src/components/consents-settings/ConsentOption/ConsentOption.tsx index 3f439819c5..9da0b46b37 100644 --- a/redisinsight/ui/src/components/consents-settings/ConsentOption/ConsentOption.tsx +++ b/redisinsight/ui/src/components/consents-settings/ConsentOption/ConsentOption.tsx @@ -15,7 +15,6 @@ interface Props { checked: boolean isSettingsPage?: boolean withoutSpacer?: boolean - linkToPrivacyPolicy?: boolean } const ConsentOption = (props: Props) => { @@ -25,7 +24,6 @@ const ConsentOption = (props: Props) => { checked, isSettingsPage = false, withoutSpacer = false, - linkToPrivacyPolicy = false, } = props return ( @@ -38,7 +36,7 @@ const ConsentOption = (props: Props) => { color="subdued" style={{ marginTop: '12px' }} > - + @@ -66,7 +64,7 @@ const ConsentOption = (props: Props) => { color="subdued" style={{ marginTop: '12px' }} > - + )} diff --git a/redisinsight/ui/src/components/consents-settings/ConsentsPrivacy/ConsentsPrivacy.tsx b/redisinsight/ui/src/components/consents-settings/ConsentsPrivacy/ConsentsPrivacy.tsx index 599480169e..8139d4304b 100644 --- a/redisinsight/ui/src/components/consents-settings/ConsentsPrivacy/ConsentsPrivacy.tsx +++ b/redisinsight/ui/src/components/consents-settings/ConsentsPrivacy/ConsentsPrivacy.tsx @@ -97,7 +97,6 @@ const ConsentsPrivacy = () => { onChangeAgreement={onChangeAgreement} isSettingsPage key={consent.agreementName} - linkToPrivacyPolicy /> ))} diff --git a/redisinsight/ui/src/components/consents-settings/ConsentsSettings.tsx b/redisinsight/ui/src/components/consents-settings/ConsentsSettings.tsx index 2cf52b45ad..6e23034f60 100644 --- a/redisinsight/ui/src/components/consents-settings/ConsentsSettings.tsx +++ b/redisinsight/ui/src/components/consents-settings/ConsentsSettings.tsx @@ -38,6 +38,7 @@ export interface IConsent { required: boolean editable: boolean disabled: boolean + linkToPrivacyPolicy: boolean category?: string since: string title: string @@ -222,17 +223,6 @@ const ConsentsSettings = ({ onSubmitted }: Props) => { {consents.length > 1 && ( <> - - - To avoid automatic execution of malicious code, when adding new - Workbench plugins, use files from trusted authors only. - - - @@ -287,7 +277,6 @@ const ConsentsSettings = ({ onSubmitted }: Props) => { checked={formik.values[consent.agreementName] ?? false} onChangeAgreement={onChangeAgreement} key={consent.agreementName} - linkToPrivacyPolicy /> ))} {!!notificationConsents.length && ( diff --git a/tests/e2e/tests/electron/critical-path/a-first-start-form/user-agreements-form.e2e.ts b/tests/e2e/tests/electron/critical-path/a-first-start-form/user-agreements-form.e2e.ts index 50faeec5fd..61fed84ac5 100644 --- a/tests/e2e/tests/electron/critical-path/a-first-start-form/user-agreements-form.e2e.ts +++ b/tests/e2e/tests/electron/critical-path/a-first-start-form/user-agreements-form.e2e.ts @@ -17,14 +17,9 @@ test('Verify that user should accept User Agreements to continue working with th await t.expect(userAgreementDialog.submitButton.hasAttribute('disabled')).ok('Submit button not disabled by default'); await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.customSettingsButton.exists).notOk('User can\'t add a database'); }); -test('Verify that the encryption enabled by default and specific message', async t => { - const expectedPluginText = 'To avoid automatic execution of malicious code, when adding new Workbench plugins, use files from trusted authors only.'; - // Verify that section with plugin warning is displayed +test('Verify that the encryption enabled by default and specific message', async t => { await t.expect(userAgreementDialog.pluginSectionWithText.exists).ok('Plugin text is not displayed'); // Verify that text that is displayed in window is 'While adding new visualization plugins, use files only from trusted authors to avoid automatic execution of malicious code.' - const pluginText = userAgreementDialog.pluginSectionWithText.innerText; - await t.expect(pluginText).eql(expectedPluginText, 'Plugin text is incorrect'); - // unskip the verification when encription will be fixed for test builds // // Verify that encryption enabled by default // await t.expect(userAgreementDialog.switchOptionEncryption.withAttribute('aria-checked', 'true').exists).ok('Encryption enabled by default'); diff --git a/tests/e2e/tests/web/critical-path/a-first-start-form/user-agreements-form.e2e.ts b/tests/e2e/tests/web/critical-path/a-first-start-form/user-agreements-form.e2e.ts index 552ef9bf30..b99fbfeaae 100644 --- a/tests/e2e/tests/web/critical-path/a-first-start-form/user-agreements-form.e2e.ts +++ b/tests/e2e/tests/web/critical-path/a-first-start-form/user-agreements-form.e2e.ts @@ -22,12 +22,7 @@ test('Verify that user should accept User Agreements to continue working with th await t.expect(myRedisDatabasePage.AddRedisDatabaseDialog.customSettingsButton.exists).notOk('User can\'t add a database'); }); test('Verify that the encryption enabled by default and specific message', async t => { - const expectedPluginText = 'To avoid automatic execution of malicious code, when adding new Workbench plugins, use files from trusted authors only.'; - // Verify that section with plugin warning is displayed await t.expect(userAgreementDialog.pluginSectionWithText.exists).ok('Plugin text is not displayed'); - // Verify that text that is displayed in window is 'While adding new visualization plugins, use files only from trusted authors to avoid automatic execution of malicious code.' - const pluginText = userAgreementDialog.pluginSectionWithText.innerText; - await t.expect(pluginText).eql(expectedPluginText, 'Plugin text is incorrect'); // Verify that encryption enabled by default await t.expect(userAgreementDialog.switchOptionEncryption.withAttribute('aria-checked', 'true').exists).ok('Encryption enabled by default'); });