Foundry expectRevert: проверка отката и ошибки
В предыдущем уроке издатель получил возможность выполнить публикацию в учебной среде. Теперь проверим обратную сторону договора: читатель не вправе менять реестр. Сам по себе любой отказ ещё не доказывает правильность полномочий. Нужно отличить ожидаемую ошибку доступа от неверных данных и случайного сбоя подготовки.
Результатом урока станет отрицательный сценарий с точной Unauthorized, а также отдельная проверка ошибки данных у разрешённого отправителя. Обе функции добавляются к нашему CourseCatalogTest. Они пока не запускались, поэтому полученные сообщения и успешные прогоны не заявляются.
Отказ тоже является ожидаемым поведением
Приложение не обязано принимать каждый вызов. Для нашего реестра запрет записи чужим адресом — часть корректного результата. В обычном сценарии неожиданный revert означает провал проверки. vm.expectRevert позволяет объяснить среде, что следующий исследуемый вызов должен отклониться определённым образом.
В базовом контракте собственная ошибка Unauthorized() не содержит аргументов. Её selector — первые четыре байта идентификатора соответствующей сигнатуры. Для такой ошибки ожидаемые данные отказа совпадают с этим селектором. Поэтому можно передать CourseCatalog.Unauthorized.selector, не вычисляя вручную строковый хеш.
Добавим следующую функцию внутрь существующего проверяющего контракта. Поля reader, catalog и подготовка нового экземпляра уже определены.
function testReaderCannotPublish() public {
bytes32 digest = keccak256("edition-one");
vm.expectRevert(CourseCatalog.Unauthorized.selector);
vm.prank(reader);
catalog.publish(17, digest);
(bytes32 stored, bool active) = catalog.courses(17);
assertEq(stored, bytes32(0));
assertFalse(active);
}
Мы задаём корректные данные публикации, но чужую роль. Это помогает связать отказ именно с правами. После ожидаемого отката читаем запись и убеждаемся, что она осталась пустой. Проверка описывает не просто сообщение, а разрешённый переход: чужое действие не изменяет состояние.
Вызовы специальных средств vm относятся к проверяющей среде. Между настройкой ожидания и действием не стоит вставлять произвольное обращение к приложению. Оно может стать следующим исследуемым вызовом и изменить смысл сценария. В нашем примере между ними находится только настройка отправителя.
Общий откат может скрыть неправильный путь
Представим менее точную проверку: среда ожидает любой отказ, автор передаёт номер 0 и пытается сделать публикацию. Если отправитель издатель, программа отклонит данные с InvalidCourse. Если отправитель читатель, она раньше отклонит доступ с Unauthorized. Без точного ожидания оба результата выглядят одинаково, хотя исследованы разные требования.
Поэтому отрицательный сценарий строим с минимальным различием. Для проверки роли данные допустимы. Для проверки данных роль разрешена. Это помогает понять, какая граница должна остановить вызов. Если одновременно меняются несколько условий, нужно явно объяснить приоритет и ожидаемую причину отказа.
function testPublisherCannotPublishZeroId() public {
bytes32 digest = keccak256("edition-one");
vm.expectRevert(CourseCatalog.InvalidCourse.selector);
vm.prank(publisher);
catalog.publish(0, digest);
}
Эта функция задаёт другой вопрос. Издатель известен, отпечаток ненулевой, запрещён только номер. Если по ошибке забыть prank, ожидаемая точная ошибка не совпадёт с отказом доступа. Проверка не должна молча пройти только потому, что вызов всё равно был отклонён.
| Исследуемое условие | Отправитель | Аргументы | Причина отказа |
|---|---|---|---|
| Запрет чужой записи | Читатель | Допустимые | Unauthorized |
| Запрет нулевого номера | Издатель | Номер 0 | InvalidCourse |
| Запрет нулевого отпечатка | Издатель | Отпечаток 0 | InvalidCourse |
| Приоритет доступа | Читатель | Неверные | Unauthorized |
Последняя строка также полезна, но она не заменяет две проверки данных. Читатель не достигает тела публикации, поэтому по его отказу нельзя заключить, что валидация номера вообще реализована. Сценарий должен доходить до того участка, свойство которого он исследует.
Ошибка с аргументами требует другого ожидания
Наши ошибки не содержат значений. В других программах ошибочный ответ может включать адрес, номер или сумму. Тогда после селектора идут ABI-кодированные аргументы. Проверка только названия и проверка всего ответа — разные уровни точности; выбор нужно связать с требованием.
Если договор обещает возвращать конкретный неподходящий адрес, следует сравнить его значение, а не любой адрес. Для полного ожидания используют соответствующее ABI-кодирование селектора и аргументов. Не переносите пример четырёхбайтовой ошибки без аргументов как универсальное правило для любого custom error.
В нашем CourseCatalog пока нет такой ошибки. Мы не будем менять публичную сигнатуру лишь ради демонстрации средства проверки. Важно сохранить исходный API, чтобы последующие уроки и браузерная интеграция продолжали читать ту же программу. Расширение ошибки должно быть отдельным явным изменением договора.
Проверяем отсутствие частичного изменения
Откат исполнения возвращает изменения состояния соответствующего вызова. Для отрицательной публикации поэтому ожидается прежняя запись. Если до вызова был активный выпуск H1, сравнить нужно именно H1 и true, а не пустые значения. Правильность ожидания зависит от подготовленного начала.
Это ещё одна причина избегать скрытой публикации в общем setUp. В нашем первом отрицательном сценарии начальное состояние видно: новый реестр без курса. Позднее для запрета обновления мы явно подготовим выпуск издателем, затем переключим роль и попытаемся записать другой отпечаток. Тогда проверка покажет сохранение конкретной предыдущей публикации.
Не все внешние ошибки относятся к revert контракта. Например, отсутствие компилятора или неверный импорт — проблема подготовки проекта. expectRevert не превращает такую проблему в корректный прикладной отказ. Также отказ пользователя в кошельке происходит до отправленного вызова и изучается в отдельном курсе интерфейса.
Честно описываем результат сценария
Сейчас у нас два исходника отрицательных проверок: запрет читателю и запрет нулевого номера издателю. Каждый называет свою причину отказа. В будущей лаборатории они позволят заметить неправильного отправителя, ослабленную валидацию или изменившийся договор ошибок.
Ожидаемый revert не является доказательством безопасности всех операций. Мы ещё не проверили события, замену выпуска, весь диапазон входов и последовательности. Но уже можем объяснить, почему узкий сценарий должен проходить или завершаться ошибкой проверки. Это важнее неопределённого сообщения «что-то не сработало».
В следующем уроке сравним журнал успешной публикации с ожидаемым событием. Устройство custom errors описано в Solidity 0.8.30, а API ожидания — в справочнике Foundry. Оглавление курса.