mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-17 01:17:29 +02:00
feat: user settings
This commit is contained in:
parent
7c37de373f
commit
dcca4f8e15
@ -27,7 +27,12 @@ const USER_COLUMNS_PUBLIC = [
|
||||
'scim_id',
|
||||
];
|
||||
|
||||
const USER_COLUMNS = [...USER_COLUMNS_PUBLIC, 'login_attempts', 'created_at'];
|
||||
const USER_COLUMNS = [
|
||||
...USER_COLUMNS_PUBLIC,
|
||||
'login_attempts',
|
||||
'created_at',
|
||||
'settings',
|
||||
];
|
||||
|
||||
const emptify = (value) => {
|
||||
if (!value) {
|
||||
@ -60,6 +65,7 @@ const rowToUser = (row) => {
|
||||
createdAt: row.created_at,
|
||||
isService: row.is_service,
|
||||
scimId: row.scim_id,
|
||||
settings: row.settings,
|
||||
});
|
||||
};
|
||||
|
||||
@ -308,6 +314,31 @@ class UserStore implements IUserStore {
|
||||
|
||||
return firstInstanceUser ? firstInstanceUser.created_at : null;
|
||||
}
|
||||
|
||||
async getSettings(userId: number): Promise<Record<string, string>> {
|
||||
const row = await this.activeUsers()
|
||||
.where({ id: userId })
|
||||
.first('settings');
|
||||
if (!row) {
|
||||
throw new NotFoundError('User not found');
|
||||
}
|
||||
return row.settings || {};
|
||||
}
|
||||
|
||||
async setSettings(
|
||||
userId: number,
|
||||
newSettings: Record<string, string | null>,
|
||||
): Promise<Record<string, string>> {
|
||||
const oldSettings = await this.getSettings(userId);
|
||||
const settings = { ...oldSettings, ...newSettings };
|
||||
Object.keys(settings).forEach((key) => {
|
||||
if (settings[key] === null) {
|
||||
delete settings[key];
|
||||
}
|
||||
});
|
||||
await this.activeUsers().where({ id: userId }).update({ settings });
|
||||
return settings as Record<string, string>;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = UserStore;
|
||||
|
11
src/lib/features/user-settings/createUserSettingsService.ts
Normal file
11
src/lib/features/user-settings/createUserSettingsService.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import type { IUnleashConfig, IUnleashStores } from '../../types';
|
||||
import type EventService from '../events/event-service';
|
||||
import { UserSettingsService } from './user-settings-service';
|
||||
|
||||
export const createUserSettingsService = (
|
||||
stores: Pick<IUnleashStores, 'userStore'>,
|
||||
config: Pick<IUnleashConfig, 'getLogger'>,
|
||||
eventService: EventService,
|
||||
): UserSettingsService => {
|
||||
return new UserSettingsService(stores, config, eventService);
|
||||
};
|
98
src/lib/features/user-settings/user-settings-controller.ts
Normal file
98
src/lib/features/user-settings/user-settings-controller.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import Controller from '../../routes/controller';
|
||||
import type { OpenApiService } from '../../services';
|
||||
import type { UserSettingsService } from './user-settings-service';
|
||||
import type {
|
||||
IFlagResolver,
|
||||
IUnleashConfig,
|
||||
IUnleashServices,
|
||||
} from '../../types';
|
||||
import {
|
||||
createRequestSchema,
|
||||
createResponseSchema,
|
||||
getStandardResponses,
|
||||
} from '../../openapi';
|
||||
import { ForbiddenError } from '../../error';
|
||||
|
||||
export default class UserSettingsController extends Controller {
|
||||
private userSettingsService: UserSettingsService;
|
||||
|
||||
private flagResolver: IFlagResolver;
|
||||
|
||||
private openApiService: OpenApiService;
|
||||
|
||||
constructor(config: IUnleashConfig, services: IUnleashServices) {
|
||||
super(config);
|
||||
this.userSettingsService = services.userSettingsService;
|
||||
this.openApiService = services.openApiService;
|
||||
this.flagResolver = config.flagResolver;
|
||||
|
||||
this.route({
|
||||
method: 'get',
|
||||
path: '',
|
||||
handler: this.getUserSettings,
|
||||
permission: 'user',
|
||||
middleware: [
|
||||
this.openApiService.validPath({
|
||||
tags: ['Unstable'], // TODO: Remove this tag when the endpoint is stable
|
||||
operationId: 'getUserSettings',
|
||||
summary: 'Get user settings',
|
||||
description:
|
||||
'Get the settings for the currently authenticated user.',
|
||||
responses: {
|
||||
200: createResponseSchema('userSettingsSchema'),
|
||||
...getStandardResponses(401, 403, 404),
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
this.route({
|
||||
method: 'put',
|
||||
path: '',
|
||||
handler: this.updateUserSettings,
|
||||
permission: 'user',
|
||||
middleware: [
|
||||
this.openApiService.validPath({
|
||||
tags: ['Unstable'], // TODO: Update/remove when endpoint stabilizes
|
||||
operationId: 'updateUserSettings',
|
||||
summary: 'Update user settings',
|
||||
description: 'Update a specific user setting by key.',
|
||||
requestBody: createRequestSchema('setUserSettingSchema'),
|
||||
responses: {
|
||||
204: { description: 'Setting updated successfully' },
|
||||
...getStandardResponses(400, 401, 403, 409, 415),
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async getUserSettings(req, res) {
|
||||
console.log({ req, flag: this.flagResolver.isEnabled('userSettings') });
|
||||
if (!this.flagResolver.isEnabled('userSettings')) {
|
||||
throw new ForbiddenError('User settings feature is not enabled');
|
||||
}
|
||||
const { user } = req;
|
||||
const settings = await this.userSettingsService.getAll(user.id);
|
||||
res.json(settings);
|
||||
}
|
||||
|
||||
async updateUserSettings(req, res) {
|
||||
if (!this.flagResolver.isEnabled('userSettings')) {
|
||||
throw new ForbiddenError('User settings feature is not enabled');
|
||||
}
|
||||
const { user } = req;
|
||||
const { key, value } = req.body;
|
||||
const allowedSettings = ['productivity-insights-email'];
|
||||
|
||||
if (!allowedSettings.includes(key)) {
|
||||
res.status(400).json({
|
||||
message: `Invalid setting key`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await this.userSettingsService.set(user.id, key, value, user);
|
||||
res.status(204).end();
|
||||
}
|
||||
}
|
48
src/lib/features/user-settings/user-settings-service.ts
Normal file
48
src/lib/features/user-settings/user-settings-service.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import type { IUnleashStores } from '../../types/stores';
|
||||
|
||||
import type { Logger } from '../../logger';
|
||||
import type { IUnleashConfig } from '../../types/option';
|
||||
import type EventService from '../events/event-service';
|
||||
import {
|
||||
type IAuditUser,
|
||||
UserSettingsUpdatedEvent,
|
||||
type IUserStore,
|
||||
} from '../../types';
|
||||
import type { UserSettingsSchema } from '../../openapi/spec/user-settings-schema';
|
||||
|
||||
export class UserSettingsService {
|
||||
private userStore: IUserStore;
|
||||
|
||||
private eventService: EventService;
|
||||
|
||||
private logger: Logger;
|
||||
|
||||
constructor(
|
||||
{ userStore }: Pick<IUnleashStores, 'userStore'>,
|
||||
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
|
||||
eventService: EventService,
|
||||
) {
|
||||
this.userStore = userStore;
|
||||
this.eventService = eventService;
|
||||
this.logger = getLogger('services/user-settings-service.js');
|
||||
}
|
||||
|
||||
async getAll(userId: number): Promise<UserSettingsSchema['settings']> {
|
||||
return this.userStore.getSettings(userId);
|
||||
}
|
||||
|
||||
async set(
|
||||
userId: number,
|
||||
param: string,
|
||||
value: string,
|
||||
auditUser: IAuditUser,
|
||||
) {
|
||||
await this.userStore.setSettings(userId, { [param]: value });
|
||||
await this.eventService.storeEvent(
|
||||
new UserSettingsUpdatedEvent({
|
||||
auditUser,
|
||||
data: { userId, param, value },
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
111
src/lib/features/user-settings/user-settings.e2e.test.ts
Normal file
111
src/lib/features/user-settings/user-settings.e2e.test.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init';
|
||||
import {
|
||||
type IUnleashTest,
|
||||
setupAppWithAuth,
|
||||
} from '../../../test/e2e/helpers/test-helper';
|
||||
import getLogger from '../../../test/fixtures/no-logger';
|
||||
import type { IUserStore } from '../../types';
|
||||
|
||||
let app: IUnleashTest;
|
||||
let db: ITestDb;
|
||||
let userStore: IUserStore;
|
||||
|
||||
const loginUser = (email: string) => {
|
||||
return app.request
|
||||
.post(`/auth/demo/login`)
|
||||
.send({
|
||||
email,
|
||||
})
|
||||
.expect(200);
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
db = await dbInit('user_settings', getLogger);
|
||||
userStore = db.stores.userStore;
|
||||
app = await setupAppWithAuth(
|
||||
db.stores,
|
||||
{
|
||||
experimental: {
|
||||
flags: {
|
||||
userSettings: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
db.rawDatabase,
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
getLogger.setMuteError(false);
|
||||
await app.destroy();
|
||||
await db.destroy();
|
||||
});
|
||||
|
||||
// beforeEach(async () => {
|
||||
// await db.stores.featureToggleStore.deleteAll();
|
||||
// await db.stores.userStore.deleteAll();
|
||||
// await db.stores.eventStore.deleteAll();
|
||||
// await db.stores.userStore.deleteAll();
|
||||
// });
|
||||
|
||||
describe('UserSettingsController', () => {
|
||||
test('should return user settings', async () => {
|
||||
const { body: user } = await loginUser('test@example.com');
|
||||
// console.log({user})
|
||||
// await db.stores.userStore.setSettings(1, {
|
||||
// 'productivity-insights-email': 'true',
|
||||
// });
|
||||
const { body } = await app.request
|
||||
.put(`/api/admin/user/settings`)
|
||||
.send({
|
||||
key: 'productivity-insights-email',
|
||||
value: 'new_value',
|
||||
})
|
||||
.expect(204);
|
||||
|
||||
const res = await app.request
|
||||
.get('/api/admin/user/settings')
|
||||
.expect(200);
|
||||
|
||||
expect(res.body).toEqual({ 'productivity-insights-email': 'true' });
|
||||
});
|
||||
|
||||
// test('should return empty object if no settings are available', async () => {
|
||||
// const res = await app.request
|
||||
// .get('/api/admin/user/settings')
|
||||
// // .set('Authorization', `Bearer ${userId}`)
|
||||
// .expect(200);
|
||||
|
||||
// expect(res.body).toEqual({});
|
||||
// });
|
||||
|
||||
// describe('PUT /settings/:key', () => {
|
||||
// const allowedKey = 'productivity-insights-email';
|
||||
|
||||
// test('should update user setting if key is valid', async () => {
|
||||
// const res = await app.request
|
||||
// .put(`/api/admin/user/settings/${allowedKey}`)
|
||||
// // .set('Authorization', `Bearer ${userId}`)
|
||||
// .send({ value: 'new_value' })
|
||||
// .expect(204);
|
||||
|
||||
// expect(res.body).toEqual({});
|
||||
|
||||
// const updatedSetting =
|
||||
// await db.stores.userStore.getSettings(userId);
|
||||
// expect(updatedSetting.value).toEqual('new_value');
|
||||
// });
|
||||
|
||||
// test('should return 400 for invalid setting key', async () => {
|
||||
// const res = await app.request
|
||||
// .put(`/api/admin/user/settings/invalid-key`)
|
||||
// // .set('Authorization', `Bearer ${userId}`)
|
||||
// .send({ value: 'some_value' })
|
||||
// .expect(400);
|
||||
|
||||
// expect(res.body).toEqual({
|
||||
// message: 'Invalid setting key',
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
});
|
@ -179,6 +179,7 @@ export * from './segment-strategies-schema';
|
||||
export * from './segments-schema';
|
||||
export * from './set-strategy-sort-order-schema';
|
||||
export * from './set-ui-config-schema';
|
||||
export * from './set-user-setting-schema';
|
||||
export * from './sort-order-schema';
|
||||
export * from './splash-request-schema';
|
||||
export * from './splash-response-schema';
|
||||
@ -208,6 +209,7 @@ export * from './update-tags-schema';
|
||||
export * from './update-user-schema';
|
||||
export * from './upsert-segment-schema';
|
||||
export * from './user-schema';
|
||||
export * from './user-settings-schema';
|
||||
export * from './users-groups-base-schema';
|
||||
export * from './users-schema';
|
||||
export * from './users-search-schema';
|
||||
|
23
src/lib/openapi/spec/set-user-setting-schema.ts
Normal file
23
src/lib/openapi/spec/set-user-setting-schema.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import type { FromSchema } from 'json-schema-to-ts';
|
||||
|
||||
export const setUserSettingSchema = {
|
||||
$id: '#/components/schemas/setUserSettingSchema',
|
||||
type: 'object',
|
||||
description: 'Schema for setting a user-specific value',
|
||||
required: ['key', 'value'],
|
||||
properties: {
|
||||
key: {
|
||||
type: 'string',
|
||||
description: 'Setting key',
|
||||
example: 'email',
|
||||
},
|
||||
value: {
|
||||
type: 'string',
|
||||
description: 'The setting value for the user',
|
||||
example: 'optOut',
|
||||
},
|
||||
},
|
||||
components: {},
|
||||
} as const;
|
||||
|
||||
export type SetUserSettingSchema = FromSchema<typeof setUserSettingSchema>;
|
44
src/lib/openapi/spec/upsert-segment-schema.test copy.ts
Normal file
44
src/lib/openapi/spec/upsert-segment-schema.test copy.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { validateSchema } from '../validate';
|
||||
|
||||
test('upsertSegmentSchema', () => {
|
||||
const validObjects = [
|
||||
{
|
||||
name: 'segment',
|
||||
constraints: [],
|
||||
},
|
||||
{
|
||||
name: 'segment',
|
||||
description: 'description',
|
||||
constraints: [],
|
||||
},
|
||||
{
|
||||
name: 'segment',
|
||||
description: 'description',
|
||||
constraints: [],
|
||||
additional: 'property',
|
||||
},
|
||||
];
|
||||
|
||||
validObjects.forEach((obj) =>
|
||||
expect(
|
||||
validateSchema('#/components/schemas/upsertSegmentSchema', obj),
|
||||
).toBeUndefined(),
|
||||
);
|
||||
|
||||
const invalidObjects = [
|
||||
{
|
||||
name: 'segment',
|
||||
},
|
||||
{
|
||||
description: 'description',
|
||||
constraints: [],
|
||||
},
|
||||
{},
|
||||
];
|
||||
|
||||
invalidObjects.forEach((obj) =>
|
||||
expect(
|
||||
validateSchema('#/components/schemas/upsertSegmentSchema', obj),
|
||||
).toMatchSnapshot(),
|
||||
);
|
||||
});
|
23
src/lib/openapi/spec/user-settings-schema.ts
Normal file
23
src/lib/openapi/spec/user-settings-schema.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import type { FromSchema } from 'json-schema-to-ts';
|
||||
|
||||
export const userSettingsSchema = {
|
||||
$id: '#/components/schemas/userSettingsSchema',
|
||||
type: 'object',
|
||||
required: ['settings'],
|
||||
description: 'Schema representing user-specific settings in the system.',
|
||||
properties: {
|
||||
settings: {
|
||||
type: 'object',
|
||||
additionalProperties: {
|
||||
type: 'string',
|
||||
description: 'A user setting, represented as a key-value pair.',
|
||||
example: '{"dark_mode_enabled": "true"}',
|
||||
},
|
||||
description:
|
||||
'An object containing key-value pairs representing user settings.',
|
||||
},
|
||||
},
|
||||
components: {},
|
||||
} as const;
|
||||
|
||||
export type UserSettingsSchema = FromSchema<typeof userSettingsSchema>;
|
@ -36,6 +36,7 @@ import { InactiveUsersController } from '../../users/inactive/inactive-users-con
|
||||
import { UiObservabilityController } from '../../features/ui-observability-controller/ui-observability-controller';
|
||||
import { SearchApi } from './search';
|
||||
import PersonalDashboardController from '../../features/personal-dashboard/personal-dashboard-controller';
|
||||
import UserSettingsController from '../../features/user-settings/user-settings-controller';
|
||||
|
||||
export class AdminApi extends Controller {
|
||||
constructor(config: IUnleashConfig, services: IUnleashServices, db: Db) {
|
||||
@ -80,6 +81,10 @@ export class AdminApi extends Controller {
|
||||
'/user/tokens',
|
||||
new PatController(config, services).router,
|
||||
);
|
||||
this.app.use(
|
||||
'/user/settings',
|
||||
new UserSettingsController(config, services).router,
|
||||
);
|
||||
|
||||
this.app.use(
|
||||
'/ui-config',
|
||||
|
@ -153,6 +153,7 @@ import {
|
||||
createFakePersonalDashboardService,
|
||||
createPersonalDashboardService,
|
||||
} from '../features/personal-dashboard/createPersonalDashboardService';
|
||||
import { createUserSettingsService } from '../features/user-settings/createUserSettingsService';
|
||||
|
||||
export const createServices = (
|
||||
stores: IUnleashStores,
|
||||
@ -236,6 +237,12 @@ export const createServices = (
|
||||
sessionService,
|
||||
settingService,
|
||||
});
|
||||
const userSettingsService = createUserSettingsService(
|
||||
stores,
|
||||
config,
|
||||
eventService,
|
||||
);
|
||||
|
||||
const accountService = new AccountService(stores, config, {
|
||||
accessService,
|
||||
});
|
||||
@ -482,6 +489,7 @@ export const createServices = (
|
||||
integrationEventsService,
|
||||
onboardingService,
|
||||
personalDashboardService,
|
||||
userSettingsService,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -204,6 +204,8 @@ export const ACTIONS_CREATED = 'actions-created' as const;
|
||||
export const ACTIONS_UPDATED = 'actions-updated' as const;
|
||||
export const ACTIONS_DELETED = 'actions-deleted' as const;
|
||||
|
||||
export const USER_SETTINGS_UPDATED = 'user-settings-updated' as const;
|
||||
|
||||
export const IEventTypes = [
|
||||
APPLICATION_CREATED,
|
||||
FEATURE_CREATED,
|
||||
@ -351,6 +353,7 @@ export const IEventTypes = [
|
||||
ACTIONS_CREATED,
|
||||
ACTIONS_UPDATED,
|
||||
ACTIONS_DELETED,
|
||||
USER_SETTINGS_UPDATED,
|
||||
] as const;
|
||||
export type IEventType = (typeof IEventTypes)[number];
|
||||
|
||||
@ -2024,3 +2027,16 @@ function mapUserToData(user: IUserEventData): any {
|
||||
rootRole: user.rootRole,
|
||||
};
|
||||
}
|
||||
|
||||
export class UserSettingsUpdatedEvent extends BaseEvent {
|
||||
readonly data: any;
|
||||
readonly preData: any;
|
||||
|
||||
constructor(eventData: {
|
||||
auditUser: IAuditUser;
|
||||
data: any;
|
||||
}) {
|
||||
super(USER_SETTINGS_UPDATED, eventData.auditUser);
|
||||
this.data = eventData.data;
|
||||
}
|
||||
}
|
||||
|
@ -62,7 +62,8 @@ export type IFlagKey =
|
||||
| 'addonUsageMetrics'
|
||||
| 'releasePlans'
|
||||
| 'navigationSidebar'
|
||||
| 'productivityReportEmail';
|
||||
| 'productivityReportEmail'
|
||||
| 'userSettings';
|
||||
|
||||
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
||||
|
||||
@ -311,6 +312,10 @@ const flags: IFlags = {
|
||||
process.env.UNLEASH_EXPERIMENTAL_PRODUCTIVITY_REPORT_EMAIL,
|
||||
false,
|
||||
),
|
||||
userSettings: parseEnvVarBoolean(
|
||||
process.env.UNLEASH_EXPERIMENTAL_USER_SETTINGS,
|
||||
false,
|
||||
),
|
||||
};
|
||||
|
||||
export const defaultExperimentalOptions: IExperimentalOptions = {
|
||||
|
@ -57,6 +57,7 @@ import type { FeatureLifecycleService } from '../features/feature-lifecycle/feat
|
||||
import type { IntegrationEventsService } from '../features/integration-events/integration-events-service';
|
||||
import type { OnboardingService } from '../features/onboarding/onboarding-service';
|
||||
import type { PersonalDashboardService } from '../features/personal-dashboard/personal-dashboard-service';
|
||||
import type { UserSettingsService } from '../features/user-settings/user-settings-service';
|
||||
|
||||
export interface IUnleashServices {
|
||||
transactionalAccessService: WithTransactional<AccessService>;
|
||||
@ -126,4 +127,5 @@ export interface IUnleashServices {
|
||||
integrationEventsService: IntegrationEventsService;
|
||||
onboardingService: OnboardingService;
|
||||
personalDashboardService: PersonalDashboardService;
|
||||
userSettingsService: UserSettingsService;
|
||||
}
|
||||
|
@ -39,4 +39,9 @@ export interface IUserStore extends Store<IUser, number> {
|
||||
successfullyLogin(user: IUser): Promise<number>;
|
||||
count(): Promise<number>;
|
||||
countServiceAccounts(): Promise<number>;
|
||||
getSettings(userId: number): Promise<Record<string, string>>;
|
||||
setSettings(
|
||||
userId: number,
|
||||
settings: Record<string, string | null>,
|
||||
): Promise<Record<string, string>>;
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ export interface UserData {
|
||||
createdAt?: Date;
|
||||
isService?: boolean;
|
||||
scimId?: string;
|
||||
settings?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface IUser {
|
||||
@ -31,6 +32,7 @@ export interface IUser {
|
||||
imageUrl?: string;
|
||||
accountType?: AccountType;
|
||||
scimId?: string;
|
||||
settings?: Record<string, string>;
|
||||
}
|
||||
|
||||
export type MinimalUser = Pick<
|
||||
@ -73,6 +75,8 @@ export default class User implements IUser {
|
||||
|
||||
scimId?: string;
|
||||
|
||||
settings?: Record<string, string>;
|
||||
|
||||
constructor({
|
||||
id,
|
||||
name,
|
||||
@ -84,6 +88,7 @@ export default class User implements IUser {
|
||||
createdAt,
|
||||
isService,
|
||||
scimId,
|
||||
settings,
|
||||
}: UserData) {
|
||||
if (!id) {
|
||||
throw new ValidationError('Id is required', [], undefined);
|
||||
@ -102,6 +107,7 @@ export default class User implements IUser {
|
||||
this.createdAt = createdAt;
|
||||
this.accountType = isService ? 'Service Account' : 'User';
|
||||
this.scimId = scimId;
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
generateImageUrl(): string {
|
||||
|
@ -193,3 +193,26 @@ test('should delete user', async () => {
|
||||
new NotFoundError('No user found'),
|
||||
);
|
||||
});
|
||||
|
||||
test('should set and update user settings', async () => {
|
||||
const user = await stores.userStore.upsert({
|
||||
email: 'user.with.settings@example.com',
|
||||
});
|
||||
|
||||
await stores.userStore.setSettings(user.id, {
|
||||
theme: 'dark',
|
||||
});
|
||||
|
||||
expect(await stores.userStore.getSettings(user.id)).toEqual({
|
||||
theme: 'dark',
|
||||
});
|
||||
|
||||
await stores.userStore.setSettings(user.id, {
|
||||
emailOptOut: 'true',
|
||||
theme: null,
|
||||
});
|
||||
|
||||
expect(await stores.userStore.getSettings(user.id)).toEqual({
|
||||
emailOptOut: 'true',
|
||||
});
|
||||
});
|
||||
|
11
src/test/fixtures/fake-user-store.ts
vendored
11
src/test/fixtures/fake-user-store.ts
vendored
@ -182,6 +182,17 @@ class UserStoreMock implements IUserStore {
|
||||
async markSeenAt(secrets: string[]): Promise<void> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
async getSettings(userId: number): Promise<Record<string, string>> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
async setSettings(
|
||||
userId: number,
|
||||
settings: Record<string, string>,
|
||||
): Promise<Record<string, string>> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = UserStoreMock;
|
||||
|
Loading…
Reference in New Issue
Block a user