У нас есть настройка интеграционного теста для тестирования поведения отсутствующих, но необходимых свойств конфигурации. Одним из этих свойств является каталог, в который следует записывать неудачные загрузки для последующих попыток. Общее поведение для этого свойства должно состоять в том, что приложение даже не запускается и сразу выходит из строя при нарушении определенных ограничений.
Свойства управляются Spring через определенные ConfigurationProperties
, среди которых у нас есть простой S3MessageUploadSettings
class
@Getter
@Setter
@ConfigurationProperties(prefix = "s3")
@Validated
public class S3MessageUploadSettings {
@NotNull
private String bucketName;
@NotNull
private String uploadErrorPath;
...
}
В соответствующей конфигурации Spring мы теперь выполняем определенные проверки правильности, такие как, существует ли путь, доступен ли для записи и каталог, и генерируем соответствующие RuntimeException
s, когда определенные утверждения не выполнены:
@Slf4j
@Import({ S3Config.class })
@Configuration
@EnableConfigurationProperties(S3MessageUploadSettings.class)
public class S3MessageUploadSpringConfig {
@Resource
private S3MessageUploadSettings settings;
...
@PostConstruct
public void checkConstraints() {
String sPath = settings.getUploadErrorPath();
Path path = Paths.get(sPath);
...
log.debug("Probing path '{}' for existence', path);
if (!Files.exists(path)) {
throw new RuntimeException("Required error upload directory '" + path + "' does not exist");
}
log.debug("Probig path '{}' for being a directory", path);
if (!Files.isDirectory(path)) {
throw new RuntimeException("Upload directory '" + path + "' is not a directoy");
}
log.debug("Probing path '{}' for write permissions", path);
if (!Files.isWritable(path)) {
throw new RuntimeException("Error upload path '" + path +"' is not writable);
}
}
}
Теперь наша тестовая установка выглядит следующим образом:
public class StartupTest {
@ClassRule
public static TemporaryFolder testFolder = new TemporaryFolder();
private static File BASE_FOLDER;
private static File ACCESSIBLE;
private static File WRITE_PROTECTED;
private static File NON_DIRECTORY;
@BeforeClass
public static void initFolderSetup() throws IOException {
BASE_FOLDER = testFolder.getRoot();
ACCESSIBLE = testFolder.newFolder("accessible");
WRITE_PROTECTED = testFolder.newFolder("writeProtected");
if (!WRITE_PROTECTED.setReadOnly()) {
fail("Could not change directory permissions to readonly")
}
if (!WRITE_PROTECTED.setWritable(false)) {
fail("Could not change directory permissions to writable(false)");
}
NON_DIRECTORY = testFolder.newFile("nonDirectory");
}
@Configuration
@Import({
S3MessageUploadSpringConfig.class,
S3MockConfig.class,
...
})
static class BaseContextConfig {
// common bean definitions
...
}
@Configuration
@Import(BaseContextConfig.class)
@PropertySource("classpath:ci.properties")
static class NotExistingPathContextConfig {
@Resource
private S3MessageUploadSettings settings;
@PostConstruct
public void updateSettings() {
settings.setUploadErrorPath(BASE_FOLDER.getPath() + "/foo/bar");
}
}
@Configuration
@Import(BaseContextConfig.class)
@PropertySource("classpath:ci.properties")
static class NotWritablePathContextConfig {
@Resource
private S3MessageUploadSettings settings;
@PostConstruct
public void updateSettings() {
settings.setUploadErrorPath(WRITE_PROTECTED.getPath());
}
}
...
@Configuration
@Import(BaseContextConfig.class)
@PropertySource("classpath:ci.properties")
static class StartableContextConfig {
@Resource
private S3MessageUploadSettings settings;
@PostConstruct
public void updateSettings() {
settings.setUploadErrorPath(ACCESSIBLE.getPath());
}
}
@Test
public void shouldFailStartupDueToNonExistingErrorPathDirectory() {
ApplicationContext context = null;
try {
context = new AnnotationConfigApplicationContext(StartupTest.NotExistingPathContextConfig.class);
fail("Should not have started the context");
} catch (Exception e) {
e.printStackTrace();
assertThat(e, instanceOf(BeanCreationException.class));
assertThat(e.getMessage(), containsString("Required error upload directory '" + BASE_FOLDER + "/foo/bar' does not exist"));
} finally {
closeContext(context);
}
}
@Test
public void shouldFailStartupDueToNonWritablePathDirectory() {
ApplicationContext context = null;
try {
context = new AnnotationConfigApplicationContext(StartupTest.NotWritablePathContextConfig.class);
fail("Should not have started the context");
} catch (Exception e) {
assertThat(e, instanceOf(BeanCreationException.class));
assertThat(e.getMessage(), containsString("Error upload path '" + WRITE_PROTECTED + "' is not writable"));
} finally {
closeContext(context);
}
}
...
@Test
public void shouldStartUpSuccessfully() {
ApplicationContext context = null;
try {
context = new AnnotationConfigApplicationContext(StartableContextConfig.class);
} catch (Exception e) {
e.printStackTrace();
fail("Should not have thrown an exception of type " + e.getClass().getSimpleName() + " with message " + e.getMessage());
} finally {
closeContext(context);
}
}
private void closeContext(ApplicationContext context) {
if (context != null) {
// check and close any running S3 mock as this may have negative impact on the startup of a further context
closeS3Mock(context);
// stop a running Spring context manually as this might interfere with a starting context of an other test
((ConfigurableApplicationContext) context).stop();
}
}
private void closeS3Mock(ApplicationContext context) {
S3Mock s3Mock = null;
try {
if (context != null) {
s3Mock = context.getBean("s3Mock", S3Mock.class);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != s3Mock) {
s3Mock.stop();
}
}
}
}
При локальном запуске все выглядит хорошо и все тесты проходят. Хотя наш CI выполняет эти тесты в док-контейнере и по какой-то причине изменение прав доступа к файлу, по-видимому, приводит к тому, что NOOP возвращает true при вызове метода, но ничего не меняет в отношении самого разрешения файла.
Neiter File.setReadOnly()
, File.setWritable(false)
и Files.setPosixFilePermissions(Path, Set<PosixFilePermission>)
, похоже, влияют на фактические права доступа к файлам в контейнере Docker.
Я также пытался изменить каталоги на реальныекаталоги, то есть /root
или /dev/pts
, которые защищены от записи, хотя, когда CI выполняет тесты как root
, эти каталоги доступны для записи приложению, и тест снова не проходит.
Я также рассмотрел использованиефайловая система в памяти (такая как JimFS), хотя здесь я не уверен, как убедить тест использовать пользовательскую файловую систему. AFAIK JimFS не поддерживает конструктор, необходимый для объявления его в качестве файловой системы по умолчанию.
Какие другие возможности существуют в Java для изменения разрешения каталогов на чтение / защита от записи при запуске внутри контейнера Docker или успешном тестировании для такогокаталог?