mirror of
https://github.com/kamask-dev/pchelpmaster.git
synced 2026-11-11 05:49:38 +00:00
test(auth): add failing tests for auth service and pb_auth cookie
Tests first for task 2.2; the implementation comes separately. - AuthService contract (auth/testing/auth-service.contract.ts), run on the in-memory fake (unit) and on PocketBaseAuthService against a test PocketBase (integration) - integration: register persists the user, login maps id/email/name, restore with a valid, broken or missing token, signals follow the auth store, two users in separate injectors, NetworkError and kept session when PocketBase is unreachable - unit (jsdom): pb_auth cookie is written on sign-in and removed on sign-out with Path=/ and SameSite=Lax, loaded into the auth store before restore() at startup, untouched on the server platform - contract stubs that throw "Not implemented": User, AuthService, PocketBaseAuthService, provideAuth, InMemoryAuthService (signInAs, failNextWith) - docs/testing.md: note on the AuthService contract Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
115b5fb036
commit
74d3c660f9
@@ -76,6 +76,8 @@ export function describeTagRepositoryContract(
|
||||
|
||||
и вызывается дважды: в `in-memory-tag-repository.spec.ts` (быстро, в `pnpm test`) и в `pocketbase-tag-repository.integration.spec.ts` (на тестовом PocketBase, в `pnpm test:integration`).
|
||||
|
||||
Так же устроен контракт сервиса `AuthService`: `auth/testing/auth-service.contract.ts`. Файл контракта не `*.spec.ts`, поэтому `describe`/`it`/`expect` импортируются из `vitest` явно.
|
||||
|
||||
## Тестовый PocketBase
|
||||
|
||||
API-, integration- и e2e-тесты работают с **отдельным временным экземпляром** PocketBase, а не с `pocketbase/pb_data`:
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { ApplicationInitStatus, PLATFORM_ID, type Provider } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { BaseAuthStore, type RecordModel } from 'pocketbase';
|
||||
import { PB_URL } from '../../shared/pocketbase/pb-url';
|
||||
import { POCKETBASE, providePocketBase } from '../../shared/pocketbase/pocketbase-client';
|
||||
import { provideAuth } from '../provide-auth';
|
||||
import { PocketBaseAuthService } from './pocketbase-auth-service';
|
||||
|
||||
const record: RecordModel = {
|
||||
id: 'u1',
|
||||
collectionId: '_pb_users_auth_',
|
||||
collectionName: 'users',
|
||||
email: 'user@test.local',
|
||||
name: 'Иван',
|
||||
};
|
||||
|
||||
/** JWT без настоящей подписи, но с exp через час: по exp SDK считает токен живым и ставит срок cookie. */
|
||||
function fakeJwt(): string {
|
||||
const encode = (part: object) =>
|
||||
btoa(JSON.stringify(part)).replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_');
|
||||
const exp = Math.floor(Date.now() / 1000) + 3600;
|
||||
return `${encode({ alg: 'HS256', typ: 'JWT' })}.${encode({ id: record.id, type: 'auth', exp })}.signature`;
|
||||
}
|
||||
|
||||
function hasAuthCookie(): boolean {
|
||||
return document.cookie.split('; ').some((cookie) => cookie.startsWith('pb_auth='));
|
||||
}
|
||||
|
||||
/** Кладёт cookie сессии так, как её оставил бы прошлый визит. */
|
||||
function putAuthCookie(token: string): void {
|
||||
const store = new BaseAuthStore();
|
||||
store.save(token, record);
|
||||
document.cookie = store.exportToCookie({ httpOnly: false, secure: false });
|
||||
}
|
||||
|
||||
/** Поднимает приложение с provideAuth() и дожидается инициализаторов; сети нет. */
|
||||
async function startApp(providers: Provider[] = []) {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
providePocketBase(),
|
||||
{ provide: PB_URL, useValue: 'http://pb.test:1234' },
|
||||
provideAuth(),
|
||||
...providers,
|
||||
],
|
||||
});
|
||||
await TestBed.inject(ApplicationInitStatus).donePromise;
|
||||
return TestBed.inject(POCKETBASE);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
document.cookie = 'pb_auth=; Path=/; Max-Age=0';
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('cookie pb_auth в браузере', () => {
|
||||
it('после входа cookie содержит сессию из auth store', async () => {
|
||||
const pb = await startApp();
|
||||
const token = fakeJwt();
|
||||
|
||||
pb.authStore.save(token, record);
|
||||
|
||||
expect(hasAuthCookie()).toBe(true);
|
||||
const restored = new BaseAuthStore();
|
||||
restored.loadFromCookie(document.cookie);
|
||||
expect(restored.token).toBe(token);
|
||||
expect(restored.record?.id).toBe(record.id);
|
||||
});
|
||||
|
||||
it('после выхода cookie нет', async () => {
|
||||
const pb = await startApp();
|
||||
pb.authStore.save(fakeJwt(), record);
|
||||
|
||||
pb.authStore.clear();
|
||||
|
||||
expect(hasAuthCookie()).toBe(false);
|
||||
});
|
||||
|
||||
it('пишется с Path=/ и SameSite=Lax, без HttpOnly и без Secure на http', async () => {
|
||||
const pb = await startApp();
|
||||
const writes = vi.spyOn(document, 'cookie', 'set');
|
||||
|
||||
pb.authStore.save(fakeJwt(), record);
|
||||
|
||||
const written = writes.mock.calls.map(([value]) => String(value));
|
||||
const authCookie = written.find((value) => value.startsWith('pb_auth='));
|
||||
expect(authCookie).toBeDefined();
|
||||
expect(authCookie).toMatch(/;\s*Path=\/(;|$)/i);
|
||||
expect(authCookie).toMatch(/;\s*SameSite=Lax(;|$)/i);
|
||||
expect(authCookie).not.toMatch(/;\s*HttpOnly/i);
|
||||
expect(authCookie).not.toMatch(/;\s*Secure/i);
|
||||
});
|
||||
|
||||
it('при старте токен из cookie попадает в auth store до вызова restore()', async () => {
|
||||
const token = fakeJwt();
|
||||
putAuthCookie(token);
|
||||
let tokenAtRestore: string | undefined;
|
||||
const restore = vi
|
||||
.spyOn(PocketBaseAuthService.prototype, 'restore')
|
||||
.mockImplementation(async () => {
|
||||
tokenAtRestore = TestBed.inject(POCKETBASE).authStore.token;
|
||||
});
|
||||
|
||||
const pb = await startApp();
|
||||
|
||||
expect(pb.authStore.token).toBe(token);
|
||||
expect(pb.authStore.record?.id).toBe(record.id);
|
||||
expect(restore).toHaveBeenCalledTimes(1);
|
||||
expect(tokenAtRestore).toBe(token);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cookie pb_auth на сервере', () => {
|
||||
it('не читается при старте и restore() не вызывается', async () => {
|
||||
putAuthCookie(fakeJwt());
|
||||
const restore = vi
|
||||
.spyOn(PocketBaseAuthService.prototype, 'restore')
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
const pb = await startApp([{ provide: PLATFORM_ID, useValue: 'server' }]);
|
||||
|
||||
expect(pb.authStore.token).toBe('');
|
||||
expect(restore).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('не пишется при изменении auth store', async () => {
|
||||
const pb = await startApp([{ provide: PLATFORM_ID, useValue: 'server' }]);
|
||||
|
||||
pb.authStore.save(fakeJwt(), record);
|
||||
|
||||
expect(hasAuthCookie()).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,153 @@
|
||||
import { EnvironmentInjector, createEnvironmentInjector } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import type PocketBase from 'pocketbase';
|
||||
import { inject } from 'vitest';
|
||||
import { asSuperuser, asUser, createUser } from '../../../../tests/support/clients';
|
||||
import { NetworkError } from '../../shared/errors/domain-errors';
|
||||
import { PB_URL } from '../../shared/pocketbase/pb-url';
|
||||
import { POCKETBASE, providePocketBase } from '../../shared/pocketbase/pocketbase-client';
|
||||
import type { AuthService } from '../domain/auth-service';
|
||||
import { describeAuthServiceContract } from '../testing/auth-service.contract';
|
||||
import { PocketBaseAuthService } from './pocketbase-auth-service';
|
||||
|
||||
// Сервис из отдельного инжектора приложения, как у отдельного SSR-запроса или вкладки.
|
||||
function createService(pbUrl: string = inject('pbUrl')): { auth: AuthService; pb: PocketBase } {
|
||||
const injector = createEnvironmentInjector(
|
||||
[providePocketBase(), { provide: PB_URL, useValue: pbUrl }, PocketBaseAuthService],
|
||||
TestBed.inject(EnvironmentInjector),
|
||||
);
|
||||
return { auth: injector.get(PocketBaseAuthService), pb: injector.get(POCKETBASE) };
|
||||
}
|
||||
|
||||
/** Токен и запись настоящего входа, как их кладёт в auth store cookie. */
|
||||
async function sessionOf(user: Awaited<ReturnType<typeof createUser>>) {
|
||||
const { authStore } = await asUser(user);
|
||||
return { token: authStore.token, record: authStore.record };
|
||||
}
|
||||
|
||||
/** JWT с той же шапкой и данными, но чужой подписью: локально выглядит валидным, сервер отвергает. */
|
||||
function withBrokenSignature(token: string): string {
|
||||
const [header, payload, signature] = token.split('.');
|
||||
return `${header}.${payload}.${'x'.repeat(signature.length)}`;
|
||||
}
|
||||
|
||||
describeAuthServiceContract(() => createService().auth);
|
||||
|
||||
describe('PocketBaseAuthService против тестового PocketBase', () => {
|
||||
it('register сохраняет пользователя в users', async () => {
|
||||
const { auth } = createService();
|
||||
const email = `user-${crypto.randomUUID()}@test.local`;
|
||||
|
||||
const user = await auth.register({ email, password: crypto.randomUUID(), name: 'Иван' });
|
||||
|
||||
const record = await (
|
||||
await asSuperuser()
|
||||
)
|
||||
.collection('users')
|
||||
.getFirstListItem(`email = "${email}"`);
|
||||
expect(record.id).toBe(user.id);
|
||||
expect(record['name']).toBe('Иван');
|
||||
});
|
||||
|
||||
it('login пользователя, созданного в PocketBase, отдаёт id, email и имя', async () => {
|
||||
const created = await createUser({ name: 'Иван' });
|
||||
const { auth } = createService();
|
||||
|
||||
await auth.login(created.email, created.password);
|
||||
|
||||
expect(auth.user()).toEqual({ id: created.id, email: created.email, name: 'Иван' });
|
||||
});
|
||||
|
||||
it('restore с валидным токеном обновляет токен и данные пользователя', async () => {
|
||||
const created = await createUser({ name: 'Иван' });
|
||||
const { token, record } = await sessionOf(created);
|
||||
const { auth, pb } = createService();
|
||||
pb.authStore.save(token, record);
|
||||
await (await asSuperuser()).collection('users').update(created.id, { name: 'Пётр' });
|
||||
// JWT выпущенный в ту же секунду совпадает с прежним: ждём смены секунды в exp.
|
||||
await new Promise((resolve) => setTimeout(resolve, 1100));
|
||||
|
||||
await auth.restore();
|
||||
|
||||
expect(auth.user()).toEqual({ id: created.id, email: created.email, name: 'Пётр' });
|
||||
expect(auth.isAuthenticated()).toBe(true);
|
||||
expect(pb.authStore.token).not.toBe('');
|
||||
expect(pb.authStore.token).not.toBe(token);
|
||||
});
|
||||
|
||||
it('restore с испорченным токеном завершает сессию', async () => {
|
||||
const { token, record } = await sessionOf(await createUser());
|
||||
const { auth, pb } = createService();
|
||||
pb.authStore.save(withBrokenSignature(token), record);
|
||||
expect(auth.user()).not.toBeNull();
|
||||
|
||||
await auth.restore();
|
||||
|
||||
expect(auth.user()).toBeNull();
|
||||
expect(auth.isAuthenticated()).toBe(false);
|
||||
expect(pb.authStore.token).toBe('');
|
||||
});
|
||||
|
||||
it('restore без токена ничего не делает', async () => {
|
||||
const { auth } = createService();
|
||||
|
||||
await expect(auth.restore()).resolves.toBeUndefined();
|
||||
|
||||
expect(auth.user()).toBeNull();
|
||||
});
|
||||
|
||||
it('user() следует за любым изменением auth store', async () => {
|
||||
const created = await createUser();
|
||||
const { auth, pb } = createService();
|
||||
await auth.login(created.email, created.password);
|
||||
|
||||
pb.authStore.clear();
|
||||
|
||||
expect(auth.user()).toBeNull();
|
||||
expect(auth.isAuthenticated()).toBe(false);
|
||||
});
|
||||
|
||||
it('сессии двух пользователей в разных инжекторах независимы', async () => {
|
||||
const [userA, userB] = await Promise.all([createUser(), createUser()]);
|
||||
const a = createService().auth;
|
||||
const b = createService().auth;
|
||||
|
||||
await Promise.all([
|
||||
a.login(userA.email, userA.password),
|
||||
b.login(userB.email, userB.password),
|
||||
]);
|
||||
|
||||
expect(a.user()?.id).toBe(userA.id);
|
||||
expect(b.user()?.id).toBe(userB.id);
|
||||
|
||||
a.logout();
|
||||
|
||||
expect(a.user()).toBeNull();
|
||||
expect(b.user()?.id).toBe(userB.id);
|
||||
});
|
||||
|
||||
describe('PocketBase недоступен', () => {
|
||||
const unreachableUrl = 'http://127.0.0.1:1';
|
||||
|
||||
it('login → NetworkError', async () => {
|
||||
const { auth } = createService(unreachableUrl);
|
||||
|
||||
await expect(auth.login('user@test.local', 'password-1')).rejects.toBeInstanceOf(
|
||||
NetworkError,
|
||||
);
|
||||
expect(auth.user()).toBeNull();
|
||||
});
|
||||
|
||||
it('restore сохраняет сессию', async () => {
|
||||
const created = await createUser();
|
||||
const { token, record } = await sessionOf(created);
|
||||
const { auth, pb } = createService(unreachableUrl);
|
||||
pb.authStore.save(token, record);
|
||||
|
||||
await expect(auth.restore()).resolves.toBeUndefined();
|
||||
|
||||
expect(auth.user()?.id).toBe(created.id);
|
||||
expect(pb.authStore.token).toBe(token);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Service, computed } from '@angular/core';
|
||||
import type { User } from '../../users';
|
||||
import { AuthService } from '../domain/auth-service';
|
||||
|
||||
// Заглушка контракта для тестов; реализация — задача 2.2.
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars -- параметры заглушки понадобятся реализации */
|
||||
// Регистрируется только через provideAuth() под токеном AuthService: авторегистрация дала бы второй экземпляр.
|
||||
@Service({ autoProvided: false })
|
||||
export class PocketBaseAuthService extends AuthService {
|
||||
readonly user = computed<User | null>(() => {
|
||||
throw new Error('Not implemented');
|
||||
});
|
||||
|
||||
readonly isAuthenticated = computed<boolean>(() => {
|
||||
throw new Error('Not implemented');
|
||||
});
|
||||
|
||||
login(_email: string, _password: string): Promise<User> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
register(_data: { email: string; password: string; name?: string }): Promise<User> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
logout(): void {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
restore(): Promise<void> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { Signal } from '@angular/core';
|
||||
import type { User } from '../../users';
|
||||
|
||||
/** Сессия пользователя: вход, регистрация, выход, текущий пользователь. */
|
||||
export abstract class AuthService {
|
||||
abstract readonly user: Signal<User | null>;
|
||||
abstract readonly isAuthenticated: Signal<boolean>;
|
||||
abstract login(email: string, password: string): Promise<User>;
|
||||
abstract register(data: { email: string; password: string; name?: string }): Promise<User>;
|
||||
abstract logout(): void;
|
||||
abstract restore(): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { EnvironmentProviders } from '@angular/core';
|
||||
|
||||
// Заглушка контракта для тестов; реализация — задача 2.2.
|
||||
export function provideAuth(): EnvironmentProviders {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
// Контракт AuthService: одни и те же кейсы для PocketBase-реализации и фейка в памяти.
|
||||
// Явный импорт из vitest: файл не *.spec.ts и попадает в программу tsconfig.app.json без глобалов vitest.
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { InvalidCredentialsError, ValidationError } from '../../shared/errors/domain-errors';
|
||||
import type { AuthService } from '../domain/auth-service';
|
||||
|
||||
function newCredentials(): { email: string; password: string } {
|
||||
return { email: `user-${crypto.randomUUID()}@test.local`, password: crypto.randomUUID() };
|
||||
}
|
||||
|
||||
/** `setup` возвращает новый экземпляр сервиса без сессии. */
|
||||
export function describeAuthServiceContract(setup: () => AuthService): void {
|
||||
describe('контракт AuthService', () => {
|
||||
it('до входа пользователя нет', () => {
|
||||
const auth = setup();
|
||||
|
||||
expect(auth.user()).toBeNull();
|
||||
expect(auth.isAuthenticated()).toBe(false);
|
||||
});
|
||||
|
||||
it('register создаёт пользователя и сразу входит', async () => {
|
||||
const auth = setup();
|
||||
const credentials = newCredentials();
|
||||
|
||||
const user = await auth.register({ ...credentials, name: 'Иван' });
|
||||
|
||||
expect(user).toEqual({
|
||||
id: expect.any(String),
|
||||
email: credentials.email,
|
||||
name: 'Иван',
|
||||
});
|
||||
expect(auth.user()).toEqual(user);
|
||||
expect(auth.isAuthenticated()).toBe(true);
|
||||
});
|
||||
|
||||
it('register без имени — пустое имя', async () => {
|
||||
const auth = setup();
|
||||
|
||||
const user = await auth.register(newCredentials());
|
||||
|
||||
expect(user.name).toBe('');
|
||||
});
|
||||
|
||||
it('register с занятым email → ValidationError с ошибкой в поле email', async () => {
|
||||
const auth = setup();
|
||||
const credentials = newCredentials();
|
||||
await auth.register(credentials);
|
||||
auth.logout();
|
||||
|
||||
const error = await auth
|
||||
.register({ email: credentials.email, password: crypto.randomUUID() })
|
||||
.catch((e: unknown) => e);
|
||||
|
||||
expect(error).toBeInstanceOf(ValidationError);
|
||||
expect((error as ValidationError).fieldErrors).toHaveProperty('email');
|
||||
});
|
||||
|
||||
it('login с верным паролем входит и отдаёт id, email и имя', async () => {
|
||||
const auth = setup();
|
||||
const credentials = newCredentials();
|
||||
const registered = await auth.register({ ...credentials, name: 'Иван' });
|
||||
auth.logout();
|
||||
|
||||
const user = await auth.login(credentials.email, credentials.password);
|
||||
|
||||
expect(user).toEqual(registered);
|
||||
expect(auth.user()).toEqual({
|
||||
id: registered.id,
|
||||
email: credentials.email,
|
||||
name: 'Иван',
|
||||
});
|
||||
expect(auth.isAuthenticated()).toBe(true);
|
||||
});
|
||||
|
||||
it('login с неверным паролем → InvalidCredentialsError, пользователя нет', async () => {
|
||||
const auth = setup();
|
||||
const credentials = newCredentials();
|
||||
await auth.register(credentials);
|
||||
auth.logout();
|
||||
|
||||
await expect(auth.login(credentials.email, 'wrong-password')).rejects.toBeInstanceOf(
|
||||
InvalidCredentialsError,
|
||||
);
|
||||
expect(auth.user()).toBeNull();
|
||||
expect(auth.isAuthenticated()).toBe(false);
|
||||
});
|
||||
|
||||
it('login с несуществующим email → InvalidCredentialsError', async () => {
|
||||
const auth = setup();
|
||||
const { email, password } = newCredentials();
|
||||
|
||||
await expect(auth.login(email, password)).rejects.toBeInstanceOf(
|
||||
InvalidCredentialsError,
|
||||
);
|
||||
});
|
||||
|
||||
it('logout завершает сессию', async () => {
|
||||
const auth = setup();
|
||||
await auth.register(newCredentials());
|
||||
|
||||
auth.logout();
|
||||
|
||||
expect(auth.user()).toBeNull();
|
||||
expect(auth.isAuthenticated()).toBe(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { NetworkError } from '../../shared/errors/domain-errors';
|
||||
import { describeAuthServiceContract } from './auth-service.contract';
|
||||
import { InMemoryAuthService } from './in-memory-auth-service';
|
||||
|
||||
describeAuthServiceContract(() => new InMemoryAuthService());
|
||||
|
||||
describe('InMemoryAuthService', () => {
|
||||
it('signInAs делает пользователя вошедшим', () => {
|
||||
const auth = new InMemoryAuthService();
|
||||
const user = { id: 'u1', email: 'user@test.local', name: 'Иван' };
|
||||
|
||||
auth.signInAs(user);
|
||||
|
||||
expect(auth.user()).toEqual(user);
|
||||
expect(auth.isAuthenticated()).toBe(true);
|
||||
});
|
||||
|
||||
it('failNextWith отклоняет только следующий вызов', async () => {
|
||||
const auth = new InMemoryAuthService();
|
||||
const credentials = { email: 'user@test.local', password: 'password-1' };
|
||||
await auth.register(credentials);
|
||||
auth.logout();
|
||||
const error = new NetworkError();
|
||||
|
||||
auth.failNextWith(error);
|
||||
|
||||
await expect(auth.login(credentials.email, credentials.password)).rejects.toBe(error);
|
||||
expect(auth.user()).toBeNull();
|
||||
await expect(auth.login(credentials.email, credentials.password)).resolves.toMatchObject({
|
||||
email: credentials.email,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { computed } from '@angular/core';
|
||||
import type { User } from '../../users';
|
||||
import { AuthService } from '../domain/auth-service';
|
||||
|
||||
// Заглушка контракта для тестов; реализация — задача 2.2.
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars -- параметры заглушки понадобятся реализации */
|
||||
/** Фейк `AuthService` в памяти для тестов других слоёв и фич. */
|
||||
export class InMemoryAuthService extends AuthService {
|
||||
readonly user = computed<User | null>(() => {
|
||||
throw new Error('Not implemented');
|
||||
});
|
||||
|
||||
readonly isAuthenticated = computed<boolean>(() => {
|
||||
throw new Error('Not implemented');
|
||||
});
|
||||
|
||||
login(_email: string, _password: string): Promise<User> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
register(_data: { email: string; password: string; name?: string }): Promise<User> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
logout(): void {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
restore(): Promise<void> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
/** Сразу делает пользователя вошедшим, без регистрации. */
|
||||
signInAs(_user: User): void {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
/** Следующий вызов `login`, `register` или `restore` отклоняется этой ошибкой. */
|
||||
failNextWith(_error: Error): void {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/** Владелец аккаунта. */
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export type { User } from './domain/user';
|
||||
Reference in New Issue
Block a user