mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-27 01:19:00 +02:00
feat: onboarding store (#8027)
This commit is contained in:
parent
bb5aa64756
commit
f27e07ab88
@ -53,6 +53,7 @@ import { IntegrationEventsStore } from '../features/integration-events/integrati
|
|||||||
import { FeatureCollaboratorsReadModel } from '../features/feature-toggle/feature-collaborators-read-model';
|
import { FeatureCollaboratorsReadModel } from '../features/feature-toggle/feature-collaborators-read-model';
|
||||||
import { createProjectReadModel } from '../features/project/createProjectReadModel';
|
import { createProjectReadModel } from '../features/project/createProjectReadModel';
|
||||||
import { OnboardingReadModel } from '../features/onboarding/onboarding-read-model';
|
import { OnboardingReadModel } from '../features/onboarding/onboarding-read-model';
|
||||||
|
import { OnboardingStore } from '../features/onboarding/onboarding-store';
|
||||||
|
|
||||||
export const createStores = (
|
export const createStores = (
|
||||||
config: IUnleashConfig,
|
config: IUnleashConfig,
|
||||||
@ -173,6 +174,7 @@ export const createStores = (
|
|||||||
featureLifecycleStore: new FeatureLifecycleStore(db),
|
featureLifecycleStore: new FeatureLifecycleStore(db),
|
||||||
featureStrategiesReadModel: new FeatureStrategiesReadModel(db),
|
featureStrategiesReadModel: new FeatureStrategiesReadModel(db),
|
||||||
onboardingReadModel: new OnboardingReadModel(db),
|
onboardingReadModel: new OnboardingReadModel(db),
|
||||||
|
onboardingStore: new OnboardingStore(db),
|
||||||
featureLifecycleReadModel: new FeatureLifecycleReadModel(
|
featureLifecycleReadModel: new FeatureLifecycleReadModel(
|
||||||
db,
|
db,
|
||||||
config.flagResolver,
|
config.flagResolver,
|
||||||
|
@ -298,6 +298,15 @@ class UserStore implements IUserStore {
|
|||||||
const row = await this.activeUsers().where({ id }).first();
|
const row = await this.activeUsers().where({ id }).first();
|
||||||
return rowToUser(row);
|
return rowToUser(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getFirstUserDate(): Promise<Date | null> {
|
||||||
|
const firstInstanceUser = await this.db('users')
|
||||||
|
.select('created_at')
|
||||||
|
.orderBy('created_at', 'asc')
|
||||||
|
.first();
|
||||||
|
|
||||||
|
return firstInstanceUser ? firstInstanceUser.created_at : null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = UserStore;
|
module.exports = UserStore;
|
||||||
|
14
src/lib/features/onboarding/fake-onboarding-store.ts
Normal file
14
src/lib/features/onboarding/fake-onboarding-store.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import type {
|
||||||
|
InstanceEvent,
|
||||||
|
IOnboardingStore,
|
||||||
|
ProjectEvent,
|
||||||
|
} from './onboarding-store-type';
|
||||||
|
|
||||||
|
export class FakeOnboardingStore implements IOnboardingStore {
|
||||||
|
insertProjectEvent(event: ProjectEvent): Promise<void> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
insertInstanceEvent(event: InstanceEvent): Promise<void> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
}
|
87
src/lib/features/onboarding/onboarding-service.e2e.test.ts
Normal file
87
src/lib/features/onboarding/onboarding-service.e2e.test.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import type { IUnleashStores } from '../../../lib/types';
|
||||||
|
import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init';
|
||||||
|
import getLogger from '../../../test/fixtures/no-logger';
|
||||||
|
import { minutesToMilliseconds } from 'date-fns';
|
||||||
|
import { OnboardingService } from './onboarding-service';
|
||||||
|
import { createTestConfig } from '../../../test/config/test-config';
|
||||||
|
|
||||||
|
let db: ITestDb;
|
||||||
|
let stores: IUnleashStores;
|
||||||
|
let onboardingService: OnboardingService;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
db = await dbInit('onboarding_store', getLogger);
|
||||||
|
const config = createTestConfig({
|
||||||
|
experimental: { flags: { onboardingMetrics: true } },
|
||||||
|
});
|
||||||
|
stores = db.stores;
|
||||||
|
const { userStore, onboardingStore, projectReadModel } = stores;
|
||||||
|
onboardingService = new OnboardingService(
|
||||||
|
{ onboardingStore, userStore, projectReadModel },
|
||||||
|
config,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await db.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Storing onboarding events', async () => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
jest.setSystemTime(new Date());
|
||||||
|
const { userStore, featureToggleStore, projectStore, projectReadModel } =
|
||||||
|
stores;
|
||||||
|
const user = await userStore.insert({});
|
||||||
|
await projectStore.create({ id: 'test_project', name: 'irrelevant' });
|
||||||
|
await featureToggleStore.create('test_project', {
|
||||||
|
name: 'test',
|
||||||
|
createdByUserId: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.advanceTimersByTime(minutesToMilliseconds(1));
|
||||||
|
await onboardingService.insert({ type: 'first-user-login' });
|
||||||
|
jest.advanceTimersByTime(minutesToMilliseconds(1));
|
||||||
|
await onboardingService.insert({ type: 'second-user-login' });
|
||||||
|
jest.advanceTimersByTime(minutesToMilliseconds(1));
|
||||||
|
await onboardingService.insert({ type: 'flag-created', flag: 'test' });
|
||||||
|
await onboardingService.insert({ type: 'flag-created', flag: 'test' });
|
||||||
|
await onboardingService.insert({ type: 'flag-created', flag: 'invalid' });
|
||||||
|
jest.advanceTimersByTime(minutesToMilliseconds(1));
|
||||||
|
await onboardingService.insert({ type: 'pre-live', flag: 'test' });
|
||||||
|
await onboardingService.insert({ type: 'pre-live', flag: 'test' });
|
||||||
|
await onboardingService.insert({ type: 'pre-live', flag: 'invalid' });
|
||||||
|
jest.advanceTimersByTime(minutesToMilliseconds(1));
|
||||||
|
await onboardingService.insert({ type: 'live', flag: 'test' });
|
||||||
|
jest.advanceTimersByTime(minutesToMilliseconds(1));
|
||||||
|
await onboardingService.insert({ type: 'live', flag: 'test' });
|
||||||
|
jest.advanceTimersByTime(minutesToMilliseconds(1));
|
||||||
|
await onboardingService.insert({ type: 'live', flag: 'invalid' });
|
||||||
|
|
||||||
|
const { rows: instanceEvents } = await db.rawDatabase.raw(
|
||||||
|
'SELECT * FROM onboarding_events_instance',
|
||||||
|
);
|
||||||
|
expect(instanceEvents).toMatchObject([
|
||||||
|
{ event: 'first-user-login', time_to_event: 60 },
|
||||||
|
{ event: 'second-user-login', time_to_event: 120 },
|
||||||
|
{ event: 'first-flag', time_to_event: 180 },
|
||||||
|
{ event: 'first-pre-live', time_to_event: 240 },
|
||||||
|
{ event: 'first-live', time_to_event: 300 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { rows: projectEvents } = await db.rawDatabase.raw(
|
||||||
|
'SELECT * FROM onboarding_events_project',
|
||||||
|
);
|
||||||
|
expect(projectEvents).toMatchObject([
|
||||||
|
{ event: 'first-flag', time_to_event: 180, project: 'test_project' },
|
||||||
|
{
|
||||||
|
event: 'first-pre-live',
|
||||||
|
time_to_event: 240,
|
||||||
|
project: 'test_project',
|
||||||
|
},
|
||||||
|
{ event: 'first-live', time_to_event: 300, project: 'test_project' },
|
||||||
|
]);
|
||||||
|
});
|
135
src/lib/features/onboarding/onboarding-service.ts
Normal file
135
src/lib/features/onboarding/onboarding-service.ts
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import type {
|
||||||
|
IFlagResolver,
|
||||||
|
IProjectReadModel,
|
||||||
|
IUnleashConfig,
|
||||||
|
IUserStore,
|
||||||
|
} from '../../types';
|
||||||
|
import type EventEmitter from 'events';
|
||||||
|
import type { Logger } from '../../logger';
|
||||||
|
import { STAGE_ENTERED, USER_LOGIN } from '../../metric-events';
|
||||||
|
import type { NewStage } from '../feature-lifecycle/feature-lifecycle-store-type';
|
||||||
|
import type {
|
||||||
|
InstanceEvent,
|
||||||
|
IOnboardingStore,
|
||||||
|
ProjectEvent,
|
||||||
|
} from './onboarding-store-type';
|
||||||
|
import { millisecondsToSeconds } from 'date-fns';
|
||||||
|
|
||||||
|
export class OnboardingService {
|
||||||
|
private flagResolver: IFlagResolver;
|
||||||
|
|
||||||
|
private eventBus: EventEmitter;
|
||||||
|
|
||||||
|
private logger: Logger;
|
||||||
|
|
||||||
|
private onboardingStore: IOnboardingStore;
|
||||||
|
|
||||||
|
private projectReadModel: IProjectReadModel;
|
||||||
|
|
||||||
|
private userStore: IUserStore;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
{
|
||||||
|
onboardingStore,
|
||||||
|
projectReadModel,
|
||||||
|
userStore,
|
||||||
|
}: {
|
||||||
|
onboardingStore: IOnboardingStore;
|
||||||
|
projectReadModel: IProjectReadModel;
|
||||||
|
userStore: IUserStore;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
flagResolver,
|
||||||
|
eventBus,
|
||||||
|
getLogger,
|
||||||
|
}: Pick<IUnleashConfig, 'flagResolver' | 'eventBus' | 'getLogger'>,
|
||||||
|
) {
|
||||||
|
this.onboardingStore = onboardingStore;
|
||||||
|
this.projectReadModel = projectReadModel;
|
||||||
|
this.userStore = userStore;
|
||||||
|
this.flagResolver = flagResolver;
|
||||||
|
this.eventBus = eventBus;
|
||||||
|
this.logger = getLogger('onboarding/onboarding-service.ts');
|
||||||
|
}
|
||||||
|
|
||||||
|
listen() {
|
||||||
|
this.eventBus.on(USER_LOGIN, async (event: { loginOrder: number }) => {
|
||||||
|
if (!this.flagResolver.isEnabled('onboardingMetrics')) return;
|
||||||
|
|
||||||
|
if (event.loginOrder === 0) {
|
||||||
|
await this.insert({ type: 'first-user-login' });
|
||||||
|
}
|
||||||
|
if (event.loginOrder === 1) {
|
||||||
|
await this.insert({
|
||||||
|
type: 'second-user-login',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.eventBus.on(STAGE_ENTERED, async (stage: NewStage) => {
|
||||||
|
if (!this.flagResolver.isEnabled('onboardingMetrics')) return;
|
||||||
|
|
||||||
|
if (stage.stage === 'initial') {
|
||||||
|
await this.insert({
|
||||||
|
type: 'flag-created',
|
||||||
|
flag: stage.feature,
|
||||||
|
});
|
||||||
|
} else if (stage.stage === 'pre-live') {
|
||||||
|
await this.insert({
|
||||||
|
type: 'pre-live',
|
||||||
|
flag: stage.feature,
|
||||||
|
});
|
||||||
|
} else if (stage.stage === 'live') {
|
||||||
|
await this.insert({
|
||||||
|
type: 'live',
|
||||||
|
flag: stage.feature,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async insert(
|
||||||
|
event:
|
||||||
|
| { flag: string; type: ProjectEvent['type'] }
|
||||||
|
| { type: 'first-user-login' | 'second-user-login' },
|
||||||
|
): Promise<void> {
|
||||||
|
await this.insertInstanceEvent(event);
|
||||||
|
if ('flag' in event) {
|
||||||
|
await this.insertProjectEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async insertInstanceEvent(event: {
|
||||||
|
flag?: string;
|
||||||
|
type: InstanceEvent['type'];
|
||||||
|
}): Promise<void> {
|
||||||
|
const firstInstanceUserDate = await this.userStore.getFirstUserDate();
|
||||||
|
if (!firstInstanceUserDate) return;
|
||||||
|
|
||||||
|
const timeToEvent = millisecondsToSeconds(
|
||||||
|
new Date().getTime() - firstInstanceUserDate.getTime(),
|
||||||
|
);
|
||||||
|
await this.onboardingStore.insertInstanceEvent({
|
||||||
|
type: event.type,
|
||||||
|
timeToEvent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async insertProjectEvent(event: {
|
||||||
|
flag: string;
|
||||||
|
type: ProjectEvent['type'];
|
||||||
|
}): Promise<void> {
|
||||||
|
const project = await this.projectReadModel.getFeatureProject(
|
||||||
|
event.flag,
|
||||||
|
);
|
||||||
|
if (!project) return;
|
||||||
|
|
||||||
|
const timeToEvent = millisecondsToSeconds(
|
||||||
|
new Date().getTime() - project.createdAt.getTime(),
|
||||||
|
);
|
||||||
|
await this.onboardingStore.insertProjectEvent({
|
||||||
|
type: event.type,
|
||||||
|
timeToEvent,
|
||||||
|
project: project.project,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
16
src/lib/features/onboarding/onboarding-store-type.ts
Normal file
16
src/lib/features/onboarding/onboarding-store-type.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
export type ProjectEvent =
|
||||||
|
| { type: 'flag-created'; project: string; timeToEvent: number }
|
||||||
|
| { type: 'pre-live'; project: string; timeToEvent: number }
|
||||||
|
| { type: 'live'; project: string; timeToEvent: number };
|
||||||
|
export type InstanceEvent =
|
||||||
|
| { type: 'flag-created'; timeToEvent: number }
|
||||||
|
| { type: 'pre-live'; timeToEvent: number }
|
||||||
|
| { type: 'live'; timeToEvent: number }
|
||||||
|
| { type: 'first-user-login'; timeToEvent: number }
|
||||||
|
| { type: 'second-user-login'; timeToEvent: number };
|
||||||
|
|
||||||
|
export interface IOnboardingStore {
|
||||||
|
insertProjectEvent(event: ProjectEvent): Promise<void>;
|
||||||
|
|
||||||
|
insertInstanceEvent(event: InstanceEvent): Promise<void>;
|
||||||
|
}
|
72
src/lib/features/onboarding/onboarding-store.ts
Normal file
72
src/lib/features/onboarding/onboarding-store.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import type { Db } from '../../db/db';
|
||||||
|
import type {
|
||||||
|
InstanceEvent,
|
||||||
|
IOnboardingStore,
|
||||||
|
ProjectEvent,
|
||||||
|
} from './onboarding-store-type';
|
||||||
|
|
||||||
|
export type DBProjectEvent = {
|
||||||
|
event: 'first-flag' | 'first-pre-live' | 'first-live';
|
||||||
|
time_to_event: number;
|
||||||
|
project: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DBInstanceEvent =
|
||||||
|
| {
|
||||||
|
event: 'first-flag' | 'first-pre-live' | 'first-live';
|
||||||
|
time_to_event: number;
|
||||||
|
project?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
event: 'first-user-login' | 'second-user-login';
|
||||||
|
time_to_event: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const projectEventLookup: Record<
|
||||||
|
ProjectEvent['type'],
|
||||||
|
DBProjectEvent['event']
|
||||||
|
> = {
|
||||||
|
'flag-created': 'first-flag',
|
||||||
|
'pre-live': 'first-pre-live',
|
||||||
|
live: 'first-live',
|
||||||
|
};
|
||||||
|
|
||||||
|
const instanceEventLookup: Record<
|
||||||
|
InstanceEvent['type'],
|
||||||
|
DBInstanceEvent['event']
|
||||||
|
> = {
|
||||||
|
'flag-created': 'first-flag',
|
||||||
|
'pre-live': 'first-pre-live',
|
||||||
|
live: 'first-live',
|
||||||
|
'first-user-login': 'first-user-login',
|
||||||
|
'second-user-login': 'second-user-login',
|
||||||
|
};
|
||||||
|
|
||||||
|
export class OnboardingStore implements IOnboardingStore {
|
||||||
|
private db: Db;
|
||||||
|
|
||||||
|
constructor(db: Db) {
|
||||||
|
this.db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
async insertInstanceEvent(event: InstanceEvent): Promise<void> {
|
||||||
|
await this.db('onboarding_events_instance')
|
||||||
|
.insert({
|
||||||
|
event: instanceEventLookup[event.type],
|
||||||
|
time_to_event: event.timeToEvent,
|
||||||
|
})
|
||||||
|
.onConflict()
|
||||||
|
.ignore();
|
||||||
|
}
|
||||||
|
|
||||||
|
async insertProjectEvent(event: ProjectEvent): Promise<void> {
|
||||||
|
await this.db<DBProjectEvent>('onboarding_events_project')
|
||||||
|
.insert({
|
||||||
|
event: projectEventLookup[event.type],
|
||||||
|
time_to_event: event.timeToEvent,
|
||||||
|
project: event.project,
|
||||||
|
})
|
||||||
|
.onConflict()
|
||||||
|
.ignore();
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,9 @@ import type {
|
|||||||
} from './project-read-model-type';
|
} from './project-read-model-type';
|
||||||
|
|
||||||
export class FakeProjectReadModel implements IProjectReadModel {
|
export class FakeProjectReadModel implements IProjectReadModel {
|
||||||
|
getFeatureProject(): Promise<{ project: string; createdAt: Date } | null> {
|
||||||
|
return Promise.resolve(null);
|
||||||
|
}
|
||||||
getProjectsForAdminUi(): Promise<ProjectForUi[]> {
|
getProjectsForAdminUi(): Promise<ProjectForUi[]> {
|
||||||
return Promise.resolve([]);
|
return Promise.resolve([]);
|
||||||
}
|
}
|
||||||
|
@ -37,4 +37,7 @@ export interface IProjectReadModel {
|
|||||||
getProjectsForInsights(
|
getProjectsForInsights(
|
||||||
query?: IProjectQuery,
|
query?: IProjectQuery,
|
||||||
): Promise<ProjectForInsights[]>;
|
): Promise<ProjectForInsights[]>;
|
||||||
|
getFeatureProject(
|
||||||
|
featureName: string,
|
||||||
|
): Promise<{ project: string; createdAt: Date } | null>;
|
||||||
}
|
}
|
||||||
|
@ -62,6 +62,22 @@ export class ProjectReadModel implements IProjectReadModel {
|
|||||||
this.flagResolver = flagResolver;
|
this.flagResolver = flagResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getFeatureProject(
|
||||||
|
featureName: string,
|
||||||
|
): Promise<{ project: string; createdAt: Date } | null> {
|
||||||
|
const result = await this.db<{ project: string; created_at: Date }>(
|
||||||
|
'features',
|
||||||
|
)
|
||||||
|
.join('projects', 'features.project', '=', 'projects.id')
|
||||||
|
.select('features.project', 'projects.created_at')
|
||||||
|
.where('features.name', featureName)
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (!result) return null;
|
||||||
|
|
||||||
|
return { project: result.project, createdAt: result.created_at };
|
||||||
|
}
|
||||||
|
|
||||||
async getProjectsForAdminUi(
|
async getProjectsForAdminUi(
|
||||||
query?: IProjectQuery,
|
query?: IProjectQuery,
|
||||||
userId?: number,
|
userId?: number,
|
||||||
|
@ -302,7 +302,7 @@ class ProjectStore implements IProjectStore {
|
|||||||
project: IProjectInsert & IProjectSettings,
|
project: IProjectInsert & IProjectSettings,
|
||||||
): Promise<IProject> {
|
): Promise<IProject> {
|
||||||
const row = await this.db(TABLE)
|
const row = await this.db(TABLE)
|
||||||
.insert(this.fieldToRow(project))
|
.insert({ ...this.fieldToRow(project), created_at: new Date() })
|
||||||
.returning('*');
|
.returning('*');
|
||||||
const settingsRow = await this.db(SETTINGS_TABLE)
|
const settingsRow = await this.db(SETTINGS_TABLE)
|
||||||
.insert({
|
.insert({
|
||||||
|
@ -50,6 +50,7 @@ import type { IntegrationEventsStore } from '../features/integration-events/inte
|
|||||||
import { IFeatureCollaboratorsReadModel } from '../features/feature-toggle/types/feature-collaborators-read-model-type';
|
import { IFeatureCollaboratorsReadModel } from '../features/feature-toggle/types/feature-collaborators-read-model-type';
|
||||||
import type { IProjectReadModel } from '../features/project/project-read-model-type';
|
import type { IProjectReadModel } from '../features/project/project-read-model-type';
|
||||||
import { IOnboardingReadModel } from '../features/onboarding/onboarding-read-model-type';
|
import { IOnboardingReadModel } from '../features/onboarding/onboarding-read-model-type';
|
||||||
|
import { IOnboardingStore } from '../features/onboarding/onboarding-store-type';
|
||||||
|
|
||||||
export interface IUnleashStores {
|
export interface IUnleashStores {
|
||||||
accessStore: IAccessStore;
|
accessStore: IAccessStore;
|
||||||
@ -104,6 +105,7 @@ export interface IUnleashStores {
|
|||||||
featureCollaboratorsReadModel: IFeatureCollaboratorsReadModel;
|
featureCollaboratorsReadModel: IFeatureCollaboratorsReadModel;
|
||||||
projectReadModel: IProjectReadModel;
|
projectReadModel: IProjectReadModel;
|
||||||
onboardingReadModel: IOnboardingReadModel;
|
onboardingReadModel: IOnboardingReadModel;
|
||||||
|
onboardingStore: IOnboardingStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@ -157,4 +159,5 @@ export {
|
|||||||
IOnboardingReadModel,
|
IOnboardingReadModel,
|
||||||
type IntegrationEventsStore,
|
type IntegrationEventsStore,
|
||||||
type IProjectReadModel,
|
type IProjectReadModel,
|
||||||
|
IOnboardingStore,
|
||||||
};
|
};
|
||||||
|
@ -34,6 +34,7 @@ export interface IUserStore extends Store<IUser, number> {
|
|||||||
disallowNPreviousPasswords: number,
|
disallowNPreviousPasswords: number,
|
||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
getPasswordsPreviouslyUsed(userId: number): Promise<string[]>;
|
getPasswordsPreviouslyUsed(userId: number): Promise<string[]>;
|
||||||
|
getFirstUserDate(): Promise<Date | null>;
|
||||||
incLoginAttempts(user: IUser): Promise<void>;
|
incLoginAttempts(user: IUser): Promise<void>;
|
||||||
successfullyLogin(user: IUser): Promise<number>;
|
successfullyLogin(user: IUser): Promise<number>;
|
||||||
count(): Promise<number>;
|
count(): Promise<number>;
|
||||||
|
16
src/test/fixtures/fake-user-store.ts
vendored
16
src/test/fixtures/fake-user-store.ts
vendored
@ -17,6 +17,22 @@ class UserStoreMock implements IUserStore {
|
|||||||
this.previousPasswords = new Map();
|
this.previousPasswords = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getFirstUserDate(): Promise<Date | null> {
|
||||||
|
if (this.data.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const oldestUser = this.data.reduce((oldest, user) => {
|
||||||
|
if (!user.createdAt) {
|
||||||
|
return oldest;
|
||||||
|
}
|
||||||
|
return !oldest.createdAt || user.createdAt < oldest.createdAt
|
||||||
|
? user
|
||||||
|
: oldest;
|
||||||
|
}, this.data[0]);
|
||||||
|
|
||||||
|
return oldestUser.createdAt || null;
|
||||||
|
}
|
||||||
|
|
||||||
getPasswordsPreviouslyUsed(userId: number): Promise<string[]> {
|
getPasswordsPreviouslyUsed(userId: number): Promise<string[]> {
|
||||||
return Promise.resolve(this.previousPasswords.get(userId) || []);
|
return Promise.resolve(this.previousPasswords.get(userId) || []);
|
||||||
}
|
}
|
||||||
|
2
src/test/fixtures/store.ts
vendored
2
src/test/fixtures/store.ts
vendored
@ -53,6 +53,7 @@ import { FakeLargestResourcesReadModel } from '../../lib/features/metrics/sizes/
|
|||||||
import { FakeFeatureCollaboratorsReadModel } from '../../lib/features/feature-toggle/fake-feature-collaborators-read-model';
|
import { FakeFeatureCollaboratorsReadModel } from '../../lib/features/feature-toggle/fake-feature-collaborators-read-model';
|
||||||
import { createFakeProjectReadModel } from '../../lib/features/project/createProjectReadModel';
|
import { createFakeProjectReadModel } from '../../lib/features/project/createProjectReadModel';
|
||||||
import { FakeOnboardingReadModel } from '../../lib/features/onboarding/fake-onboarding-read-model';
|
import { FakeOnboardingReadModel } from '../../lib/features/onboarding/fake-onboarding-read-model';
|
||||||
|
import { FakeOnboardingStore } from '../../lib/features/onboarding/fake-onboarding-store';
|
||||||
|
|
||||||
const db = {
|
const db = {
|
||||||
select: () => ({
|
select: () => ({
|
||||||
@ -115,6 +116,7 @@ const createStores: () => IUnleashStores = () => {
|
|||||||
integrationEventsStore: {} as IntegrationEventsStore,
|
integrationEventsStore: {} as IntegrationEventsStore,
|
||||||
featureCollaboratorsReadModel: new FakeFeatureCollaboratorsReadModel(),
|
featureCollaboratorsReadModel: new FakeFeatureCollaboratorsReadModel(),
|
||||||
projectReadModel: createFakeProjectReadModel(),
|
projectReadModel: createFakeProjectReadModel(),
|
||||||
|
onboardingStore: new FakeOnboardingStore(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user