diff --git a/apis/fluent-en_GB/atrium-api-fluent-en_GB-common/src/test/kotlin/ch/tutteli/atrium/api/fluent/en_GB/samples/ThrowableExpectationSamples.kt b/apis/fluent-en_GB/atrium-api-fluent-en_GB-common/src/test/kotlin/ch/tutteli/atrium/api/fluent/en_GB/samples/ThrowableExpectationSamples.kt index 8b3685748f..3f7fc13b2e 100644 --- a/apis/fluent-en_GB/atrium-api-fluent-en_GB-common/src/test/kotlin/ch/tutteli/atrium/api/fluent/en_GB/samples/ThrowableExpectationSamples.kt +++ b/apis/fluent-en_GB/atrium-api-fluent-en_GB-common/src/test/kotlin/ch/tutteli/atrium/api/fluent/en_GB/samples/ThrowableExpectationSamples.kt @@ -58,7 +58,7 @@ class ThrowableExpectationSamples { } fails { // because wrong type expected (IllegalStateException instead of IndexOutOfBoundsException), but since we use a block... - expect(IllegalStateException(IndexOutOfBoundsException("abc"))).cause { + expect(IllegalStateException(IndexOutOfBoundsException("abc"))).cause { messageToContain("b") // ... reporting mentions that subject's message was expected `to contain: "b"` } } diff --git a/apis/fluent-en_GB/atrium-api-fluent-en_GB-jvm/src/main/kotlin/ch/tutteli/atrium/api/fluent/en_GB/pathExpectations.kt b/apis/fluent-en_GB/atrium-api-fluent-en_GB-jvm/src/main/kotlin/ch/tutteli/atrium/api/fluent/en_GB/pathExpectations.kt index 1788755131..100fc07d6a 100644 --- a/apis/fluent-en_GB/atrium-api-fluent-en_GB-jvm/src/main/kotlin/ch/tutteli/atrium/api/fluent/en_GB/pathExpectations.kt +++ b/apis/fluent-en_GB/atrium-api-fluent-en_GB-jvm/src/main/kotlin/ch/tutteli/atrium/api/fluent/en_GB/pathExpectations.kt @@ -115,6 +115,28 @@ fun Expect.notToExist(): Expect = fun Expect.toBeReadable(): Expect = _logicAppend { isReadable() } +/** + * Expects that the subject of `this` expectation (a [Path]) is not readable; + * meaning that there is a file system entry at the location the [Path] points to and + * that the current thread does not have the permission to read from it. + * + * This assertion _resolves_ symbolic links. + * Therefore, if a symbolic link exists at the location the subject points to, + * search will continue at the location the link points at. + * + * This assertion is not atomic with respect to concurrent file system operations on the paths the assertion works on. + * Its result, in particular its extended explanations, may be wrong if such concurrent file system operations + * take place. + * + * @return an [Expect] for the subject of `this` expectation. + * + * @sample ch.tutteli.atrium.api.fluent.en_GB.samples.PathExpectationSamples.notToBeReadable + * + * @since 0.17.0 + */ +fun Expect.notToBeReadable(): Expect = + _logicAppend { isNotReadable() } + /** * Expects that the subject of `this` expectation (a [Path]) is writable; * meaning that there is a file system entry at the location the [Path] points to and diff --git a/apis/fluent-en_GB/atrium-api-fluent-en_GB-jvm/src/test/kotlin/ch/tutteli/atrium/api/fluent/en_GB/PathExpectationsSpec.kt b/apis/fluent-en_GB/atrium-api-fluent-en_GB-jvm/src/test/kotlin/ch/tutteli/atrium/api/fluent/en_GB/PathExpectationsSpec.kt index 2187fbaef4..142cd9d4eb 100644 --- a/apis/fluent-en_GB/atrium-api-fluent-en_GB-jvm/src/test/kotlin/ch/tutteli/atrium/api/fluent/en_GB/PathExpectationsSpec.kt +++ b/apis/fluent-en_GB/atrium-api-fluent-en_GB-jvm/src/test/kotlin/ch/tutteli/atrium/api/fluent/en_GB/PathExpectationsSpec.kt @@ -13,6 +13,7 @@ class PathExpectationsSpec : ch.tutteli.atrium.specs.integration.PathExpectation fun1(Expect::toEndWith), fun1(Expect::notToEndWith), fun0(Expect::toBeReadable), + fun0(Expect::notToBeReadable), fun0(Expect::toBeWritable), fun0(Expect::notToBeWritable), fun0(Expect::toBeExecutable), @@ -58,6 +59,7 @@ class PathExpectationsSpec : ch.tutteli.atrium.specs.integration.PathExpectation a1.toEndWith(Paths.get("a")) a1.notToEndWith(Paths.get("a")) a1.toBeReadable() + a1.notToBeReadable() a1.toBeWritable() a1.notToBeWritable() a1.toBeExecutable() diff --git a/apis/fluent-en_GB/atrium-api-fluent-en_GB-jvm/src/test/kotlin/ch/tutteli/atrium/api/fluent/en_GB/samples/PathExpectationSamples.kt b/apis/fluent-en_GB/atrium-api-fluent-en_GB-jvm/src/test/kotlin/ch/tutteli/atrium/api/fluent/en_GB/samples/PathExpectationSamples.kt index d30f4a73bb..640469b4bd 100644 --- a/apis/fluent-en_GB/atrium-api-fluent-en_GB-jvm/src/test/kotlin/ch/tutteli/atrium/api/fluent/en_GB/samples/PathExpectationSamples.kt +++ b/apis/fluent-en_GB/atrium-api-fluent-en_GB-jvm/src/test/kotlin/ch/tutteli/atrium/api/fluent/en_GB/samples/PathExpectationSamples.kt @@ -120,6 +120,39 @@ class PathExpectationSamples { } } + @Test + fun notToBeReadable() { + assertIf(ifPosixSupported) { + val writeOnlyPermissions = + PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("-w--w--w-")) + val readyWritePermissions = + PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-rw-rw-")) + + val writeOnlyDir = tempDir.newDirectory("write_only_dir", writeOnlyPermissions) + val writeOnlyFile = tempDir.newFile("write_only_file", writeOnlyPermissions) + val readWriteDir = tempDir.newDirectory("read_write_dir", readyWritePermissions) + val readWriteFile = tempDir.newFile("read_write_file", readyWritePermissions) + + expect(writeOnlyDir).notToBeReadable() + expect(writeOnlyFile).notToBeReadable() + + fails { + expect(readWriteDir).notToBeReadable() + } + fails { + expect(readWriteFile).notToBeReadable() + } + + fails { + expect(readWriteDir).notToBeReadable() + } + } + + fails { + expect(Paths.get("non_existing_dir")).notToBeReadable() + } + } + @Test fun toBeWritable() { val dir = tempDir.newDirectory("test_dir") diff --git a/apis/infix-en_GB/atrium-api-infix-en_GB-jvm/src/main/kotlin/ch/tutteli/atrium/api/infix/en_GB/pathAssertions.kt b/apis/infix-en_GB/atrium-api-infix-en_GB-jvm/src/main/kotlin/ch/tutteli/atrium/api/infix/en_GB/pathAssertions.kt index b8e40fc5bd..e09f4e78e1 100644 --- a/apis/infix-en_GB/atrium-api-infix-en_GB-jvm/src/main/kotlin/ch/tutteli/atrium/api/infix/en_GB/pathAssertions.kt +++ b/apis/infix-en_GB/atrium-api-infix-en_GB-jvm/src/main/kotlin/ch/tutteli/atrium/api/infix/en_GB/pathAssertions.kt @@ -280,6 +280,7 @@ fun path(path: String, assertionCreator: Expect.() -> Unit): PathWithCrea infix fun Expect.toBe(@Suppress("UNUSED_PARAMETER") readable: readable): Expect = _logicAppend { isReadable() } + //TODO move to pathExpectations.kt with 0.18.0 /** * Expects that the subject of `this` expectation (a [Path]) is writable; diff --git a/apis/infix-en_GB/atrium-api-infix-en_GB-jvm/src/main/kotlin/ch/tutteli/atrium/api/infix/en_GB/pathExpectations.kt b/apis/infix-en_GB/atrium-api-infix-en_GB-jvm/src/main/kotlin/ch/tutteli/atrium/api/infix/en_GB/pathExpectations.kt index c18a7ca20d..3b9d328c88 100644 --- a/apis/infix-en_GB/atrium-api-infix-en_GB-jvm/src/main/kotlin/ch/tutteli/atrium/api/infix/en_GB/pathExpectations.kt +++ b/apis/infix-en_GB/atrium-api-infix-en_GB-jvm/src/main/kotlin/ch/tutteli/atrium/api/infix/en_GB/pathExpectations.kt @@ -129,6 +129,28 @@ infix fun Expect.toHaveTheSameTextualContentAs(pathWithEncoding: P infix fun Expect.toHaveTheSameBinaryContentAs(targetPath: Path): Expect = _logicAppend { hasSameBinaryContentAs(targetPath) } +/** + * Expects that the subject of `this` expectation (a [Path]) is not readable; + * meaning that there is a file system entry at the location the [Path] points to and + * that the current thread does not have the permission to read from it. + * + * This assertion _resolves_ symbolic links. + * Therefore, if a symbolic link exists at the location the subject points to, + * search will continue at the location the link points at. + * + * This assertion is not atomic with respect to concurrent file system operations on the paths the assertion works on. + * Its result, in particular its extended explanations, may be wrong if such concurrent file system operations + * take place. + * + * @return an [Expect] for the subject of `this` expectation. + * + * @sample ch.tutteli.atrium.api.infix.en_GB.samples.PathExpectationSamples.notToBeReadable + * + * @since 0.17.0 + */ +infix fun Expect.notToBe(@Suppress("UNUSED_PARAMETER") readable: readable): Expect = + _logicAppend { isNotReadable() } + /** * Expects that the subject of `this` expectation (a [Path]) is not writable; * meaning that there is a file system entry at the location the [Path] points to and diff --git a/apis/infix-en_GB/atrium-api-infix-en_GB-jvm/src/test/kotlin/ch/tutteli/atrium/api/infix/en_GB/PathExpectationsSpec.kt b/apis/infix-en_GB/atrium-api-infix-en_GB-jvm/src/test/kotlin/ch/tutteli/atrium/api/infix/en_GB/PathExpectationsSpec.kt index 4451ef4126..fa3d1c2041 100644 --- a/apis/infix-en_GB/atrium-api-infix-en_GB-jvm/src/test/kotlin/ch/tutteli/atrium/api/infix/en_GB/PathExpectationsSpec.kt +++ b/apis/infix-en_GB/atrium-api-infix-en_GB-jvm/src/test/kotlin/ch/tutteli/atrium/api/infix/en_GB/PathExpectationsSpec.kt @@ -14,6 +14,7 @@ class PathExpectationsSpec : ch.tutteli.atrium.specs.integration.PathExpectation fun1(Expect::toEndWith), fun1(Expect::notToEndWith), "toBe ${readable::class.simpleName}" to Companion::toBeReadable, + "notToBe ${readable::class.simpleName}" to Companion::notToBeReadable, "toBe ${writable::class.simpleName}" to Companion::toBeWritable, "notToBe ${writable::class.simpleName}" to Companion::notToBeWritable, "toBe ${executable::class.simpleName}" to Companion::toBeExecutable, @@ -45,6 +46,7 @@ class PathExpectationsSpec : ch.tutteli.atrium.specs.integration.PathExpectation private fun toExist(expect: Expect) = expect toBe existing private fun notToExist(expect: Expect) = expect notToBe existing private fun toBeReadable(expect: Expect) = expect toBe readable + private fun notToBeReadable(expect: Expect) = expect notToBe readable private fun toBeWritable(expect: Expect) = expect toBe writable private fun notToBeWritable(expect: Expect) = expect notToBe writable private fun toBeExecutable(expect: Expect) = expect toBe executable @@ -91,6 +93,7 @@ class PathExpectationsSpec : ch.tutteli.atrium.specs.integration.PathExpectation a1 toEndWith Paths.get("a") a1 notToEndWith Paths.get("a") a1 toBe readable + a1 notToBe readable a1 toBe writable a1 notToBe writable a1 toBe executable diff --git a/apis/infix-en_GB/atrium-api-infix-en_GB-jvm/src/test/kotlin/ch/tutteli/atrium/api/infix/en_GB/samples/PathExpectationSamples.kt b/apis/infix-en_GB/atrium-api-infix-en_GB-jvm/src/test/kotlin/ch/tutteli/atrium/api/infix/en_GB/samples/PathExpectationSamples.kt index 798af40654..3cdda738e5 100644 --- a/apis/infix-en_GB/atrium-api-infix-en_GB-jvm/src/test/kotlin/ch/tutteli/atrium/api/infix/en_GB/samples/PathExpectationSamples.kt +++ b/apis/infix-en_GB/atrium-api-infix-en_GB-jvm/src/test/kotlin/ch/tutteli/atrium/api/infix/en_GB/samples/PathExpectationSamples.kt @@ -118,6 +118,35 @@ class PathExpectationSamples { } } + @Test + fun notToBeReadable() { + assertIf(fileSystemSupportsPosixPermissions()) { + val writeOnlyPermissions = + PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("-w--w--w-")) + val readWritePermissions = + PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-rw-rw-")) + + val writeOnlyDir = tempDir.newDirectory("write_only_dir", writeOnlyPermissions) + val writeOnlyFile = tempDir.newFile("write_only_file", writeOnlyPermissions) + val readWriteDir = tempDir.newDirectory("read_write_dir", readWritePermissions) + val readWriteFile = tempDir.newFile("read_write_file", readWritePermissions) + + expect(writeOnlyDir) notToBe readable + expect(writeOnlyFile) notToBe readable + + fails { + expect(readWriteDir) notToBe readable + } + fails { + expect(readWriteFile) notToBe readable + } + } + + fails { + expect(Paths.get("non_existing_dir")) notToBe readable + } + } + @Test fun toBeWritable() { val dir = tempDir.newDirectory("test_dir") diff --git a/logic/atrium-logic-jvm/src/generated/kotlin/ch/tutteli/atrium/logic/path.kt b/logic/atrium-logic-jvm/src/generated/kotlin/ch/tutteli/atrium/logic/path.kt index 0f08befb6f..5564ce4b4e 100644 --- a/logic/atrium-logic-jvm/src/generated/kotlin/ch/tutteli/atrium/logic/path.kt +++ b/logic/atrium-logic-jvm/src/generated/kotlin/ch/tutteli/atrium/logic/path.kt @@ -30,6 +30,7 @@ fun AssertionContainer.exists(linkOption: LinkOption? = null): Ass fun AssertionContainer.existsNot(linkOption: LinkOption? = null): Assertion = impl.existsNot(this, linkOption) fun AssertionContainer.isReadable(): Assertion = impl.isReadable(this) +fun AssertionContainer.isNotReadable(): Assertion = impl.isNotReadable(this) fun AssertionContainer.isWritable(): Assertion = impl.isWritable(this) fun AssertionContainer.isNotWritable(): Assertion = impl.isNotWritable(this) fun AssertionContainer.isExecutable(): Assertion = impl.isExecutable(this) diff --git a/logic/atrium-logic-jvm/src/main/kotlin/ch/tutteli/atrium/logic/PathAssertions.kt b/logic/atrium-logic-jvm/src/main/kotlin/ch/tutteli/atrium/logic/PathAssertions.kt index 0eed2fa71b..e9da72bd5f 100644 --- a/logic/atrium-logic-jvm/src/main/kotlin/ch/tutteli/atrium/logic/PathAssertions.kt +++ b/logic/atrium-logic-jvm/src/main/kotlin/ch/tutteli/atrium/logic/PathAssertions.kt @@ -25,6 +25,7 @@ interface PathAssertions { fun existsNot(container: AssertionContainer, linkOption: LinkOption? = null): Assertion fun isReadable(container: AssertionContainer): Assertion + fun isNotReadable(container: AssertionContainer): Assertion fun isWritable(container: AssertionContainer): Assertion fun isNotWritable(container: AssertionContainer): Assertion fun isExecutable(container: AssertionContainer): Assertion diff --git a/logic/atrium-logic-jvm/src/main/kotlin/ch/tutteli/atrium/logic/impl/DefaultPathAssertions.kt b/logic/atrium-logic-jvm/src/main/kotlin/ch/tutteli/atrium/logic/impl/DefaultPathAssertions.kt index 1f1f429bf3..20558639d9 100644 --- a/logic/atrium-logic-jvm/src/main/kotlin/ch/tutteli/atrium/logic/impl/DefaultPathAssertions.kt +++ b/logic/atrium-logic-jvm/src/main/kotlin/ch/tutteli/atrium/logic/impl/DefaultPathAssertions.kt @@ -100,6 +100,15 @@ class DefaultPathAssertions : PathAssertions { override fun isReadable(container: AssertionContainer): Assertion = filePermissionAssertion(container, READABLE, AccessMode.READ, DescriptionBasic.IS, shouldHaveAccess = true) + override fun isNotReadable(container: AssertionContainer): Assertion = + filePermissionAssertion( + container, + READABLE, + AccessMode.READ, + DescriptionBasic.IS_NOT, + shouldHaveAccess = false + ) + override fun isWritable(container: AssertionContainer): Assertion = filePermissionAssertion(container, WRITABLE, AccessMode.WRITE, DescriptionBasic.IS, shouldHaveAccess = true) diff --git a/misc/specs/atrium-specs-jvm/src/main/kotlin/ch/tutteli/atrium/specs/integration/PathExpectationsSpec.kt b/misc/specs/atrium-specs-jvm/src/main/kotlin/ch/tutteli/atrium/specs/integration/PathExpectationsSpec.kt index 68d40dfce7..36f004c553 100644 --- a/misc/specs/atrium-specs-jvm/src/main/kotlin/ch/tutteli/atrium/specs/integration/PathExpectationsSpec.kt +++ b/misc/specs/atrium-specs-jvm/src/main/kotlin/ch/tutteli/atrium/specs/integration/PathExpectationsSpec.kt @@ -35,6 +35,7 @@ abstract class PathExpectationsSpec( toEndWith: Fun1, notToEndWith: Fun1, toBeReadable: Fun0, + notToBeReadable: Fun0, toBeWritable: Fun0, notToBeWritable: Fun0, toBeExecutable: Fun0, @@ -71,6 +72,7 @@ abstract class PathExpectationsSpec( toEndWith.forSubjectLess(Paths.get("a")), notToEndWith.forSubjectLess(Paths.get("a")), toBeReadable.forSubjectLess(), + notToBeReadable.forSubjectLess(), toBeWritable.forSubjectLess(), notToBeWritable.forSubjectLess(), toBeExecutable.forSubjectLess(), @@ -540,6 +542,115 @@ abstract class PathExpectationsSpec( } } + describeFun(notToBeReadable) { + val notToBeReadableFun = notToBeReadable.lambda + val expectedMessage = "$isNotDescr: ${READABLE.getDefault()}" + + context("not accessible") { + it("throws an AssertionError for a non-existent path") withAndWithoutSymlink { maybeLink -> + val file = maybeLink.create(tempFolder.tmpDir.resolve("nonExistent")) + expect { + expect(file).notToBeReadableFun() + }.toThrow().message { + toContain( + expectedMessage, + FAILURE_DUE_TO_NO_SUCH_FILE.getDefault() + ) + containsExplanationFor(maybeLink) + } + } + + itPrintsFileAccessProblemDetails( + // because if we cannot access parent then it is still not readable + checkParentHints = false + ) { testFile -> + expect(testFile).notToBeReadableFun() + } + } + + context("POSIX: readable", skip = ifPosixNotSupported) { + it("throws an AssertionError for a file") withAndWithoutSymlink { maybeLink -> + val file = maybeLink.create(tempFolder.newFile("readable")) + + file.whileWithPermissions(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE, OTHERS_EXECUTE) { + expect { + expect(file).notToBeReadableFun() + }.toThrow().message { + toContain(expectedMessage) + } + } + } + + it("throws an AssertionError for a directory") withAndWithoutSymlink { maybeLink -> + val folder = maybeLink.create(tempFolder.newDirectory("readable")) + + folder.whileWithPermissions(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE, OTHERS_EXECUTE) { + expect { + expect(folder).notToBeReadableFun() + }.toThrow().message { + toContain(expectedMessage) + } + } + } + } + + context("POSIX: not readable", skip = ifPosixNotSupported) { + it("does not throw for a file") withAndWithoutSymlink { maybeLink -> + val file = maybeLink.create(tempFolder.newFile("not-readable")) + file.whileWithPermissions(OWNER_WRITE, OWNER_EXECUTE, OTHERS_EXECUTE) { + expect(file).notToBeReadableFun() + } + } + + it("does not throw for a directory") withAndWithoutSymlink { maybeLink -> + val folder = maybeLink.create(tempFolder.newDirectory("not-readable")) + folder.whileWithPermissions(OWNER_WRITE, OWNER_EXECUTE, OTHERS_EXECUTE) { + expect(folder).notToBeReadableFun() + } + } + } + + context("ACL: not writable", skip = ifAclNotSupported) { + it("does not throw for a file") withAndWithoutSymlink { maybeLink -> + val file = maybeLink.create(tempFolder.newFile("not-readable")) + file.whileWithAcl(TestAcls::ownerNoRead) { + expect(file).notToBeReadableFun() + } + } + + it("does not throw for a directory") withAndWithoutSymlink { maybeLink -> + val folder = maybeLink.create(tempFolder.newDirectory("not-readable")) + folder.whileWithAcl(TestAcls::ownerNoRead) { + expect(folder).notToBeReadableFun() + } + } + } + + context("ACL: writable", skip = ifAclNotSupported) { + it("throws an AssertionError for a file") withAndWithoutSymlink { maybeLink -> + val file = maybeLink.create(tempFolder.newFile("readable")) + file.whileWithAcl(TestAcls::all) { + expect { + expect(file).notToBeReadableFun() + }.toThrow().message { + toContain(expectedMessage) + } + } + } + + it("throws an AssertionError for a directory") withAndWithoutSymlink { maybeLink -> + val folder = maybeLink.create(tempFolder.newDirectory("readable")) + folder.whileWithAcl(TestAcls::all) { + expect { + expect(folder).notToBeReadableFun() + }.toThrow().message { + toContain(expectedMessage) + } + } + } + } + } + describeFun(toBeWritable) { val toBeWritableFun = toBeWritable.lambda val expectedMessage = "$isDescr: ${WRITABLE.getDefault()}"