Принципы тестирования доступов
Функционал доступов обязательно должен быть покрыт тестами.
Алгоритм покрытия Policy тестами
Пример policy:
export class BooksPolicyStore {
private readonly policy: PermissionsPolicy;
constructor(
policyManager: PolicyManagerStore,
private readonly billingRepo: BillingRepository,
private readonly userRepo: UserRepository,
) {
makeAutoObservable(this, {}, { autoBind: true });
this.policy = policyManager.createPolicy({
name: 'books',
prepareData: async () => {
await Promise.all([
this.userRepo.getRolesQuery().async(),
this.billingRepo.getBillingInfoQuery().async(),
]);
},
});
}
/**
* Возможность добавить на полку книгу
*/
public get addingToShelf() {
return this.policy.createPermission((allow, deny) => {
if (this.userRepo.getRolesQuery().data?.isAdmin) {
return allow();
}
const billingInfo = this.billingRepo.getBillingInfoQuery()?.data;
if (!billingInfo?.paid) {
return deny(PermissionDenialReason.NoPayAccount);
}
if (
billingInfo.info.shelf.currentCount >=
billingInfo.info.shelf.allowedCount
) {
return deny(PermissionDenialReason.ExceedShelfCount);
}
allow();
});
}
}
На каждый permission, определенный в policy, необходимо писать тесты.
Для каждого permission необходимо создавать отдельный describe
describe('AdministrationPolicyStore', () => {
describe('Добавление книги на полку', () => {});
});
Для каждого permission необходимо обработать положительные и отрицательные кейсы
Формирование кейсов происходит в соответствии с вызовом allow и deny в коде:
public get addingToShelf() {
return this.policy.createPermission((allow, deny) => {
// Тест-кейс: Доступно администратору
if (this.userRepo.getRolesQuery().data?.isAdmin) {
return allow();
}
const billingInfo = this.billingRepo.getBillingInfoQuery()?.data;
// Тест-кейс: Недоступно, если аккаунт не оплачен
if (!billingInfo?.paid) {
return deny(PermissionDenialReason.NoPayAccount);
}
// Тест-кейс: Недоступно, если превышено количество добавлений
if (
billingInfo.info.shelf.currentCount >=
billingInfo.info.shelf.allowedCount
) {
return deny(PermissionDenialReason.ExceedShelfCount);
}
// Тест-кейс: Доступно, если аккаунт оплачен и не превышено максимальное количество книг на полке
allow();
});
}
Реализуемые тест-кейсы:
describe('BooksPolicyStore', () => {
describe('Добавление книги на полку', () => {
it('Доступно администратору', async () => {
const { sut } = await setup({ isAdmin: true });
expect(sut.addingToShelf.isAllowed).toBeTruthy();
});
it('Недоступно, если аккаунт не оплачен', async () => {});
it('Недоступно, если превышено количество добавлений', async () => {});
it('Недоступно, если достигнуто максимальное количество добавлений', async () => {});
it('Доступно, если аккаунт оплачен и не превышено максимальное количество книг на полке', async () => {});
});
});
Перед началом выполнения теста необходимо всегда вызывать prepareData
PolicyManagerStore
поддерживает асинхронный вызов prepareData - prepareDataAsync
.
describe('BooksPolicyStore', () => {
const setup = async ({
isAdmin,
billingInfo,
}: {
isAdmin: boolean;
billingInfo?: Partial<BillingRepositoryDTO.BillingInfo>;
}) => {
const policyManager = createPolicyManagerStore();
const cacheService = createCacheService();
const userRepoMock = mock<UserRepository>({
getRolesQuery: () =>
cacheService.createQuery(['roles'], async () => ({
isAdmin,
})),
});
const billingRepoMock = mock<BillingRepository>({
getBillingInfoQuery: () =>
cacheService.createQuery(['billing'], async () =>
billingRepositoryFaker.makeBillingInfo(billingInfo),
),
});
const sut = new BooksPolicyStore(
policyManager,
billingRepoMock,
userRepoMock,
);
await policyManager.prepareDataAsync();
return { sut };
};
describe('Добавление книги на полку', () => {
it('Доступно администратору', async () => {
const { sut } = await setup({ isAdmin: true });
expect(sut.addingToShelf.isAllowed).toBeTruthy();
});
});
});
Если не вызвать prepareData, то все доступы будут недоступны.
При тестировании отказа в доступе, необходимо проверять reason
Тест-кейс Недоступно, если аккаунт не оплачен
должен считаться пройденным только если reason соответствует PermissionDenialReason.NoPayAccount
:
it('Недоступно, если аккаунт не оплачен', async () => {
const { sut } = await setup({
isAdmin: false,
billingInfo: { paid: false },
});
expect(sut.addingToShelf.isAllowed).toBeFalsy();
expect(sut.addingToShelf.reason).toBe(
PermissionDenialReason.NoPayAccount,
);
});