xcodecocoaunit-testingappstore-sandboxxcode-server

NSURL bookmarks failing in unit tests on Xcode Server


In the unit tests for my app, I create an document-scoped NSURL bookmark. These tests have always worked correctly on my machine (and still do), but are now failing when run on an Xcode Server bot. I don't codesign the unit test bundle.

- (void)testBookmarks
{
    // Create testing directory
    NSFileManager *fm = [NSFileManager defaultManager];
    NSString *sourceDir = [fm currentDirectoryPath];
    NSString *testingDir = [sourceDir stringByAppendingPathComponent:@"~testing dir"];

    if ([fm fileExistsAtPath:testingDir]) {
        [fm removeItemAtPath:testingDir error:NULL];
    }

    [fm createDirectoryAtPath:testingDir
  withIntermediateDirectories:NO
                   attributes:nil
                        error:NULL];

    // Create file to create bookmark to
    NSString *bookmarkedFilePath = [testingDir stringByAppendingPathComponent:@"fileToBookmark.txt"];
    [fm createFileAtPath:bookmarkedFilePath
                contents:nil
              attributes:nil];
    NSURL *originalURL = [NSURL fileURLWithPath:bookmarkedFilePath];

    // Create file to create bookmark relative to
    NSString *relativeFilePath = [testingDir stringByAppendingPathComponent:@"relativeToFile.txt"];
    [fm createFileAtPath:relativeFilePath
                contents:nil
              attributes:nil];

    // Create a document-scoped bookmark
    NSError *docScopedError = nil;
    NSURL *relativeToURL = [NSURL fileURLWithPath:relativeFilePath];
    NSData *bookmark = [originalURL bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope
                             includingResourceValuesForKeys:nil
                                              relativeToURL:relativeToURL
                                                      error:&docScopedError];

    // Assert everything went well
    XCTAssertNil(docScopedError, @"Error while creating document-scoped bookmark from URL:\n%@\nrelative to: %@",
                 originalURL, relativeToURL);
    XCTAssertNotNil(bookmark, @"No bookmark created to URL:\n%@\nrelative to: %@",
                 originalURL, relativeToURL);
}

Both assertions fail, and the message that gets logged verifies that both URLs are not nil, and I was able to verify that both files do exist on disk. They are both contained within the Git checkout directory, which the account has full access to. The relativeToUrl points to a file created earlier in the test.

The NSError produced has the following info:

"Error Domain=NSCocoaErrorDomain Code=256 "The file couldn’t be opened." (Item URL disallowed by security policy) UserInfo=0x10691c6d0 {NSDebugDescription=Item URL disallowed by security policy}"

What security policy could it be referring to, and how would I update it? Again, all of this works fine on my local development machine.

Update

I created a demo project, and pushed it to GitHub. Feel free to create your own Xcode Bot that pulls from there to see if you can reproduce. I was able to reproduce with a clean OS X, Xcode, and Server installation.


Solution

  • Here is what I learned (so far) from my discussion with a DTS team member.

    1. Security-scoped bookmarks aren't meant to work with non-sandboxed (and non-codesigned) apps. My unit tests are not signed or sandboxed (and doing so introduced other problems), but they do work, regardless. They may break when the next OS X release comes out, but time will tell there

    2. Apparently, there are certain directories that the security-scoping mechanism considers off-limits. I'm going to start a list below (given the absence of documentation), and update it as I find more exceptions (please do the same).

       /Library
       ~/Library
       /private/var
       /var
       /tmp
      

    My unit tests created a directory inside the checkout location, and create a bookmark to one of those files. Since my local Xcode checkout is in ~/Source Code, it worked fine. Xcode Server, however, checkout out to /Library/Developer/XcodeServer/Caches/..., which is what caused problems.

    I updated my sample GitHub project with my implementation that works around this. In a nutshell, I have the following code in my unit test:

    NSString *envVarTestingDir = [[NSProcessInfo processInfo].environment objectForKey:@"UNIT_TESTING_DIR"];
    NSString *sourceRelativeDir = [[fm currentDirectoryPath] stringByAppendingPathComponent:@"~testing dir"];
    NSString *testingDirPath = [envVarTestingDir length] > 0 ? envVarTestingDir : sourceRelativeDir;
    

    And then, I have a separate scheme for CI builds, which defines the UNIT_TESTING_DIR environment variable to be /Users/Shared/~testing dir, since that is a globally accessible location. That way, I get my local builds to write to my preferred location, but CI builds don't fail. Win/win!

    PS

    Apparently, Xcode Server reassigns the directory returned by NSHomeDirectory() to be /var/_xcsbuildd, which is off-limits.