JUnit4 - 如果使用 docker 运行,如何测试只读/写保护目录



我们有一个集成测试设置,用于测试缺少但必需的配置属性的行为。其中一个属性中有一个目录,失败的上载应写入该目录以供以后重试。此属性的一般行为应该是,当违反某些约束时,应用程序甚至不会启动并立即失败。

这些属性由Spring通过某些ConfigurationProperties其中我们有一个简单的S3MessageUploadSettings

@Getter
@Setter
@ConfigurationProperties(prefix = "s3")
@Validated
public class S3MessageUploadSettings {
@NotNull
private String bucketName;
@NotNull
private String uploadErrorPath;
...
}

在相应的 Spring 配置中,我们现在执行某些验证检查,例如路径是否存在、可写和目录,并在不满足某些断言时抛出相应的RuntimeException

@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 在 docker 容器中运行这些测试,并且由于某种原因更改文件权限似乎最终导致 NOOP 在方法调用时返回 true,尽管没有更改有关文件权限本身的任何内容。

NeiterFile.setReadOnly()File.setWritable(false)Files.setPosixFilePermissions(Path, Set<PosixFilePermission>)似乎对 docker 容器中的实际文件权限有影响。

我还尝试将目录更改为真实目录,即/root/dev/pts写保护,但当 CI 按root运行时,这些目录可由应用程序写入,并且测试再次失败。

我还考虑过使用内存文件系统(例如 JimFS(,尽管在这里我不确定如何说服测试使用自定义文件系统。AFAIK JimFS 不支持将其声明为默认文件系统所需的构造函数。

在 Java 中还有哪些其他可能性可以在 docker 容器内运行时将目录权限更改为只读/写保护,或者成功测试此类目录?

我认为这是由于 JVM 的权限和策略,如果操作系统阻止了 JVM 的某些权限,则无法从代码中执行任何操作。

您可以尝试编辑 java.policy 文件并设置适当的文件权限。

也许这些将是一些将设置写入权限的给定文件,例如:

grant {
permission java.io.FilePermission "/dev/pts/*", "read,write,delete";
};

文档中的更多示例:https://docs.oracle.com/javase/8/docs/technotes/guides/security/spec/security-spec.doc3.html。

最新更新