'Not all those who wander are lost' - J.R.R Tolkien

Windows Impersonation and The Selfish Test

Well, you know, there's automated testing. And then there's 'this' automated testing. And then there is 'that' automated testing. And then there is 'this-and-that' automated testing. And then there are the debates about 'this' vs 'that' vs 'this-and-that'.

Except for the fact that, nobody really tells you what's in it for you.

After all it is just a boatload more work. And frankly, nobody pays for work which exists just to prove Parkinson's Law.

Which brings us to the million dollar question - What's in it for the dear programmer, toiling away to finish a ticket?

Welcome to the programmers day job.

The Programmers day job - 101.

Along came a context...

You are relatively new into a project.

A ticket arrives in a sprint. Blessed by the hands of the product owner. The basic requirement is to be able to run a file copy operation in windows in C# under an account different from the logged in users.

So we immediately raise our hands - Windows Impersonation.

Eh? right ! Slow down a bit, will you. Lets finish with the context, ok?.

Your job is to run that private method under a different user-account. Lets hear it again now for that Windows Impersonation thing.

Welcome to a programmers life, my friend, Its all about the context !

Solutioning

You need a change in code, but, as a developer, you cannot just add the code and hand it over to QA - you need to be sure that your code works. And thus, you have a few problems.

Development

Going by the 'Principle of least interference'. the code change should preferably be limited to the code in that private method, encapsulating the copy operation. And let's just go ahead and do the hard thing first and name the method FileCopy.

Now you start by writing a wrapper library around the basic Windows Impersonation logic from the Microsoft Windows Impersonation documentation. Better still - don't write it. Someone's already written one here. A simple API based on the using statement gives you nice, elegant syntax, and the ability to wrap any block of code in an impersonation section.

using (Impersonation.LogonUser(domain, username, password, logonType))
{
  // do whatever you want as this user.```
}

(Note that there is debate about using using this way - find discussed here, here and here.),

So off you go -

Infrastructure

You have asked your network admin to give you a test-account on the network, and you have configured all settings, and you are all set.

Verification

Now comes the difficult part - How to verify a file-copy was by a specific account ? - when you cannot execute the whole system or the whole code.

You start writing an automated test, because, sometimes, there is just no other viable way to verify your code. Interestingly, however, in this particular context, it is also the most complicated coding involved in this particular ticket. Yet it is effectively unavoidable as all other ways of verifying the code you wrote are not feasible on account of time constraints.

So lets structure the test.

Arrange - For a start, we need to instantiate the target class. Luckily there was master suite of tests which needed this class so class instantiation with all dependencies was a solved problem.

Act - Execute the FileCopy operation. The FileCopy operation reads in the user account information, creates a new impersonation block and executes the file copy operation with that scope.

Assert - Read in the windows file system attributes and compare the file Created By attribute with the configured account for the file copy. They should match and we should be done.

In real life, you still have a few problems.

But at this point lets get down to sample code. Find below the relevant snippets of the program and the test code, well commmented, and hopefully, self explanatory.

The Program Code

First, the program code - the part that goes into production, and the simpler part of the coding bit -

Public class BigBadClassWithLotsOfInitialization
{
    ...
    ... OTHER CODE ELIDED ...
    ...

    //Note: this is a private method. We change it to public/internal 
    //to test. Once done, we switch it back to private before check-in.
    public void FileCopy(string localXmlFilePath, string newPath)
    {
        string domainAndUser = Cfg.GetServiceUserName();
        string domain = domainAndUser.Split('\\')[0];
        string user = domainAndUser.Split('\\')[1];
        string password = Cfg.GetServiceUserPwd();
        LogonType logonType = LogonType.Interactive;

        //Impersonation block.
        using (Impersonation.LogonUser(domain, user, password, logonType))
        {
            // pr-existing code.

            var fileName = Path.GetFileName(localXmlFilePath);
            if (fileName != null)
                newPath = Path.Combine( newPath
                               ,fileName.Replace(".xml", GetTimestamp() + ".xml"));

            Logger.InfoFormat("Moving file {0} to {1}", localXmlFilePath, newPath);

            if (File.Exists(newPath))
            {
                File.Delete(newPath);
            }
            File.Move(localXmlFilePath, newPath);

            // end pr-existing code.
        }
    }
}

The Test Code

This is the part of the code that goes no-where, and doesn't even work after its checked-in. And ironically, the more complex part of the coding, mainly because of the Windows file system know-how involved.

[Test, Ignore]
public void CanWriteFileAsDifferentUserThroughJob()
{
    //Notes | This is a shortcut to test whether elevated permissions 
    //are happening when 'FileCopy' is called. 'FileCopy' is a private 
    //method. So we need to change to public when we need to test. 
    //Change it back when done.
    var job = new BigBadClassWithLotsOfInitialization();

    //Needs to be xml file.
    var fname = Guid.NewGuid().ToString();
    var fileName = fname + ".xml"; 

    var fromPath = Cfg.GetAutomaticPlaceOrdersExportOutputDirectoryPath();
    var fromFile = Path.Combine(fromPath, fileName);

    //using File.Create opens a fileStream and keeps it so, thus 
    //causing file cannot be used errors. this appends if it 
    //exists. It is ok to keep creating without deleting as 
    //MoveFile deletes source file after successful move.
    using (StreamWriter sw = new StreamWriter(fromFile, true))
    {
        sw.Write(fname);
    }

    var toPath = Cfg.GetAutomaticPlaceOrdersRemoteDirectoryPath(440);

    //NOTE 1: this line does not compile when the MoveFile method is 
    //private. so you need to change it before running the test.
    //NOTE 2: when using an account which does not have write access 
    //to the target folder, this line throws an exception. so if the 
    //user account set in config does not have permission, this line 
    //throws an error. If it has permission then write happens ok, but 
    //you encounter the subsequent notes.
    job.FileCopy(fromFile, toPath);

    var toFile = new DirectoryInfo(toPath
                     .GetFiles()
                     .First(e => e.Name.Contains(fname));
    var fs = File.GetAccessControl(toFile.FullName);
    var idRef = fs.GetOwner(typeof(SecurityIdentifier))
                  .Translate(typeof(NTAccount));
    
    //this part is not working. why? Because the only other domain 
    //account i have is also an administrator. when doing stuff with 
    //administrator accounts, windows upgrades the user detail to the 
    //administator group. For details check stackoverflow - https://stackoverflow.com/questions/3370146/how-can-i-find-out-who-created-a-file-in-windows-using-net for more details. So following assert doesnt work.
    //Assert.AreEqual(Cfg.GetServiceUserName(), idRef.Value);

    //This does.
    Assert.AreEqual('BUILTIN\Administrators', 'idRef.Value');
}

Thats all of the significant code. It might take some work to put into a consistently reproducible state - a class library, a test project. some settings, a couple of user accounts on your machine. But most of that should be relatively standard.

The Selfish Test

a.k.a Automated Testing, By Developer, For Developer.

So coming back to the question - what's in it for you, the developer?

so lets consider the test above -

BUT CAN WE AVOID IT? NO.

Simply, because none of the other approaches to verification are feasible options.

So exactly what sort of automated test is this? What benefits does it provide? Whom does it help?

The answer is rather selfish. This test, gives you, the developer -

And thus we have The Selfish Test - Automated verification, by developer, for developer. The above is an example of a Selfish Test. You write this to help yourself. This is the only cheap, easy way to help you to debug as well as to verify the code you wrote. Its lifetime and use is limited to the duration of your development time. Its only purpose is to help you.

So, dear developer, go help yourself.