/
Kots
/
bvc
Обзор
Документация
Войти
/
Kots
/
bvc
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
develop
src/tests/service/Bvc.Service.Application/Services/FileServiceTests.cs
644 строки
18 KB
Константин Телеснин
BVC-023 Разработаны тесты для двух сервисов
03 май 2026, 23:28
03 май 2026, 23:28
48ba0bb
Код
Авторство
О чём код?
using System.Text; using System.Text.Json; using Bvc.Service.Application.Services; using Bvc.Service.Domain.Services; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; namespace Bvc.Service.Application.UnitTests.Services; /// <summary> /// Юнит-тесты для <see cref="FileService"/>. /// Файловая система используется реальная (через временную папку), /// поскольку FileService обращается к статическим методам File/Directory напрямую. /// ILogger подменяется заглушкой NSubstitute. /// </summary> public class FileServiceTests : IDisposable { #region инфраструктура (конструкторы/деструкторы) private readonly ILogger<IFileService> _logger; private readonly FileService _sut; private readonly string _tempDir; /// <summary>Конструктор</summary> public FileServiceTests() { _logger = Substitute.For<ILogger<IFileService>>(); _sut = new FileService(_logger); _tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); _ = Directory.CreateDirectory(_tempDir); } /// <summary>Освободить ресурсы</summary> public void Dispose() { GC.SuppressFinalize(this); if (Directory.Exists(_tempDir)) Directory.Delete(_tempDir, recursive: true); } #endregion #region чтение текста из файла /// <summary>Когда файл существует - вернуть текст</summary> [Fact] public void TryRead_WhenFileExists_ReturnsFileContent() { // Arrange const string expected = "Привет, мир!"; var path = TempFile("hello.txt"); File.WriteAllText(path, expected, Encoding.UTF8); // Act var result = _sut.TryRead(path); // Assert Assert.Equal(expected, result); } /// <summary>Когда файл не существует - вернуть null</summary> [Fact] public void TryRead_WhenFileDoesNotExist_ReturnsNull() { // Arrange var path = TempFile("missing.txt"); // файл не создаём // Act var result = _sut.TryRead(path); // Assert Assert.Null(result); } /// <summary>Когда файл не существует - сделать запись в журнале</summary> [Fact] public void TryRead_WhenFileDoesNotExist_LogsError() { // Arrange var path = TempFile("missing.txt"); // Act _ = _sut.TryRead(path); // Assert VerifyLogError(); } /// <summary>Когда файл пуст - вернуть пустую строку</summary> [Fact] public void TryRead_WhenFileIsEmpty_ReturnsEmptyString() { // Arrange var path = TempFile("empty.txt"); File.WriteAllText(path, string.Empty); // Act var result = _sut.TryRead(path); // Assert Assert.Equal(string.Empty, result); } #endregion #region запись текста в файл /// <summary>Если путь при записи валидный - вернуть true</summary> [Fact] public void TryWrite_WhenPathIsValid_ReturnsTrue() { // Arrange var path = TempFile("output.txt"); // Act var result = _sut.TryWrite(path, "data"); // Assert Assert.True(result); } /// <summary>Если путь при записи валидный - Записать текст</summary> [Fact] public void TryWrite_WhenPathIsValid_WritesContentToFile() { // Arrange const string content = "тестовое содержимое"; var path = TempFile("output.txt"); // Act _ = _sut.TryWrite(path, content); // Assert Assert.Equal(content, File.ReadAllText(path, Encoding.UTF8)); } /// <summary>Если папка не существует - создать и вернуть true</summary> [Fact] public void TryWrite_WhenDirectoryDoesNotExist_CreatesDirectoryAndReturnsTrue() { // Arrange var nestedPath = Path.Combine(_tempDir, "sub", "dir", "file.txt"); // Act var result = _sut.TryWrite(nestedPath, "nested"); // Assert Assert.True(result); Assert.True(File.Exists(nestedPath)); } /// <summary>Если при записи путь содержит недопустимые комбинации символов - вернуть false</summary> [Fact] public void TryWrite_WhenPathIsInvalid_ReturnsFalse() { // Arrange — нулевой символ в имени вызывает исключение на всех ОС var invalidPath = Path.Combine(_tempDir, "inva\0lid.txt"); // Act var result = _sut.TryWrite(invalidPath, "data"); // Assert Assert.False(result); } /// <summary>Если при записи путь содержит недопустимые комбинации символов - записать ошибку в журнал</summary> [Fact] public void TryWrite_WhenPathIsInvalid_LogsError() { // Arrange var invalidPath = Path.Combine(_tempDir, "inva\0lid.txt"); // Act _ = _sut.TryWrite(invalidPath, "data"); // Assert VerifyLogError(); } #endregion #region удаление файла /// <summary>Когда имя файла null - вернуть false без обращения к диску</summary> [Fact] public void TryDelete_WhenFilenameIsNull_ReturnsFalse() { // Act var result = _sut.TryDelete(null); // Assert Assert.False(result); } /// <summary>Когда имя файла состоит из пробелов - вернуть false без обращения к диску</summary> [Fact] public void TryDelete_WhenFilenameIsWhitespace_ReturnsFalse() { // Act var result = _sut.TryDelete(" "); // Assert Assert.False(result); } /// <summary>Когда файл не существует - вернуть false без обращения к диску</summary> [Fact] public void TryDelete_WhenFileDoesNotExist_ReturnsFalse() { // Arrange var path = TempFile("ghost.txt"); // файл не создаём // Act var result = _sut.TryDelete(path); // Assert Assert.False(result); } /// <summary>Когда файл существует - удалить файл и вернуть true</summary> [Fact] public void TryDelete_WhenFileExists_ReturnsTrueAndDeletesFile() { // Arrange var path = TempFile("to-delete.txt"); File.WriteAllText(path, "данные"); // Act var result = _sut.TryDelete(path); // Assert Assert.True(result); Assert.False(File.Exists(path)); } /// <summary>Когда удаление файла завершается ошибкой - вернуть false</summary> [Fact] public void TryDelete_WhenDeletionFails_ReturnsFalse() { // Arrange var path = TempFile("protected.txt"); File.WriteAllText(path, "данные"); MakeUndeletable(path); try { // Act var result = _sut.TryDelete(path); // Assert Assert.False(result); } finally { RestoreDeletable(path); } } /// <summary>Когда удаление файла завершается ошибкой - записать ошибку в журнал</summary> [Fact] public void TryDelete_WhenDeletionFails_LogsError() { // Arrange var path = TempFile("protected-log.txt"); File.WriteAllText(path, "данные"); MakeUndeletable(path); try { // Act _ = _sut.TryDelete(path); // Assert VerifyLogError(); } finally { RestoreDeletable(path); } } #endregion #region чтение текстов из многих файлов /// <summary>Когда есть файлы - вернуть словарь</summary> [Fact] public void TryReadAll_WhenDirectoryHasFiles_ReturnsDictionaryWithContent() { // Arrange File.WriteAllText(TempFile("a.txt"), "alpha"); File.WriteAllText(TempFile("b.txt"), "beta"); // Act var result = _sut.TryReadAll(_tempDir, "*.txt"); // Assert Assert.Equal(2, result.Count); Assert.Contains(result.Values, v => v == "alpha"); Assert.Contains(result.Values, v => v == "beta"); } /// <summary>Отфильтровать файлы с пустым контентом</summary> [Fact] public void TryReadAll_WhenFilesHaveEmptyContent_FiltersThemOut() { // Arrange File.WriteAllText(TempFile("empty.txt"), string.Empty); File.WriteAllText(TempFile("whitespace.txt"), " "); File.WriteAllText(TempFile("valid.txt"), "content"); // Act var result = _sut.TryReadAll(_tempDir, "*.txt"); // Assert _ = Assert.Single(result); Assert.Contains(result.Values, v => v == "content"); } /// <summary>Когда папка не существует - вернуть пустой словарь</summary> [Fact] public void TryReadAll_WhenDirectoryDoesNotExist_ReturnsEmptyDictionary() { // Arrange var missing = Path.Combine(_tempDir, "ghost_dir"); // Act var result = _sut.TryReadAll(missing); // Assert Assert.Empty(result); } /// <summary>Когда папка не существует - записать ошибку в журнал</summary> [Fact] public void TryReadAll_WhenDirectoryDoesNotExist_LogsError() { // Arrange var missing = Path.Combine(_tempDir, "ghost_dir"); // Act _ = _sut.TryReadAll(missing); // Assert VerifyLogError(); } /// <summary>Вернуть только файлы, соответствующие шаблону поиска</summary> [Fact] public void TryReadAll_WithSearchPattern_ReturnsOnlyMatchingFiles() { // Arrange File.WriteAllText(TempFile("data.json"), "{}"); File.WriteAllText(TempFile("notes.txt"), "hello"); // Act var result = _sut.TryReadAll(_tempDir, "*.json"); // Assert _ = Assert.Single(result); Assert.Contains(result.Values, v => v == "{}"); } #endregion #region загрузка объекта /// <summary>Когда файл содержит корректный JSON - вернуть десериализованный объект</summary> [Fact] public void TryLoad_WhenFileContainsValidJson_ReturnsDeserializedObject() { // Arrange var expected = new SampleDto(42, "тест"); var path = TempFile("obj.json"); File.WriteAllText(path, JsonSerializer.Serialize(expected)); // Act var result = _sut.TryLoad<SampleDto>(path); // Assert Assert.NotNull(result); Assert.Equal(expected.Id, result.Id); Assert.Equal(expected.Name, result.Name); } /// <summary>Когда файл не существует - вернуть значение по умолчанию</summary> [Fact] public void TryLoad_WhenFileDoesNotExist_ReturnsDefault() { // Arrange var path = TempFile("missing.json"); // Act var result = _sut.TryLoad<SampleDto>(path); // Assert Assert.Null(result); } /// <summary>Когда файл содержит некорректный JSON - вернуть значение по умолчанию</summary> [Fact] public void TryLoad_WhenFileContainsInvalidJson_ReturnsDefault() { // Arrange var path = TempFile("broken.json"); File.WriteAllText(path, "{ не валидный json }"); // Act var result = _sut.TryLoad<SampleDto>(path); // Assert Assert.Null(result); } /// <summary>Когда файл содержит некорректный JSON - записать предупреждение в журнал</summary> [Fact] public void TryLoad_WhenFileContainsInvalidJson_LogsWarning() { // Arrange var path = TempFile("broken.json"); File.WriteAllText(path, "!!!invalid!!!"); // Act _ = _sut.TryLoad<SampleDto>(path); // Assert VerifyLogWarning(); } #endregion #region сохранение объекта /// <summary>Если путь при сохранении валидный - вернуть true</summary> [Fact] public void TrySave_WhenPathIsValid_ReturnsTrue() { // Arrange var path = TempFile("save.json"); var value = new SampleDto(1, "сохранение"); // Act var result = _sut.TrySave(path, value); // Assert Assert.True(result); } /// <summary>Если путь при сохранении валидный - записать сериализованный JSON</summary> [Fact] public void TrySave_WhenPathIsValid_WritesSerializedJson() { // Arrange var path = TempFile("save.json"); var value = new SampleDto(7, "объект"); // Act _ = _sut.TrySave(path, value); // Assert var written = File.ReadAllText(path, Encoding.UTF8); var deserialized = JsonSerializer.Deserialize<SampleDto>(written); Assert.NotNull(deserialized); Assert.Equal(value.Id, deserialized.Id); Assert.Equal(value.Name, deserialized.Name); } /// <summary>Если при сохранении путь содержит недопустимые комбинации символов - вернуть false</summary> [Fact] public void TrySave_WhenPathIsInvalid_ReturnsFalse() { // Arrange var invalidPath = Path.Combine(_tempDir, "inva\0lid.json"); // Act var result = _sut.TrySave(invalidPath, new SampleDto(0)); // Assert Assert.False(result); } /// <summary>Если объект не сериализуем - вернуть false</summary> /// <remarks> /// Покрывает catch-блок TrySave (строки 97-101): /// TryWrite перехватывает исключения файловой системы внутри себя и возвращает false, /// поэтому для попадания в catch самого TrySave нужно уронить JsonSerializer.Serialize. /// </remarks> [Fact] public void TrySave_WhenValueIsNotSerializable_ReturnsFalse() { // Arrange var path = TempFile("unreachable.json"); // Act var result = _sut.TrySave(path, new NonSerializableDto()); // Assert Assert.False(result); } /// <summary>Если объект не сериализуем - записать предупреждение в журнал</summary> [Fact] public void TrySave_WhenValueIsNotSerializable_LogsWarning() { // Arrange var path = TempFile("unreachable.json"); // Act _ = _sut.TrySave(path, new NonSerializableDto()); // Assert VerifyLogWarning(); } #endregion #region загрузка объектов из многих файлов /// <summary>Когда есть файлы с корректным JSON - вернуть словарь</summary> [Fact] public void TryLoadAll_WhenDirectoryHasValidJsonFiles_ReturnsDictionary() { // Arrange var item1 = new SampleDto(1, "один"); var item2 = new SampleDto(2, "два"); File.WriteAllText(TempFile("1.json"), JsonSerializer.Serialize(item1)); File.WriteAllText(TempFile("2.json"), JsonSerializer.Serialize(item2)); // Act var result = _sut.TryLoadAll<SampleDto>(_tempDir, "*.json"); // Assert Assert.Equal(2, result.Count); Assert.Contains(result.Values, v => v?.Id == 1); Assert.Contains(result.Values, v => v?.Id == 2); } /// <summary>Отфильтровать файлы с некорректным JSON</summary> [Fact] public void TryLoadAll_WhenFilesContainInvalidJson_FiltersThemOut() { // Arrange File.WriteAllText(TempFile("valid.json"), JsonSerializer.Serialize(new SampleDto(5))); File.WriteAllText(TempFile("broken.json"), "не JSON"); // Act var result = _sut.TryLoadAll<SampleDto>(_tempDir, "*.json"); // Assert _ = Assert.Single(result); Assert.Contains(result.Values, v => v?.Id == 5); } /// <summary>Когда папка не существует - вернуть пустой словарь</summary> [Fact] public void TryLoadAll_WhenDirectoryDoesNotExist_ReturnsEmptyDictionary() { // Arrange var missing = Path.Combine(_tempDir, "ghost_dir"); // Act var result = _sut.TryLoadAll<SampleDto>(missing); // Assert Assert.Empty(result); } /// <summary>Когда папка не существует - записать ошибку в журнал</summary> [Fact] public void TryLoadAll_WhenDirectoryDoesNotExist_LogsError() { // Arrange var missing = Path.Combine(_tempDir, "ghost_dir"); // Act _ = _sut.TryLoadAll<SampleDto>(missing); // Assert VerifyLogError(); } #endregion #region вспомогательные методы и типы /// <summary>Формирует путь к файлу во временной папке (не создаёт его).</summary> private string TempFile(string name) => Path.Combine(_tempDir, name); /// <summary> /// Делает файл неудаляемым кросс-платформенно. /// Linux — снимает бит записи с родительской директории (она контролирует удаление файла). /// Windows — выставляет атрибут ReadOnly на самом файле. /// Оба вызова выполняются всегда: на каждой платформе один из них срабатывает, второй игнорируется. /// </summary> private static void MakeUndeletable(string filePath) { File.SetAttributes(Path.GetDirectoryName(filePath)!, FileAttributes.ReadOnly); // Linux File.SetAttributes(filePath, FileAttributes.ReadOnly); // Windows } /// <summary> /// Восстанавливает права после <see cref="MakeUndeletable"/>, чтобы Dispose смог очистить папку. /// </summary> private static void RestoreDeletable(string filePath) { File.SetAttributes(Path.GetDirectoryName(filePath)!, FileAttributes.Normal); File.SetAttributes(filePath, FileAttributes.Normal); } /// <summary>Проверить, что вызвана запись в журнал с уровнем Error</summary> protected void VerifyLogError() => _logger.Received(1).Log( LogLevel.Error, Arg.Any<EventId>(), Arg.Any<object>(), Arg.Any<Exception?>(), Arg.Any<Func<object, Exception?, string>>() ); /// <summary>Проверить, что вызвана запись в журнал с уровнем Warning</summary> protected void VerifyLogWarning() => _logger.Received(1).Log( LogLevel.Warning, Arg.Any<EventId>(), Arg.Any<object>(), Arg.Any<Exception?>(), Arg.Any<Func<object, Exception?, string>>() ); /// <summary>Тестовый тип</summary> protected record SampleDto(int Id, string? Name = null); /// <summary>Тестовый тип, намеренно ломающий сериализацию</summary> protected class NonSerializableDto { /// <summary>Вспомогательное поле</summary> protected string _value = string.Empty; /// <summary>Свойство, чтение которого вызывает исключение </summary> public string Value => _value.Length > 0 ? _value : throw new InvalidOperationException("Сериализация намеренно запрещена"); } #endregion }