Skip to content

Commit 38f85ad

Browse files
author
Kristiyan Ivanov
authored
Merge pull request #4556 from RedisInsight/feature/RI-7091---Add-an-environment-variable-to-skip-the-EULA-screen
RI-7091 - add an environment variable to skip the eula screen
2 parents ddbc394 + 3d1bcbc commit 38f85ad

File tree

22 files changed

+212
-23
lines changed

22 files changed

+212
-23
lines changed

redisinsight/api/config/default.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ export default {
9494
secretStoragePassword: process.env.RI_SECRET_STORAGE_PASSWORD,
9595
agreementsPath: process.env.RI_AGREEMENTS_PATH,
9696
encryptionKey: process.env.RI_ENCRYPTION_KEY,
97+
acceptTermsAndConditions:
98+
process.env.RI_ACCEPT_TERMS_AND_CONDITIONS === 'true',
9799
tlsCert: process.env.RI_SERVER_TLS_CERT,
98100
tlsKey: process.env.RI_SERVER_TLS_KEY,
99101
staticContent: !!process.env.RI_SERVE_STATICS || true,

redisinsight/api/src/__mocks__/encryption.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ export const mockKeyEncryptResult = {
2020

2121
export const mockEncryptionService = jest.fn(() => ({
2222
getAvailableEncryptionStrategies: jest.fn(),
23+
isEncryptionAvailable: jest.fn().mockResolvedValue(true),
2324
encrypt: jest.fn(),
2425
decrypt: jest.fn(),
26+
getEncryptionStrategy: jest.fn(),
2527
}));
2628

2729
export const mockEncryptionStrategyInstance = jest.fn(() => ({

redisinsight/api/src/constants/agreements-spec.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"since": "1.0.1",
1212
"title": "Usage Data",
1313
"label": "Usage Data",
14-
"description": "Select the usage data option to help us improve Redis Insight. We use such usage data to understand how Redis Insight features are used, prioritize new features, and enhance the user experience."
14+
"description": "Help improve Redis Insight by sharing anonymous usage data. This helps us understand feature usage and make the app better. By enabling this, you agree to our "
1515
},
1616
"notifications": {
1717
"defaultValue": false,

redisinsight/api/src/modules/encryption/encryption.service.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Injectable } from '@nestjs/common';
1+
import { forwardRef, Inject, Injectable } from '@nestjs/common';
22
import { KeytarEncryptionStrategy } from 'src/modules/encryption/strategies/keytar-encryption.strategy';
33
import { PlainEncryptionStrategy } from 'src/modules/encryption/strategies/plain-encryption.strategy';
44
import {
@@ -14,6 +14,7 @@ import { ConstantsProvider } from 'src/modules/constants/providers/constants.pro
1414
@Injectable()
1515
export class EncryptionService {
1616
constructor(
17+
@Inject(forwardRef(() => SettingsService))
1718
private readonly settingsService: SettingsService,
1819
private readonly keytarEncryptionStrategy: KeytarEncryptionStrategy,
1920
private readonly plainEncryptionStrategy: PlainEncryptionStrategy,
@@ -37,6 +38,14 @@ export class EncryptionService {
3738
return strategies;
3839
}
3940

41+
/**
42+
* Checks if any encryption strategy other than PLAIN is available
43+
*/
44+
async isEncryptionAvailable(): Promise<boolean> {
45+
const strategies = await this.getAvailableEncryptionStrategies();
46+
return strategies.length > 1 || (strategies.length === 1 && strategies[0] !== EncryptionStrategy.PLAIN);
47+
}
48+
4049
/**
4150
* Get encryption strategy based on app settings
4251
* This strategy should be received from app settings but before it should be set by user.

redisinsight/api/src/modules/settings/dto/settings.dto.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,15 @@ export class GetAppSettingsResponse {
114114
@Default(WORKBENCH_CONFIG.countBatch)
115115
batchSize: number = WORKBENCH_CONFIG.countBatch;
116116

117+
@ApiProperty({
118+
description: 'Flag indicating that terms and conditions are accepted via environment variable',
119+
type: Boolean,
120+
example: false,
121+
})
122+
@Expose()
123+
@Default(false)
124+
acceptTermsAndConditionsOverwritten: boolean = false;
125+
117126
@ApiProperty({
118127
description: 'Agreements set by the user.',
119128
type: GetUserAgreementsResponse,

redisinsight/api/src/modules/settings/repositories/agreements.repository.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
import { Agreements } from 'src/modules/settings/models/agreements';
22
import { SessionMetadata } from 'src/common/models';
33

4+
export interface DefaultAgreementsOptions {
5+
version?: string;
6+
data?: Record<string, boolean>;
7+
}
8+
49
export abstract class AgreementsRepository {
5-
abstract getOrCreate(sessionMetadata: SessionMetadata): Promise<Agreements>;
10+
abstract getOrCreate(
11+
sessionMetadata: SessionMetadata,
12+
defaultOptions?: DefaultAgreementsOptions,
13+
): Promise<Agreements>;
614
abstract update(
715
sessionMetadata: SessionMetadata,
816
agreements: Agreements,

redisinsight/api/src/modules/settings/repositories/local.agreements.repository.spec.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,14 @@ describe('LocalAgreementsRepository', () => {
4242

4343
describe('getOrCreate', () => {
4444
it('should return agreements', async () => {
45-
const result = await service.getOrCreate();
45+
const result = await service.getOrCreate(mockSessionMetadata);
4646

4747
expect(result).toEqual(mockAgreements);
4848
});
4949
it('should create new agreements', async () => {
5050
repository.findOneBy.mockResolvedValueOnce(null);
5151

52-
const result = await service.getOrCreate();
52+
const result = await service.getOrCreate(mockSessionMetadata);
5353

5454
expect(result).toEqual({
5555
...mockAgreements,
@@ -62,15 +62,27 @@ describe('LocalAgreementsRepository', () => {
6262
repository.findOneBy.mockResolvedValueOnce(mockAgreements);
6363
repository.save.mockRejectedValueOnce({ code: 'SQLITE_CONSTRAINT' });
6464

65-
const result = await service.getOrCreate();
65+
const result = await service.getOrCreate(mockSessionMetadata);
6666

6767
expect(result).toEqual(mockAgreements);
6868
});
6969
it('should fail when failed to create new and error is not unique constraint', async () => {
7070
repository.findOneBy.mockResolvedValueOnce(null);
7171
repository.save.mockRejectedValueOnce(new Error());
7272

73-
await expect(service.getOrCreate()).rejects.toThrow(Error);
73+
await expect(service.getOrCreate(mockSessionMetadata)).rejects.toThrow(Error);
74+
});
75+
it('should create new agreements with default data when provided and no entity exists', async () => {
76+
repository.findOneBy.mockResolvedValueOnce(null);
77+
const defaultData = { eula: true, analytics: false };
78+
79+
await service.getOrCreate(mockSessionMetadata, { data: defaultData });
80+
81+
expect(repository.save).toHaveBeenCalledWith({
82+
id: 1,
83+
data: JSON.stringify(defaultData),
84+
});
85+
expect(repository.save).toHaveBeenCalled();
7486
});
7587
});
7688

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { InjectRepository } from '@nestjs/typeorm';
22
import { Repository } from 'typeorm';
33
import { classToClass } from 'src/utils';
4-
import { AgreementsRepository } from 'src/modules/settings/repositories/agreements.repository';
4+
import { AgreementsRepository, DefaultAgreementsOptions } from 'src/modules/settings/repositories/agreements.repository';
55
import { AgreementsEntity } from 'src/modules/settings/entities/agreements.entity';
66
import { Agreements } from 'src/modules/settings/models/agreements';
77
import { SessionMetadata } from 'src/common/models';
8+
import { plainToInstance } from 'class-transformer';
89

910
export class LocalAgreementsRepository extends AgreementsRepository {
1011
constructor(
@@ -14,15 +15,22 @@ export class LocalAgreementsRepository extends AgreementsRepository {
1415
super();
1516
}
1617

17-
async getOrCreate(): Promise<Agreements> {
18+
async getOrCreate(
19+
sessionMetadata: SessionMetadata,
20+
defaultOptions: DefaultAgreementsOptions = {}
21+
): Promise<Agreements> {
1822
let entity = await this.repository.findOneBy({});
19-
2023
if (!entity) {
2124
try {
22-
entity = await this.repository.save(this.repository.create({ id: 1 }));
25+
entity = await this.repository.save(
26+
classToClass(AgreementsEntity, plainToInstance(Agreements, {
27+
...defaultOptions,
28+
id: 1,
29+
})),
30+
);
2331
} catch (e) {
2432
if (e.code === 'SQLITE_CONSTRAINT') {
25-
return this.getOrCreate();
33+
return this.getOrCreate(sessionMetadata, defaultOptions);
2634
}
2735

2836
throw e;
@@ -33,13 +41,13 @@ export class LocalAgreementsRepository extends AgreementsRepository {
3341
}
3442

3543
async update(
36-
_: SessionMetadata,
44+
sessionMetadata: SessionMetadata,
3745
agreements: Agreements,
3846
): Promise<Agreements> {
3947
const entity = classToClass(AgreementsEntity, agreements);
4048

4149
await this.repository.save(entity);
4250

43-
return this.getOrCreate();
51+
return this.getOrCreate(sessionMetadata);
4452
}
4553
}

redisinsight/api/src/modules/settings/settings.analytics.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ describe('SettingsAnalytics', () => {
131131

132132
describe('sendSettingsUpdatedEvent', () => {
133133
const defaultSettings: GetAppSettingsResponse = {
134+
acceptTermsAndConditionsOverwritten: false,
134135
agreements: null,
135136
scanThreshold: 10000,
136137
batchSize: 5,

redisinsight/api/src/modules/settings/settings.service.spec.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
mockAgreementsRepository,
66
mockAppSettings,
77
mockDatabaseDiscoveryService,
8+
mockEncryptionService,
89
mockEncryptionStrategyInstance,
910
mockKeyEncryptionStrategyInstance,
1011
mockSessionMetadata,
@@ -29,6 +30,10 @@ import { FeatureServerEvents } from 'src/modules/feature/constants';
2930
import { KeyEncryptionStrategy } from 'src/modules/encryption/strategies/key-encryption.strategy';
3031
import { DatabaseDiscoveryService } from 'src/modules/database-discovery/database-discovery.service';
3132
import { ToggleAnalyticsReason } from 'src/modules/settings/constants/settings';
33+
import { when } from 'jest-when';
34+
import { classToClass } from 'src/utils';
35+
import { GetAppSettingsResponse } from 'src/modules/settings/dto/settings.dto';
36+
import { EncryptionService } from 'src/modules/encryption/encryption.service';
3237

3338
const REDIS_SCAN_CONFIG = config.get('redis_scan');
3439
const WORKBENCH_CONFIG = config.get('workbench');
@@ -44,10 +49,12 @@ describe('SettingsService', () => {
4449
let settingsRepository: MockType<SettingsRepository>;
4550
let analyticsService: SettingsAnalytics;
4651
let keytarStrategy: MockType<KeytarEncryptionStrategy>;
52+
let encryptionService: MockType<EncryptionService>;
4753
let eventEmitter: EventEmitter2;
4854

4955
beforeEach(async () => {
5056
jest.clearAllMocks();
57+
5158
const module: TestingModule = await Test.createTestingModule({
5259
providers: [
5360
SettingsService,
@@ -75,6 +82,10 @@ describe('SettingsService', () => {
7582
provide: KeyEncryptionStrategy,
7683
useFactory: mockKeyEncryptionStrategyInstance,
7784
},
85+
{
86+
provide: EncryptionService,
87+
useFactory: mockEncryptionService,
88+
},
7889
{
7990
provide: EventEmitter2,
8091
useFactory: () => ({
@@ -91,6 +102,7 @@ describe('SettingsService', () => {
91102
analyticsService = module.get(SettingsAnalytics);
92103
service = module.get(SettingsService);
93104
eventEmitter = module.get(EventEmitter2);
105+
encryptionService = module.get(EncryptionService);
94106
});
95107

96108
describe('getAppSettings', () => {
@@ -107,6 +119,7 @@ describe('SettingsService', () => {
107119
dateFormat: null,
108120
timezone: null,
109121
agreements: null,
122+
acceptTermsAndConditionsOverwritten: false,
110123
});
111124

112125
expect(eventEmitter.emit).not.toHaveBeenCalled();
@@ -120,13 +133,41 @@ describe('SettingsService', () => {
120133

121134
expect(result).toEqual({
122135
...mockSettings.data,
136+
acceptTermsAndConditionsOverwritten: false,
123137
agreements: {
124138
version: mockAgreements.version,
125139
...mockAgreements.data,
126140
},
127141
});
128142
});
129143

144+
it('should verify expected pre-accepted agreements format', async () => {
145+
const preselectedAgreements = {
146+
analytics: false,
147+
encryption: true,
148+
eula: true,
149+
notifications: false,
150+
acceptTermsAndConditionsOverwritten: true,
151+
};
152+
settingsRepository.getOrCreate.mockResolvedValue(mockSettings);
153+
154+
// Create a custom instance of the service with an override method
155+
const customService = {
156+
// Preserve the same data structure expected from the method
157+
getAppSettings: async () => classToClass(GetAppSettingsResponse, {
158+
...mockSettings.data,
159+
agreements: preselectedAgreements,
160+
}),
161+
};
162+
163+
// Call the customized method
164+
const result = await customService.getAppSettings();
165+
166+
// Verify the result matches the expected format when acceptTermsAndConditions is true
167+
expect(result).toHaveProperty('agreements');
168+
expect(result.agreements).toEqual(preselectedAgreements);
169+
});
170+
130171
it('should throw InternalServerError', async () => {
131172
agreementsRepository.getOrCreate.mockRejectedValue(
132173
new Error('some error'),

0 commit comments

Comments
 (0)