mirror of
https://github.com/samsonjs/FileOtter.git
synced 2026-03-25 08:25:49 +00:00
341 lines
11 KiB
Swift
341 lines
11 KiB
Swift
//
|
|
// FilePermissionTests.swift
|
|
// FileOtterTests
|
|
//
|
|
// Created by Sami Samhuri on 2025-08-19.
|
|
//
|
|
|
|
@testable import FileOtter
|
|
import XCTest
|
|
|
|
final class FilePermissionTests: XCTestCase {
|
|
var tempDir: URL!
|
|
var testFile: URL!
|
|
var readOnlyFile: URL!
|
|
var executableFile: URL!
|
|
|
|
override func setUpWithError() throws {
|
|
tempDir = URL.temporaryDirectory
|
|
.appendingPathComponent("FilePermissionTests-\(UUID().uuidString)")
|
|
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
|
|
|
|
testFile = tempDir.appendingPathComponent("test.txt")
|
|
try "Test content".write(to: testFile, atomically: true, encoding: .utf8)
|
|
|
|
readOnlyFile = tempDir.appendingPathComponent("readonly.txt")
|
|
try "Read only".write(to: readOnlyFile, atomically: true, encoding: .utf8)
|
|
|
|
executableFile = tempDir.appendingPathComponent("script.sh")
|
|
try "#!/bin/sh\necho hello".write(to: executableFile, atomically: true, encoding: .utf8)
|
|
}
|
|
|
|
override func tearDownWithError() throws {
|
|
if FileManager.default.fileExists(atPath: tempDir.path) {
|
|
try FileManager.default.removeItem(at: tempDir)
|
|
}
|
|
}
|
|
|
|
// MARK: - Basic Permission Tests
|
|
|
|
func testIsReadable() throws {
|
|
// Normal files should be readable
|
|
XCTAssertTrue(File.isReadable(testFile))
|
|
|
|
// System files are generally readable
|
|
XCTAssertTrue(File.isReadable(URL(fileURLWithPath: "/etc/hosts")))
|
|
|
|
// Non-existent files are not readable
|
|
let nonExistent = tempDir.appendingPathComponent("nonexistent")
|
|
XCTAssertFalse(File.isReadable(nonExistent))
|
|
}
|
|
|
|
func testIsWritable() throws {
|
|
// Files we created should be writable
|
|
XCTAssertTrue(File.isWritable(testFile))
|
|
|
|
// System files are generally not writable
|
|
XCTAssertFalse(File.isWritable(URL(fileURLWithPath: "/etc/hosts")))
|
|
|
|
// Non-existent files are not writable
|
|
let nonExistent = tempDir.appendingPathComponent("nonexistent")
|
|
XCTAssertFalse(File.isWritable(nonExistent))
|
|
}
|
|
|
|
func testIsExecutable() throws {
|
|
// Make the script executable
|
|
try FileManager.default.setAttributes(
|
|
[.posixPermissions: 0o755],
|
|
ofItemAtPath: executableFile.path,
|
|
)
|
|
XCTAssertTrue(File.isExecutable(executableFile))
|
|
|
|
// System executables
|
|
XCTAssertTrue(File.isExecutable(URL(fileURLWithPath: "/bin/ls")))
|
|
XCTAssertTrue(File.isExecutable(URL(fileURLWithPath: "/usr/bin/swift")))
|
|
}
|
|
|
|
func testIsExecutableForNonExecutable() throws {
|
|
// Regular text files are not executable
|
|
XCTAssertFalse(File.isExecutable(testFile))
|
|
XCTAssertFalse(File.isExecutable(readOnlyFile))
|
|
|
|
// Non-existent files are not executable
|
|
let nonExistent = tempDir.appendingPathComponent("nonexistent")
|
|
XCTAssertFalse(File.isExecutable(nonExistent))
|
|
}
|
|
|
|
// MARK: - Ownership Tests
|
|
|
|
func testIsOwned() throws {
|
|
// Files we create should be owned by us
|
|
XCTAssertTrue(File.isOwned(testFile))
|
|
XCTAssertTrue(File.isOwned(readOnlyFile))
|
|
XCTAssertTrue(File.isOwned(executableFile))
|
|
|
|
// System files may not be owned by us
|
|
// This depends on the user running the test
|
|
}
|
|
|
|
func testIsGroupOwned() throws {
|
|
// Files we create should be owned by our effective group
|
|
XCTAssertTrue(File.isGroupOwned(testFile))
|
|
XCTAssertTrue(File.isGroupOwned(readOnlyFile))
|
|
XCTAssertTrue(File.isGroupOwned(executableFile))
|
|
}
|
|
|
|
// MARK: - World Permission Tests
|
|
|
|
func testIsWorldReadable() throws {
|
|
// Make file world readable
|
|
try FileManager.default.setAttributes(
|
|
[.posixPermissions: 0o644],
|
|
ofItemAtPath: testFile.path,
|
|
)
|
|
|
|
let perms = File.isWorldReadable(testFile)
|
|
XCTAssertNotNil(perms)
|
|
if let perms {
|
|
XCTAssertEqual(perms & 0o004, 0o004) // Check world read bit
|
|
}
|
|
}
|
|
|
|
func testIsWorldReadableForPrivate() throws {
|
|
// Make file not world readable
|
|
try FileManager.default.setAttributes(
|
|
[.posixPermissions: 0o640],
|
|
ofItemAtPath: readOnlyFile.path,
|
|
)
|
|
|
|
XCTAssertNil(File.isWorldReadable(readOnlyFile))
|
|
}
|
|
|
|
func testIsWorldWritable() throws {
|
|
// Make file world writable (dangerous in practice!)
|
|
try FileManager.default.setAttributes(
|
|
[.posixPermissions: 0o666],
|
|
ofItemAtPath: testFile.path,
|
|
)
|
|
|
|
let perms = File.isWorldWritable(testFile)
|
|
XCTAssertNotNil(perms)
|
|
if let perms {
|
|
XCTAssertEqual(perms & 0o002, 0o002) // Check world write bit
|
|
}
|
|
}
|
|
|
|
func testIsWorldWritableForProtected() throws {
|
|
// Make file not world writable
|
|
try FileManager.default.setAttributes(
|
|
[.posixPermissions: 0o644],
|
|
ofItemAtPath: readOnlyFile.path,
|
|
)
|
|
|
|
XCTAssertNil(File.isWorldWritable(readOnlyFile))
|
|
}
|
|
|
|
// MARK: - Special Bit Tests
|
|
|
|
func testIsSetuid() throws {
|
|
// Setuid is rarely used on regular files
|
|
// Most files should not have setuid
|
|
XCTAssertFalse(File.isSetuid(testFile))
|
|
|
|
// /usr/bin/sudo typically has setuid (if it exists)
|
|
let sudo = URL(fileURLWithPath: "/usr/bin/sudo")
|
|
if FileManager.default.fileExists(atPath: sudo.path) {
|
|
// This might be true on some systems
|
|
_ = File.isSetuid(sudo)
|
|
}
|
|
}
|
|
|
|
func testIsSetgid() throws {
|
|
// Setgid is rarely used on regular files
|
|
XCTAssertFalse(File.isSetgid(testFile))
|
|
}
|
|
|
|
func testIsSticky() throws {
|
|
// Sticky bit is typically set on /tmp
|
|
let tmpDir = URL(fileURLWithPath: "/tmp")
|
|
if FileManager.default.fileExists(atPath: tmpDir.path) {
|
|
// /tmp usually has sticky bit
|
|
_ = File.isSticky(tmpDir)
|
|
}
|
|
|
|
// Regular files should not have sticky bit
|
|
XCTAssertFalse(File.isSticky(testFile))
|
|
}
|
|
|
|
// MARK: - chmod Tests
|
|
|
|
func testChmod() throws {
|
|
// Change file to read-only
|
|
try File.chmod(testFile, permissions: 0o444)
|
|
|
|
// Verify permissions changed
|
|
let stat = try File.fileStatus(testFile)
|
|
let perms = stat.mode & 0o777
|
|
XCTAssertEqual(perms, 0o444)
|
|
|
|
// File should still be readable but not writable
|
|
XCTAssertTrue(File.isReadable(testFile))
|
|
XCTAssertFalse(File.isWritable(testFile))
|
|
|
|
// Change back to read-write
|
|
try File.chmod(testFile, permissions: 0o644)
|
|
let stat2 = try File.fileStatus(testFile)
|
|
let perms2 = stat2.mode & 0o777
|
|
XCTAssertEqual(perms2, 0o644)
|
|
}
|
|
|
|
func testChmodThrowsForNonExistent() throws {
|
|
let nonExistent = tempDir.appendingPathComponent("does-not-exist.txt")
|
|
XCTAssertThrowsError(try File.chmod(nonExistent, permissions: 0o644))
|
|
}
|
|
|
|
func testLchmod() throws {
|
|
// Create a symlink
|
|
let link = tempDir.appendingPathComponent("test-link")
|
|
try File.symlink(source: testFile, destination: link)
|
|
|
|
// On macOS, lchmod is a no-op for symlinks
|
|
// This should not throw but also won't change symlink permissions
|
|
try File.lchmod(link, permissions: 0o777)
|
|
|
|
// The target file permissions should not be affected
|
|
let targetStat = try File.fileStatus(testFile)
|
|
let targetPerms = targetStat.mode & 0o777
|
|
XCTAssertNotEqual(targetPerms, 0o777)
|
|
}
|
|
|
|
func testInstanceChmod() throws {
|
|
// Skip for now as it requires File instance implementation
|
|
throw XCTSkip("Instance methods not yet implemented")
|
|
}
|
|
|
|
// MARK: - chown Tests
|
|
|
|
func testChown() throws {
|
|
// Get current ownership
|
|
let stat = try File.fileStatus(testFile)
|
|
let currentUid = stat.uid
|
|
let currentGid = stat.gid
|
|
|
|
// Try to set to same owner/group (should always succeed)
|
|
try File.chown(testFile, owner: currentUid, group: currentGid)
|
|
|
|
// Verify ownership unchanged
|
|
let newStat = try File.fileStatus(testFile)
|
|
XCTAssertEqual(newStat.uid, currentUid)
|
|
XCTAssertEqual(newStat.gid, currentGid)
|
|
|
|
// Note: Changing to different owner usually requires root privileges
|
|
// So we can't test that in normal unit tests
|
|
}
|
|
|
|
func testChownWithNilValues() throws {
|
|
// Get current ownership
|
|
let stat = try File.fileStatus(testFile)
|
|
let currentUid = stat.uid
|
|
let currentGid = stat.gid
|
|
|
|
// Change only owner (group stays same)
|
|
try File.chown(testFile, owner: currentUid, group: nil)
|
|
let stat1 = try File.fileStatus(testFile)
|
|
XCTAssertEqual(stat1.uid, currentUid)
|
|
XCTAssertEqual(stat1.gid, currentGid)
|
|
|
|
// Change only group (owner stays same)
|
|
try File.chown(testFile, owner: nil, group: currentGid)
|
|
let stat2 = try File.fileStatus(testFile)
|
|
XCTAssertEqual(stat2.uid, currentUid)
|
|
XCTAssertEqual(stat2.gid, currentGid)
|
|
|
|
// Change neither (no-op)
|
|
try File.chown(testFile, owner: nil, group: nil)
|
|
let stat3 = try File.fileStatus(testFile)
|
|
XCTAssertEqual(stat3.uid, currentUid)
|
|
XCTAssertEqual(stat3.gid, currentGid)
|
|
}
|
|
|
|
func testLchown() throws {
|
|
// Create a symlink
|
|
let link = tempDir.appendingPathComponent("owner-link")
|
|
try File.symlink(source: testFile, destination: link)
|
|
|
|
// Get current ownership of symlink
|
|
let linkStat = try File.linkStatus(link)
|
|
let currentUid = linkStat.uid
|
|
let currentGid = linkStat.gid
|
|
|
|
// Try to set to same owner/group (should always succeed)
|
|
try File.lchown(link, owner: currentUid, group: currentGid)
|
|
|
|
// Verify symlink ownership unchanged
|
|
let newLinkStat = try File.linkStatus(link)
|
|
XCTAssertEqual(newLinkStat.uid, currentUid)
|
|
XCTAssertEqual(newLinkStat.gid, currentGid)
|
|
|
|
// Target file ownership should not be affected
|
|
let targetStat = try File.fileStatus(testFile)
|
|
XCTAssertEqual(targetStat.uid, currentUid)
|
|
XCTAssertEqual(targetStat.gid, currentGid)
|
|
}
|
|
|
|
func testInstanceChown() throws {
|
|
// Skip for now as it requires File instance implementation
|
|
throw XCTSkip("Instance methods not yet implemented")
|
|
}
|
|
|
|
// MARK: - umask Tests
|
|
|
|
func testUmask() throws {
|
|
// Get current umask
|
|
let currentMask = File.umask()
|
|
|
|
// Umask should be a reasonable value (typically 0o022 or 0o002)
|
|
XCTAssertGreaterThanOrEqual(currentMask, 0)
|
|
XCTAssertLessThan(currentMask, 0o777)
|
|
}
|
|
|
|
func testUmaskSet() throws {
|
|
// Get current umask
|
|
let originalMask = File.umask()
|
|
|
|
// Set new umask
|
|
let newMask = 0o027
|
|
let returnedMask = File.umask(newMask)
|
|
|
|
// Returned value should be the old mask
|
|
XCTAssertEqual(returnedMask, originalMask)
|
|
|
|
// Current mask should be the new value
|
|
let currentMask = File.umask()
|
|
XCTAssertEqual(currentMask, newMask)
|
|
|
|
// Restore original umask
|
|
_ = File.umask(originalMask)
|
|
|
|
// Verify restoration
|
|
XCTAssertEqual(File.umask(), originalMask)
|
|
}
|
|
}
|