首页 > 解决方案 > JUnit4 - 如果使用 docker 运行,如何测试只读/写保护目录

问题描述

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

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

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

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

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

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

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

标签: javaspringdockerjunit4

解决方案


我认为这是由于 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 。


推荐阅读