Test.php #1

  • //
  • guest/
  • perforce_software/
  • chronicle/
  • main/
  • tests/
  • phpunit/
  • P4Cms/
  • Categorization/
  • Test.php
  • View
  • Commits
  • Open Download .zip Download (85 KB)
<?php
/**
 * Test methods for the categorization packages.
 *
 * @copyright   2011 Perforce Software. All rights reserved.
 * @license     Please see LICENSE.txt in top-level folder of this distribution.
 * @version     <release>/<patch>
 * @todo        add adapter testing
 */
class P4Cms_Categorization_Test extends TestCase
{
    /**
     * Create dummy model records for testing.
     */
    public function setUp()
    {
        parent::setUp();

        // create a P4Cms_Record adapter
        $adapter = new P4Cms_Record_Adapter;
        $adapter->setConnection($this->p4)
                ->setBasePath("//depot");
        P4Cms_Record::setDefaultAdapter($adapter);
    }

    /**
     * Remove dummy model records.
     */
    public function tearDown()
    {
        // remove the P4Cms_Record adapter?
        P4Cms_Record::clearDefaultAdapter();

        parent::tearDown();
    }

    /**
     * Test idToFilespec.
     */
    public function testIdToFilespec()
    {
        $index = P4Cms_Categorization_Dir::CATEGORY_FILENAME;
        $tests = array(
            array(
                'label'          => __LINE__ .': category null',
                'id'             => null,
                'error'          => "Cannot get filespec for an empty id.",
                'dirFilespec'    => "//depot/folders/$index",
                'friendFilespec' => "//depot/friends/$index",
                'roundTripId'    => '',
            ),
            array(
                'label'          => __LINE__ .': category empty',
                'id'             => '',
                'error'          => "Cannot get filespec for an empty id.",
                'dirFilespec'    => "//depot/folders/$index",
                'friendFilespec' => "//depot/friends/$index",
                'roundTripId'    => '',
            ),
            array(
                'label'          => __LINE__ .': category number',
                'id'             => 123,
                'error'          => null,
                'dirFilespec'    => "//depot/folders/123/$index",
                'friendFilespec' => "//depot/friends/123/$index",
                'roundTripId'    => '123',
            ),
            array(
                'label'          => __LINE__ .': category numeric',
                'id'             => '123',
                'error'          => null,
                'dirFilespec'    => "//depot/folders/123/$index",
                'friendFilespec' => "//depot/friends/123/$index",
                'roundTripId'    => '123',
            ),
            array(
                'label'          => __LINE__ .': category alphanumeric',
                'id'             => 'abc123',
                'error'          => null,
                'dirFilespec'    => "//depot/folders/abc123/$index",
                'friendFilespec' => "//depot/friends/abc123/$index",
                'roundTripId'    => 'abc123',
            ),
            array(
                'label'          => __LINE__ .': category *',
                'id'             => '*',
                'error'          => null,
                'dirFilespec'    => "//depot/folders/*/$index",
                'friendFilespec' => "//depot/friends/*/$index",
                'roundTripId'    => '*',
            ),
            array(
                'label'          => __LINE__ .': category %%',
                'id'             => '%%',
                'error'          => null,
                'dirFilespec'    => "//depot/folders/%%/$index",
                'friendFilespec' => "//depot/friends/%%/$index",
                'roundTripId'    => '%%',
            ),
        );

        foreach ($tests as $test) {
            $label = $test['label'];
            $filespec = null;

            try {
                $filespec = P4Cms_Categorization_Dir::idToFilespec($test['id']);
                if (isset($test['error'])) {
                    $this->fail("$label - unexpected dir success.");
                }
            } catch (PHPUnit_Framework_AssertionFailedError $e) {
                $this->fail($e->getMessage());
            } catch (InvalidArgumentException $e) {
                if (isset($test['error'])) {
                    $this->assertSame(
                        $test['error'],
                        $e->getMessage(),
                        "$label - expected dir error."
                    );
                } else {
                    $this->fail("$label - unexpected dir exception:". $e->getMessage());
                }
            }

            if (!isset($test['error'])) {
                $this->assertSame(
                    $test['dirFilespec'],
                    $filespec,
                    "$label - expected dir filespec"
                );
                $roundTripId = P4Cms_Categorization_Dir::depotFileToId($filespec);
                $this->assertSame(
                    $test['roundTripId'],
                    $roundTripId,
                    "$label - expected dir roundTripId"
                );
            }

            try {
                $filespec = P4Cms_Categorization_Friend::idToFilespec($test['id']);
                if (isset($test['error'])) {
                    $this->fail("$label - unexpected friend success.");
                }
            } catch (PHPUnit_Framework_AssertionFailedError $e) {
                $this->fail($e->getMessage());
            } catch (InvalidArgumentException $e) {
                if (isset($test['error'])) {
                    $this->assertSame(
                        $test['error'],
                        $e->getMessage(),
                        "$label - expected friend error."
                    );
                } else {
                    $this->fail("$label - unexpected friend exception:". $e->getMessage());
                }
            }

            if (!isset($test['error'])) {
                $this->assertSame(
                    $test['friendFilespec'],
                    $filespec,
                    "$label - expected friend filespec"
                );
                $roundTripId = P4Cms_Categorization_Friend::depotFileToId($filespec);
                $this->assertSame(
                    $test['roundTripId'],
                    $roundTripId,
                    "$label - expected dir roundTripId"
                );
            }
        }
    }

    /**
     * Test nestingAllowed.
     */
    public function testNestingAllowed()
    {
        $this->assertTrue(P4Cms_Categorization_Dir::isNestingAllowed(), 'dir should allow nesting');
        $this->assertFalse(P4Cms_Categorization_Friend::isNestingAllowed(), 'friend should not allow nesting');
    }

    /**
     * Test checkNestability.
     */
    public function testCheckNestability()
    {
        // Dir should not cause an exception
        try {
            P4Cms_Categorization_Dir::callProtectedStaticFunc('_checkNestability');
        } catch (Exception $e) {
            $this->fail(
                'Unexpected exception for Dir ('
                . get_class($e) .') :'. $e->getMessage()
            );
        }

        // Friend should cause an exception.
        try {
            P4Cms_Categorization_Friend::callProtectedStaticFunc('_checkNestability');
            $this->fail("Unexpected success for Friend");
        } catch (PHPUnit_Framework_AssertionFailedError $e) {
            $this->fail($e->getMessage());
        } catch (P4Cms_Categorization_Exception $e) {
            $this->assertSame(
                "This category does not permit nesting.",
                $e->getMessage(),
                "Expected error for Friend"
            );
        } catch (Exception $e) {
            $this->fail(
                'Unexpected exception for Friend ('
                . get_class($e) .') :'. $e->getMessage()
            );
        }
    }

    /**
     * Test existence of categories and their entries.
     */
    public function testExistence()
    {
        $index = P4Cms_Categorization_Dir::CATEGORY_FILENAME;
        $this->assertFalse(
            P4Cms_Categorization_Dir::exists('adir'),
            'adir should not exist.'
        );

        $category = new P4Cms_Categorization_Dir;
        $category->setId('adir');
        $this->assertFalse(
            $category->hasEntry('entry'),
            'adir/entry should not exist.'
        );

        $this->assertFalse(
            P4Cms_Categorization_Friend::exists('afriend'),
            'afriend should not exist.'
        );

        $encodedId = P4Cms_Categorization_Dir::encodeEntryId('entry');
        $file = new P4_File;
        $file->setFilespec("//depot/folders/adir/$encodedId")
             ->add()
             ->setLocalContents('')
             ->submit('added adir/entry');
        $category = new P4Cms_Categorization_Dir;
        $category->setId('adir');
        $this->assertTrue(
            $category->hasEntry('entry'),
            'adir/entry should now exist.'
        );
        $this->assertFalse(
            P4Cms_Categorization_Dir::exists('adir'),
            'adir should still not exist.'
        );

        $file2 = new P4_File;
        $file2->setFilespec("//depot/folders/adir/$index")
              ->add()
              ->setLocalContents('')
              ->submit('added adir metadata');
        $this->assertTrue(
            P4Cms_Categorization_Dir::exists('adir'),
            'adir should now exist.'
        );

        $file3 = new P4_File;
        $file3->setFilespec("//depot/friends/afriend/$index")
              ->add()
              ->setLocalContents('')
              ->submit('added afriend');
        $this->assertTrue(
            P4Cms_Categorization_Friend::exists('afriend'),
            'afriend should now exist.'
        );
    }

    /**
     * Test setId.
     */
    public function testSetId()
    {
        $index = P4Cms_Categorization_Dir::CATEGORY_FILENAME;
        $tests = array(
            array(
                'label'     => __LINE__ .': null',
                'id'        => null,
                'class'     => 'P4Cms_Categorization_Dir',
                'error'     => null,
            ),
            array(
                'label'     => __LINE__ .': trailing underscore',
                'id'        => 'a_',
                'class'     => 'P4Cms_Categorization_Dir',
                'error'     => null,
            ),
            array(
                'label'     => __LINE__ .': leading period',
                'id'        => '.',
                'class'     => 'P4Cms_Categorization_Dir',
                'error'     => array(
                    'InvalidArgumentException'
                        => 'Cannot set id: Leading periods are not permitted in category ids.',
                ),
            ),
            array(
                'label'     => __LINE__ .': leading dash',
                'id'        => '-',
                'class'     => 'P4Cms_Categorization_Dir',
                'error'     => array(
                    'InvalidArgumentException'
                        => 'Cannot set id: Leading dashes are not permitted in category ids.',
                ),
            ),
            array(
                'label'     => __LINE__ .': trailing underscore',
                'id'        => 'a_',
                'class'     => 'P4Cms_Categorization_Dir',
                'error'     => null,
            ),
            array(
                'label'     => __LINE__ .': reserved index',
                'id'        => P4Cms_Categorization_CategoryAbstract::CATEGORY_FILENAME,
                'class'     => 'P4Cms_Categorization_Dir',
                'error'     => array(
                    'InvalidArgumentException' => 'Cannot set id: Id is reserved for internal use.',
                ),
            ),
            array(
                'label'     => __LINE__ .': / where nesting allowed',
                'id'        => 'a/b',
                'class'     => 'P4Cms_Categorization_Dir',
                'error'     => null,
            ),
            array(
                'label'     => __LINE__ .': / where nesting not allowed',
                'id'        => 'a/b',
                'class'     => 'P4Cms_Categorization_Friend',
                'error'     => array(
                    'P4Cms_Categorization_Exception' => 'This category does not permit nesting.',
                ),
            ),
        );

        foreach ($tests as $test) {
            $label = $test['label'];
            $object = null;
            try {
                $object = new $test['class'];
                $object->setId($test['id']);
                if (isset($test['error'])) {
                    $this->fail("$label - unexpected success");
                }
            } catch (PHPUnit_Framework_AssertionFailedError $e) {
                $this->fail($e->getMessage());
            } catch (Exception $e) {
                if (isset($test['error'])) {
                    $error = each($test['error']);
                    $this->assertSame(
                        $error[0],
                        get_class($e),
                        "$label - Expected exception. Got (". get_class($e) .') :'. $e->getMessage()
                    );
                    $this->assertSame(
                        $error[1],
                        $e->getMessage(),
                        "$label - Expected error message."
                    );
                } else {
                    $this->fail(
                        "$label - Unexpected exception ("
                        . get_class($e) .') :'. $e->getMessage()
                    );
                }
            }

            if (!isset($test['error'])) {
                $this->assertSame(
                    $test['id'],
                    $object->getId(),
                    "$label - Expected id."
                );
            }
        }
    }

    /**
     * Test remove.
     */
    public function testRemove()
    {
        // prepare file layout
        $index = P4Cms_Categorization_Dir::CATEGORY_FILENAME;
        $file1 = new P4_File;
        $file1->setFilespec("//depot/folders/adir/$index")
              ->add()
              ->setLocalContents('')
              ->submit('added adir metadata');
        $this->assertTrue(
            P4Cms_Categorization_Dir::exists('adir'),
            'adir should exist.'
        );

        $encodedId = P4Cms_Categorization_Dir::encodeEntryId('entry');
        $file2 = new P4_File;
        $file2->setFilespec("//depot/folders/adir/$encodedId")
              ->add()
              ->setLocalContents('')
              ->submit('added adir/entry');
        $category = new P4Cms_Categorization_Dir;
        $category->setId('adir');
        $this->assertTrue(
            $category->hasEntry('entry'),
            'adir/entry should exist.'
        );

        $file3 = new P4_File;
        $file3->setFilespec("//depot/folders/bdir/$index")
              ->add()
              ->setLocalContents('')
              ->submit('added bdir metadata');
        $this->assertTrue(
            P4Cms_Categorization_Dir::exists('bdir'),
            'bdir should exist.'
        );

        $file4 = new P4_File;
        $file4->setFilespec("//depot/folders/bdir/$encodedId")
              ->add()
              ->setLocalContents('')
              ->submit('added bdir/entry');
        $category = new P4Cms_Categorization_Dir;
        $category->setId('bdir');
        $this->assertTrue(
            $category->hasEntry('entry'),
            'bdir/entry should exist.'
        );

        $file5 = new P4_File;
        $file5->setFilespec("//depot/folders/bdir/subdir/$index")
              ->add()
              ->setLocalContents('')
              ->submit('added bdir/subdir metadata');
        $this->assertTrue(
            P4Cms_Categorization_Dir::exists('bdir/subdir'),
            'bdir/subdir should exist.'
        );

        $file6 = new P4_File;
        $file6->setFilespec("//depot/folders/bdir/subdir2/$index")
              ->add()
              ->setLocalContents('')
              ->submit('added bdir/subdir2 metadata');
        $this->assertTrue(
            P4Cms_Categorization_Dir::exists('bdir/subdir2'),
            'bdir/subdir2 should exist.'
        );

        // try to delete a subdir
        $path = 'bdir/subdir';
        try {
            P4Cms_Categorization_Dir::remove($path);
        } catch (Exception $e) {
            $this->fail(
                "Unexpected exception removing '$path' ("
                . get_class($e) .') :'. $e->getMessage()
            );
        }
        $this->assertTrue(
            P4Cms_Categorization_Dir::exists('bdir'),
            'bdir should exist.'
        );
        $this->assertFalse(
            P4Cms_Categorization_Dir::exists('bdir/subdir'),
            'bdir/subdir should no longer exist.'
        );
        $this->assertTrue(
            P4Cms_Categorization_Dir::exists('bdir/subdir2'),
            'bdir/subdir2 should still exist.'
        );

        // try to delete adir
        $path = 'adir';
        try {
            P4Cms_Categorization_Dir::remove($path);
        } catch (Exception $e) {
            $this->fail(
                "Unexpected exception removing '$path' ("
                . get_class($e) .') :'. $e->getMessage()
            );
        }
        $this->assertFalse(
            P4Cms_Categorization_Dir::exists('adir'),
            'adir should no longer exist.'
        );
        $category = new P4Cms_Categorization_Dir;
        $category->setId('adir');
        $this->assertFalse(
            $category->hasEntry('entry'),
            'adir/entry should no longer exist.'
        );

        // try to delete nested path
        $path = 'bdir';
        try {
            P4Cms_Categorization_Dir::remove($path);
        } catch (Exception $e) {
            $this->fail(
                "Unexpected exception removing '$path' ("
                . get_class($e) .') :'. $e->getMessage()
            );
        }
        $this->assertFalse(
            P4Cms_Categorization_Dir::exists('bdir'),
            'bdir should no longer exist.'
        );
        $category = new P4Cms_Categorization_Dir;
        $category->setId('bdir');
        $this->assertFalse(
            $category->hasEntry('entry'),
            'bdir/entry should no longer exist.'
        );
        $this->assertFalse(
            P4Cms_Categorization_Dir::exists('bdir/subdir2'),
            'bdir/subdir2 should no longer exist.'
        );

        // try to delete again
        $path = 'bdir';
        try {
            P4Cms_Categorization_Dir::remove($path);
            $this->fail("Unexpected success removing already removed path '$path'");
        } catch (PHPUnit_Framework_AssertionFailedError $e) {
            $this->fail($e->getMessage());
        } catch (P4Cms_Categorization_Exception $e) {
            $this->assertSame(
                'Cannot delete category. Category does not exist.',
                $e->getMessage(),
                "Expected error removing already removed path '$path'"
            );
        }
    }

    /**
     * Test whether a category exists.
     */
    public function testExists()
    {
        $index = P4Cms_Categorization_Dir::CATEGORY_FILENAME;
        $this->assertFalse(
            P4Cms_Categorization_Dir::exists('dne'),
            'dne in root dir should not exist.'
        );

        // create an entry in each hierarchy for existence checks
        $file = new P4_File;
        $file->setFilespec("//depot/folders/newdir/$index")
             ->add()
             ->setLocalContents('')
             ->submit('added newdir');
        $file2 = new P4_File;
        $file2->setFilespec('//depot/folders/newdir/newentry')
              ->add()
              ->setLocalContents('')
              ->submit('added newdir/newentry');
        $file3 = new P4_File;
        $file3->setFilespec('//depot/friends/newfriend')
              ->add()
              ->setLocalContents('')
              ->submit('added newfriend');

        // test updated existance
        $this->assertTrue(
            P4Cms_Categorization_Dir::exists('newdir'),
            'newdir should exist.'
        );
        $this->assertFalse(
            P4Cms_Categorization_Dir::exists('newdir/'),
            'newdir/ should not exist.'
        );

        // category content should NOT be identified with exists.
        $this->assertFalse(
            P4Cms_Categorization_Dir::exists('newdir/newentry'),
            'newdir/newentry should exist.'
        );
        $this->assertFalse(
            P4Cms_Categorization_Friend::exists('newfriend'),
            'newfriend should exist.'
        );
    }

    /**
     * Test hasChildren and getChildren
     */
    public function testHasChildrenGetChildren()
    {
        $dir = new P4Cms_Categorization_Dir;

        $this->assertFalse(
            $dir->setId('nokids')->hasChildren(),
            'nokids should have no children'
        );
        $this->assertFalse(
            $dir->setId('somekids')->hasChildren(),
            'somekids should have no children yet'
        );
        $this->assertFalse(
            $dir->setId('somekids/anotherkid')->hasChildren(),
            'somekids/anotherkid should have no children'
        );

        // verify that friends do not have children
        $friend = new P4Cms_Categorization_Friend;
        try {
            $result = $friend->setId('bob')->hasChildren();
            $this->fail('Unexpected success with hasChildren on friend.');
        } catch (PHPUnit_Framework_AssertionFailedError $e) {
            $this->fail($e->getMessage());
        } catch (P4Cms_Categorization_Exception $e) {
            $this->assertSame(
                'This category does not permit nesting.',
                $e->getMessage(),
                'Expected error with hasChildren on friend.'
            );
        }

        try {
            $children = $friend->getChildren();
            $this->fail('Unexpected success with getChildren on friend.');
        } catch (PHPUnit_Framework_AssertionFailedError $e) {
            $this->fail($e->getMessage());
        } catch (P4Cms_Categorization_Exception $e) {
            $this->assertSame(
                'This category does not permit nesting.',
                $e->getMessage(),
                'Expected error with getChildren on friend.'
            );
        }

        // populate category
        $index = P4Cms_Categorization_Dir::CATEGORY_FILENAME;
        $file = new P4_File;
        $file->setFilespec("//depot/folders/somekids/$index")
             ->add()
             ->setLocalContents('')
             ->submit("added somekids/$index");
        $file2 = new P4_File;
        $file2->setFilespec("//depot/folders/somekids/anotherkid/$index")
              ->add()
              ->setLocalContents('')
              ->submit("added somekids/anotherkid/$index");
        $file3 = new P4_File;
        $file3->setFilespec("//depot/folders/somekids/anotherkid/deeper/$index")
              ->add()
              ->setLocalContents('')
              ->submit('added somekids/anotherkid/deeper_index');
        $file4 = new P4_File;
        $file4->setFilespec("//depot/folders/nokids/$index")
              ->add()
              ->setLocalContents('')
              ->submit("added nokids/$index");
        $file5 = new P4_File;
        $file5->setFilespec("//depot/friends/bob/$index")
              ->add()
              ->setLocalContents('')
              ->submit("added bob/$index");
        $file5 = new P4_File;
        $file5->setFilespec('//depot/friends/bob/invalid')
              ->add()
              ->setLocalContents('')
              ->submit('added bob/invalid');

        // verify that friends still do not have children
        $friend = new P4Cms_Categorization_Friend;
        try {
            $result = $friend->setId('bob')->hasChildren();
            $this->fail('Unexpected success with hasChildren on friend #2.');
        } catch (PHPUnit_Framework_AssertionFailedError $e) {
            $this->fail($e->getMessage());
        } catch (P4Cms_Categorization_Exception $e) {
            $this->assertSame(
                'This category does not permit nesting.',
                $e->getMessage(),
                'Expected error with hasChildren on friend #2.'
            );
        }

        try {
            $children = $friend->getChildren();
            $this->fail('Unexpected success with getChildren on friend #2.');
        } catch (PHPUnit_Framework_AssertionFailedError $e) {
            $this->fail($e->getMessage());
        } catch (P4Cms_Categorization_Exception $e) {
            $this->assertSame(
                'This category does not permit nesting.',
                $e->getMessage(),
                'Expected error with getChildren on friend #2.'
            );
        }

        // get child categories non-recursively
        $ids = array();
        $categories = $dir->setId('somekids')->getChildren();
        foreach ($categories as $category) {
            $ids[] = $category->getId();
        }
        $this->assertSame(
            array('somekids/anotherkid'),
            $ids,
            'Expected children in somekids, non-recursive'
        );

        // get child categories recursively
        $ids = array();
        $categories = $dir->setId('somekids')->getChildren(true);
        foreach ($categories as $category) {
            $ids[] = $category->getId();
        }
        $this->assertSame(
            array(
                'somekids/anotherkid',
                'somekids/anotherkid/deeper',
            ),
            $ids,
            'Expected children in somekids, recursive'
        );

        $this->assertFalse(
            $dir->setId('nokids')->hasChildren(),
            'nokids should have no children'
        );
        $this->assertTrue(
            $dir->setId('somekids')->hasChildren(),
            'somekids should have children now'
        );
        $this->assertTrue(
            $dir->setId('somekids/anotherkid')->hasChildren(),
            'somekids/anotherkid should have children'
        );
        $this->assertFalse(
            $dir->setId('somekids/anotherkid/deeper')->hasChildren(),
            'somekids/anotherkid/deeper should have no children'
        );
    }

    /**
     * Test hasParent() and getParent().
     */
    public function testHasParentGetParent()
    {
        // test with no id.
        $dir = new P4Cms_Categorization_Dir;
        $result = $dir->hasParent();
        $this->assertFalse($dir->hasParent(), 'Category with no id should have no parent.');

        try {
            $dir->getParent();
            $this->fail('Unexpected success with getParent on root category.');
        } catch (PHPUnit_Framework_AssertionFailedError $e) {
            $this->fail($e->getMessage());
        } catch (P4Cms_Categorization_Exception $e) {
            $this->assertSame(
                'Cannot get parent. This category has no parent.',
                $e->getMessage(),
                'Expected error with getParent on root category.'
            );
        }

        // test with a variety of ids that do not (yet) exist.
        $testCategories = array(
            'category', 'category/sub'
        );

        foreach ($testCategories as $category) {
            $dir = new P4Cms_Categorization_Dir;
            $this->assertFalse(
                $dir->setId($category)->hasParent(),
                "Category '$category' should not have a parent."
            );
        }

        $index = P4Cms_Categorization_Dir::CATEGORY_FILENAME;
        $file = new P4_File;
        $file->setFilespec("//depot/folders/category/$index")
             ->add()
             ->setLocalContents('')
             ->submit("added category/$index");
        $file2 = new P4_File;
        $file2->setFilespec("//depot/folders/category/sub/$index")
              ->add()
              ->setLocalContents('')
              ->submit("added category/sub/$index");

        // test with ids that should now exist.
        $testCategories = array(
            'category/sub'  => 'category',
            'category/sub2' => 'category',
        );
        foreach ($testCategories as $category => $expected) {
            $dir = new P4Cms_Categorization_Dir;
            $this->assertTrue(
                $dir->setId($category)->hasParent(),
                "Expected '$category' to have a parent."
            );
            $parent = $dir->getParent();
            $this->assertSame($expected, $parent->getId(), "Expected parent id for '$category'");
        }
    }

    /**
     * Test create and delete.
     */
    public function testCreateDelete()
    {
        $query = P4_File_Query::create()->addFilespec('//depot/folders/...');
        $files = P4_File::fetchAll($query);
        $this->assertEquals(0, count($files), 'Expect no categories at outset.');

        $tests = array(
            array(
                'label'     => __LINE__ .': null',
                'class'     => 'P4Cms_Categorization_Dir',
                'values'    => array('id' => null),
                'error'     => array(
                    'P4Cms_Categorization_Exception' => 'Cannot save; category id is not set.'
                ),
            ),
            array(
                'label'     => __LINE__ .': empty string',
                'class'     => 'P4Cms_Categorization_Dir',
                'values'    => array('id' => null),
                'error'     => array(
                    'P4Cms_Categorization_Exception' => 'Cannot save; category id is not set.'
                ),
            ),
            array(
                'label'     => __LINE__ .': id with underscore',
                'class'     => 'P4Cms_Categorization_Dir',
                'values'    => array(
                    'id'            => 'a_b',
                    'title'         => 'a title',
                    'description'   => 'a description',
                ),
                'error'     => null,
            ),
            array(
                'label'     => __LINE__ .': id with leading period',
                'class'     => 'P4Cms_Categorization_Dir',
                'values'    => array('id' => '.ab'),
                'error'     => array(
                    'InvalidArgumentException'
                        => 'Cannot set id: Leading periods are not permitted in category ids.',
                ),
            ),
            array(
                'label'     => __LINE__ .': id matching CATEGORY_FILENAME',
                'class'     => 'P4Cms_Categorization_Dir',
                'values'    => array('id' => P4Cms_Categorization_CategoryAbstract::CATEGORY_FILENAME),
                'error'     => array(
                    'InvalidArgumentException' => 'Cannot set id: Id is reserved for internal use.',
                ),
            ),
            array(
                'label'     => __LINE__ .': invalid nesting',
                'class'     => 'P4Cms_Categorization_Friend',
                'values'    => array('id' => 'a/b'),
                'error'     => array(
                    'P4Cms_Categorization_Exception' => 'This category does not permit nesting.',
                ),
            ),
            array(
                'label'     => __LINE__ .': non-existant path',
                'class'     => 'P4Cms_Categorization_Dir',
                'values'    => array('id' => 'a/b'),
                'error'     => array(
                    'InvalidArgumentException' => 'Cannot create new category;'
                        . ' category ancestry does not exist.',
                ),
            ),
            array(
                'label'     => __LINE__ .': success a',
                'class'     => 'P4Cms_Categorization_Dir',
                'values'    => array(
                    'id'            => 'a',
                    'title'         => 'a title',
                    'description'   => 'a description',
                ),
                'error'     => null,
            ),
            array(
                'label'     => __LINE__ .': success a/b',
                'class'     => 'P4Cms_Categorization_Dir',
                'values'    => array(
                    'id'            => 'a/b',
                    'title'         => 'a/b title',
                    'description'   => 'a/b description',
                ),
                'error'     => null,
            ),
            array(
                'label'     => __LINE__ .': success a/b/c',
                'class'     => 'P4Cms_Categorization_Dir',
                'values'    => array(
                    'id'            => 'a/b/c',
                    'title'         => 'a/b/c title',
                    'description'   => 'a/b/c description',
                ),
                'error'     => null,
            ),
            array(
                'label'     => __LINE__ .': success a',
                'class'     => 'P4Cms_Categorization_Dir',
                'values'    => array(
                    'id'            => 'a',
                    'title'         => 'a title',
                    'description'   => 'a description',
                ),
            ),
        );

        foreach ($tests as $test) {
            $label = $test['label'];
            $category = null;
            try {
                $category = $test['class']::store($test['values']);
                if (isset($test['error'])) {
                    $this->fail("$label - Unexpected success");
                }
            } catch (PHPUnit_Framework_AssertionFailedError $e) {
                $this->fail($e->getMessage());
            } catch (Exception $e) {
                if (isset($test['error'])) {
                    $actual = array(get_class($e) => $e->getMessage());
                    $this->assertSame($test['error'], $actual, "$label - expected exception");
                } else {
                    $this->fail(
                        "$label - Unexpected exception ("
                        . get_class($e) .') :'. $e->getMessage()
                    );
                }
            }

            if (!isset($test['error'])) {
                $this->assertSame($test['values']['id'], $category->getId(), "$label - expected id");
                foreach ($test['values'] as $key => $value) {
                    $this->assertSame(
                        $value,
                        $category->getValue($key),
                        "$label - expected value for '$key'"
                    );
                }
            }
        }

        // confirm files in depot
        $prefix = '//depot/folders';
        $index = P4Cms_Categorization_Dir::CATEGORY_FILENAME;
        $query = P4_File_Query::create()->addFilespec("$prefix/...");
        $files = P4_File::fetchAll($query);
        $filespecs = array();
        foreach ($files as $file) {
            $filespecs[] = $file->getFilespec();
        }
        $this->assertSame(
            array(
                "$prefix/a/$index",
                "$prefix/a/b/$index",
                "$prefix/a/b/c/$index",
                "$prefix/a_b/$index",
            ),
            $filespecs,
            'Expected categories after creation.'
        );

        // test deletions of constructed categories
        $this->assertTrue(
            (bool)P4_File::exists("$prefix/a/b/c/$index", null, true),
            'Expected third file to exist'
        );
        $category = new P4Cms_Categorization_Dir;
        $category->setId('a/b/c')->delete();
        $this->assertFalse(
            P4_File::exists("$prefix/a/b/c/$index", null, true),
            'Expected third file to no longer exist'
        );

        // try deleting a.
        $category = new P4Cms_Categorization_Dir;
        $category->setId('a');
        $category->delete();
        $this->assertFalse(P4_File::exists("$prefix/a/$index", null, true), 'Expected a to no longer exist');
        $this->assertFalse(P4_File::exists("$prefix/a/b/$index", null, true), 'Expected a/b to no longer exist');
    }

    /**
     * Test move.
     */
    public function testMove()
    {
        // create some categories containing entries
        try {
            P4Cms_Categorization_Dir::store('zero');
            P4Cms_Categorization_Dir::store('one');
            P4Cms_Categorization_Dir::store('one/sub1');
            $category = P4Cms_Categorization_Dir::store('two');
            $category->addEntry('a');
            $category = P4Cms_Categorization_Dir::store('three');
            $category->addEntries(array('b', 'c'));
            $category = P4Cms_Categorization_Dir::store('three/sub3');
            $category->addEntries(array('d'));
        } catch (Exception $e) {
            $this->fail(
                'Unexpected exception creating categories for move testing ('
                . get_class($e) .') '. $e->getMessage()
            );
        }

        $prefix = '//depot/folders';
        $index = P4Cms_Categorization_Dir::CATEGORY_FILENAME;
        $encodedA = P4Cms_Categorization_Dir::encodeEntryId('a');
        $encodedB = P4Cms_Categorization_Dir::encodeEntryId('b');
        $encodedC = P4Cms_Categorization_Dir::encodeEntryId('c');
        $encodedD = P4Cms_Categorization_Dir::encodeEntryId('d');

        $query = P4_File_Query::create()->addFilespec("$prefix/...");
        $files = P4_File::fetchAll($query);
        $filespecs = array();
        foreach ($files as $file) {
            $filespecs[] = $file->getFilespec();
        }
        $this->assertSame(
            array(
                "$prefix/one/$index",
                "$prefix/one/sub1/$index",
                "$prefix/three/$index",
                "$prefix/three/$encodedB",
                "$prefix/three/$encodedC",
                "$prefix/three/sub3/$index",
                "$prefix/three/sub3/$encodedD",
                "$prefix/two/$index",
                "$prefix/two/$encodedA",
                "$prefix/zero/$index",
            ),
            $filespecs,
            "Expected initial category layout."
        );

        $tests = array(
            array(
                'label'     => __LINE__ .': null source and target categories',
                'sourceId'  => null,
                'targetId'  => null,
                'error'     => 'Cannot move category; both the source and target category must be specified.',
                'expect'    => null,
            ),
            array(
                'label'     => __LINE__ .': null source category',
                'sourceId'  => null,
                'targetId'  => 'bogus',
                'error'     => 'Cannot move category; both the source and target category must be specified.',
                'expect'    => null,
            ),
            array(
                'label'     => __LINE__ .': null target category',
                'sourceId'  => 'bogus',
                'targetId'  => null,
                'error'     => 'Cannot move category; both the source and target category must be specified.',
                'expect'    => null,
            ),
            array(
                'label'     => __LINE__ .': root source category',
                'sourceId'  => '',
                'targetId'  => 'one',
                'error'     => 'Cannot move category; neither the source or target category can be "".',
                'expect'    => null,
            ),
            array(
                'label'     => __LINE__ .': root target category',
                'sourceId'  => 'one',
                'targetId'  => '',
                'error'     => 'Cannot move category; neither the source or target category can be "".',
                'expect'    => null,
            ),
            array(
                'label'     => __LINE__ .': non-existant source category',
                'sourceId'  => 'bogus',
                'targetId'  => 'bogus2',
                'error'     => 'Cannot move category; source category does not exist.',
                'expect'    => null,
            ),
            array(
                'label'     => __LINE__ .': source category == target category',
                'sourceId'  => 'one',
                'targetId'  => 'one',
                'error'     => 'Cannot move category; target category already exists.',
                'expect'    => null,
            ),
            array(
                'label'     => __LINE__ .': target category exists',
                'sourceId'  => 'one',
                'targetId'  => 'two',
                'error'     => 'Cannot move category; target category already exists.',
                'expect'    => null,
            ),
            array(
                'label'     => __LINE__ .': target category is subcat of source',
                'sourceId'  => 'three',
                'targetId'  => 'three/bogus',
                'error'     => 'Cannot move category; target category is within source category.',
                'expect'    => null,
            ),
            array(
                'label'     => __LINE__ .': move category with no contents',
                'sourceId'  => 'zero',
                'targetId'  => 'moved',
                'error'     => null,
                'expect'    => array(
                    "$prefix/moved/$index",
                    "$prefix/one/$index",
                    "$prefix/one/sub1/$index",
                    "$prefix/three/$index",
                    "$prefix/three/$encodedB",
                    "$prefix/three/$encodedC",
                    "$prefix/three/sub3/$index",
                    "$prefix/three/sub3/$encodedD",
                    "$prefix/two/$index",
                    "$prefix/two/$encodedA",
                ),
            ),
            array(
                'label'     => __LINE__ .': move back category with no contents',
                'sourceId'  => 'moved',
                'targetId'  => 'zero',
                'error'     => null,
                'expect'    => array(
                    "$prefix/one/$index",
                    "$prefix/one/sub1/$index",
                    "$prefix/three/$index",
                    "$prefix/three/$encodedB",
                    "$prefix/three/$encodedC",
                    "$prefix/three/sub3/$index",
                    "$prefix/three/sub3/$encodedD",
                    "$prefix/two/$index",
                    "$prefix/two/$encodedA",
                    "$prefix/zero/$index",
                ),
            ),
            array(
                'label'     => __LINE__ .': move a category with target id starts with source id',
                'sourceId'  => 'zero',
                'targetId'  => 'zeroOne',
                'error'     => null,
                'expect'    => array(
                    "$prefix/one/$index",
                    "$prefix/one/sub1/$index",
                    "$prefix/three/$index",
                    "$prefix/three/$encodedB",
                    "$prefix/three/$encodedC",
                    "$prefix/three/sub3/$index",
                    "$prefix/three/sub3/$encodedD",
                    "$prefix/two/$index",
                    "$prefix/two/$encodedA",
                    "$prefix/zeroOne/$index",
                ),
            ),
            array(
                'label'     => __LINE__ .': move a category with source id starts with target id',
                'sourceId'  => 'zeroOne',
                'targetId'  => 'zero',
                'error'     => null,
                'expect'    => array(
                    "$prefix/one/$index",
                    "$prefix/one/sub1/$index",
                    "$prefix/three/$index",
                    "$prefix/three/$encodedB",
                    "$prefix/three/$encodedC",
                    "$prefix/three/sub3/$index",
                    "$prefix/three/sub3/$encodedD",
                    "$prefix/two/$index",
                    "$prefix/two/$encodedA",
                    "$prefix/zero/$index",
                ),
            ),
            array(
                'label'     => __LINE__ .': move category with entries',
                'sourceId'  => 'two',
                'targetId'  => 'moved',
                'error'     => null,
                'expect'    => array(
                    "$prefix/moved/$index",
                    "$prefix/moved/$encodedA",
                    "$prefix/one/$index",
                    "$prefix/one/sub1/$index",
                    "$prefix/three/$index",
                    "$prefix/three/$encodedB",
                    "$prefix/three/$encodedC",
                    "$prefix/three/sub3/$index",
                    "$prefix/three/sub3/$encodedD",
                    "$prefix/zero/$index",
                ),
            ),
            array(
                'label'     => __LINE__ .': move back category with entries',
                'sourceId'  => 'moved',
                'targetId'  => 'two',
                'error'     => null,
                'expect'    => array(
                    "$prefix/one/$index",
                    "$prefix/one/sub1/$index",
                    "$prefix/three/$index",
                    "$prefix/three/$encodedB",
                    "$prefix/three/$encodedC",
                    "$prefix/three/sub3/$index",
                    "$prefix/three/sub3/$encodedD",
                    "$prefix/two/$index",
                    "$prefix/two/$encodedA",
                    "$prefix/zero/$index",
                ),
            ),
            array(
                'label'     => __LINE__ .': move category with entries and subcats',
                'sourceId'  => 'three',
                'targetId'  => 'moved',
                'error'     => null,
                'expect'    => array(
                    "$prefix/moved/$index",
                    "$prefix/moved/$encodedB",
                    "$prefix/moved/$encodedC",
                    "$prefix/moved/sub3/$index",
                    "$prefix/moved/sub3/$encodedD",
                    "$prefix/one/$index",
                    "$prefix/one/sub1/$index",
                    "$prefix/two/$index",
                    "$prefix/two/$encodedA",
                    "$prefix/zero/$index",
                ),
            ),
            array(
                'label'     => __LINE__ .': move back category with entries and subcats',
                'sourceId'  => 'moved',
                'targetId'  => 'three',
                'error'     => null,
                'expect'    => array(
                    "$prefix/one/$index",
                    "$prefix/one/sub1/$index",
                    "$prefix/three/$index",
                    "$prefix/three/$encodedB",
                    "$prefix/three/$encodedC",
                    "$prefix/three/sub3/$index",
                    "$prefix/three/sub3/$encodedD",
                    "$prefix/two/$index",
                    "$prefix/two/$encodedA",
                    "$prefix/zero/$index",
                ),
            ),
            array(
                'label'     => __LINE__ .': move category to subcat',
                'sourceId'  => 'three',
                'targetId'  => 'one/three',
                'error'     => null,
                'expect'    => array(
                    "$prefix/one/$index",
                    "$prefix/one/sub1/$index",
                    "$prefix/one/three/$index",
                    "$prefix/one/three/$encodedB",
                    "$prefix/one/three/$encodedC",
                    "$prefix/one/three/sub3/$index",
                    "$prefix/one/three/sub3/$encodedD",
                    "$prefix/two/$index",
                    "$prefix/two/$encodedA",
                    "$prefix/zero/$index",
                ),
            ),
            array(
                'label'     => __LINE__ .': move subcat back to category',
                'sourceId'  => 'one/three',
                'targetId'  => 'three',
                'error'     => null,
                'expect'    => array(
                    "$prefix/one/$index",
                    "$prefix/one/sub1/$index",
                    "$prefix/three/$index",
                    "$prefix/three/$encodedB",
                    "$prefix/three/$encodedC",
                    "$prefix/three/sub3/$index",
                    "$prefix/three/sub3/$encodedD",
                    "$prefix/two/$index",
                    "$prefix/two/$encodedA",
                    "$prefix/zero/$index",
                ),
            ),
        );

        foreach ($tests as $test) {
            $label = $test['label'];
            try {
                P4Cms_Categorization_Dir::move($test['sourceId'], $test['targetId']);
                if (isset($test['error'])) {
                    $this->fail("$label - Unexpected success");
                }
            } catch (PHPUnit_Framework_AssertionFailedError $e) {
                $this->fail($e->getMessage());
            } catch (InvalidArgumentException $e) {
                if (isset($test['error'])) {
                    $this->assertSame(
                        $test['error'],
                        $e->getMessage(),
                        "$label - Expected error message."
                    );
                } else {
                    $this->fail("$label - Unexpected argument exception: ". $e->getMessage());
                }
            } catch (Exception $e) {
                $this->fail(
                    "$label - Unexpected exception ("
                    . get_class($e) .') :'. $e->getMessage()
                );
            }

            if (isset($test['expect'])) {
                $query = P4_File_Query::create()->addFilespec("$prefix/...");
                $files = P4_File::fetchAll($query);
                $filespecs = array();
                foreach ($files as $file) {
                    if ($file->isDeleted()) {
                        continue;
                    }
                    $filespecs[] = $file->getFilespec();
                }
                $this->assertSame(
                    $test['expect'],
                    $filespecs,
                    "$label - Expected category layout."
                );
            }
        }

    }

    /**
     * Test that addEntry accepts integer entries.
     */
    public function testAddIntegerEntry()
    {
        $friend = new P4Cms_Categorization_Friend;
        $friend->setId('test');
        $friend->addEntry(1);
        $entries = $friend->getEntries();
        $this->assertSame(array('1'), $entries, 'Expected entries');
    }

    /**
     * Test addEntry with bogus entry.
     */
    public function testAddBogusEntry()
    {
        $dir = new P4Cms_Categorization_Dir;
        $dir->setId('test');
        try {
            $dir->addEntry(null);
            $this->fail('Unexpected success adding a null entry.');
        } catch (PHPUnit_Framework_AssertionFailedError $e) {
            $this->fail($e->getMessage());
        } catch (InvalidArgumentException $e) {
            $this->assertSame(
                "Cannot add entries; all entries must either be strings or known entry types.",
                $e->getMessage(),
                'Expected error adding a null entry.'
            );
        } catch (Exception $e) {
            $this->fail(
                'Unexpected exception adding a null entry ('
                . get_class($e) .') :'. $e->getMessage()
            );
        }

        try {
            $dir->addEntries(array(null, 'four'));
            $this->fail('Unexpected success adding a list with bogus entry.');
        } catch (PHPUnit_Framework_AssertionFailedError $e) {
            $this->fail($e->getMessage());
        } catch (InvalidArgumentException $e) {
            $this->assertSame(
                "Cannot add entries; all entries must either be strings or known entry types.",
                $e->getMessage(),
                'Expected error adding a list with bogus entry.'
            );
        } catch (Exception $e) {
            $this->fail(
                'Unexpected exception adding a list with bogus entry ('
                . get_class($e) .') :'. $e->getMessage()
            );
        }

        try {
            $dir->addEntries(array(array(null, null)));
            $this->fail('Unexpected success adding a list with bogus entry+sort value.');
        } catch (PHPUnit_Framework_AssertionFailedError $e) {
            $this->fail($e->getMessage());
        } catch (InvalidArgumentException $e) {
            $this->assertSame(
                "Cannot add entries; all entries must either be strings or known entry types.",
                $e->getMessage(),
                'Expected error adding a list with bogus entry+sort value.'
            );
        } catch (Exception $e) {
            $this->fail(
                'Unexpected exception adding a list with bogus entry ('
                . get_class($e) .') :'. $e->getMessage()
            );
        }
    }

    /**
     * Test deleteEntry with bogus entry.
     */
    public function testDeleteBogusEntry()
    {
        $dir = new P4Cms_Categorization_Dir;
        $dir->setId('test');
        try {
            $dir->deleteEntry(null);
            $this->fail('Unexpected success deleting a null entry.');
        } catch (PHPUnit_Framework_AssertionFailedError $e) {
            $this->fail($e->getMessage());
        } catch (InvalidArgumentException $e) {
            $this->assertSame(
                "Cannot delete entries; all entries must either be strings or known entry types.",
                $e->getMessage(),
                'Expected error deleting a null entry.'
            );
        } catch (Exception $e) {
            $this->fail(
                'Unexpected exception deleting a null entry ('
                . get_class($e) .') :'. $e->getMessage()
            );
        }

        try {
            $dir->deleteEntries(array(null, 'four'));
            $this->fail('Unexpected success deleting list with bogus entry.');
        } catch (PHPUnit_Framework_AssertionFailedError $e) {
            $this->fail($e->getMessage());
        } catch (InvalidArgumentException $e) {
            $this->assertSame(
                "Cannot delete entries; all entries must either be strings or known entry types.",
                $e->getMessage(),
                'Expected error deleting list with bogus entry.'
            );
        } catch (Exception $e) {
            $this->fail(
                'Unexpected exception deleting list with bogus entry ('
                . get_class($e) .') :'. $e->getMessage()
            );
        }
    }

    /**
     * Test recursive entry fetching.
     */
    public function testGetEntriesRecursively()
    {
        $content = $this->_makeContent(
            array(
                'test1' => 'One',
                'test2' => 'Two',
                'test3' => 'Three',
                'test4' => 'Four',
            )
        );
        $dir = new P4Cms_Categorization_Dir;
        $dir->setId('deep')
            ->setTitle('Deep Category')
            ->setDescription('Deep Category Description')
            ->save();
        $dir->addEntry('test1');

        $dir2 = P4Cms_Categorization_Dir::store('deep/subdir1');
        $this->assertSame('deep/subdir1', $dir2->getId(), 'Exected id for dir2');
        $dir2->addEntry('test2');

        $dir3 = P4Cms_Categorization_Dir::store('deep/subdir1/deeper');
        $dir3->addEntry('test3');

        $dir4 = P4Cms_Categorization_Dir::store('deep/subdir1/deeper/subdir2');
        $dir4->addEntry('test4');

        // start with the parent directory
        $entries = $dir->getEntries();
        sort($entries);
        $this->assertSame(
            array('test1'),
            $entries,
            'Expected non-recursive entries for dir.'
        );

        $entries = $dir->getEntries(array('recursive' => true));
        $this->assertSame(
            array('test4', 'test1', 'test3', 'test2'),
            $entries,
            'Expected recursive entries for dir.'
        );

        // again with subdir1
        $entries = $dir2->getEntries();
        sort($entries);
        $this->assertSame(
            array('test2'),
            $entries,
            'Expected non-recursive entries for dir2.'
        );

        $entries = $dir2->getEntries(array('recursive' => true));
        $this->assertSame(
            array('test4', 'test3', 'test2'),
            $entries,
            'Expected recursive entries for dir2.'
        );

        // again, but fetch objects
        $entries = $dir2->getEntries(array('recursive' => true, 'dereference' => true));
        $this->assertEquals(3, count($entries), "Expected object count.");
        $this->assertSame('test4', $entries['test4']->id, 'Expected object id #1');
        $this->assertSame('test3', $entries['test3']->id, 'Expected object id #2');
        $this->assertSame('test2', $entries['test2']->id, 'Expected object id #3');
    }

    /**
     * Test encoding/decoding of entry ids.
     */
    public function testEncodeDecodeEntryId()
    {
        $tests = array(
            array(
                'label'     => __LINE__ .': encode null',
                'value'     => null,
                'method'    => 'encodeEntryId',
                'expect'    => null,
                'error'     => 'Cannot encode entry id; id not set or has no length.',
            ),
            array(
                'label'     => __LINE__ .': encode empty string',
                'value'     => '',
                'method'    => 'encodeEntryId',
                'expect'    => null,
                'error'     => 'Cannot encode entry id; id not set or has no length.',
            ),
            array(
                'label'     => __LINE__ .': encode string',
                'value'     => 'the quick brown fox jumped over the lazy dog',
                'method'    => 'encodeEntryId',
                'expect'
                    =>'_74686520717569636b2062726f776e20666f78206a756d706564206f76657220746865206c617a7920646f67',
                'error'     => null,
            ),
            array(
                'label'     => __LINE__ .': decode null',
                'value'     => null,
                'method'    => 'decodeEntryId',
                'expect'    => null,
                'error'     => 'Cannot decode entry id; encoded id not set or has no length.',
            ),
            array(
                'label'     => __LINE__ .': decode empty string',
                'value'     => '',
                'method'    => 'decodeEntryId',
                'expect'    => null,
                'error'     => 'Cannot decode entry id; encoded id not set or has no length.',
            ),
            array(
                'label'     => __LINE__ .': decode string',
                'value'
                    => '_74686520717569636b2062726f776e20666f78206a756d706564206f76657220746865206c617a7920646f67',
                'method'    => 'decodeEntryId',
                'expect'    => 'the quick brown fox jumped over the lazy dog',
                'error'     => null,
            ),
            array(
                'label'     => __LINE__ .': decode bogus encoding',
                'value'     => '_#',
                'method'    => 'decodeEntryId',
                'expect'    => null,
                'error'     => 'Cannot decode entry id; encoded id contains invalid characters.',
            ),
        );

        foreach ($tests as $test) {
            $label = $test['label'];
            $result = null;
            try {
                $result = P4Cms_Categorization_Dir::$test['method']($test['value']);
                if (isset($test['error'])) {
                    $this->fail("$label - Unexpected success");
                }
            } catch (PHPUnit_Framework_AssertionFailedError $e) {
                $this->fail($e->getMessage());
            } catch (InvalidArgumentException $e) {
                if (isset($test['error'])) {
                    $this->assertSame(
                        $test['error'],
                        $e->getMessage(),
                        "$label - expected error."
                    );
                } else {
                    $this->fail("$label - Unexpected argument exception: ". $e->getMessage());
                }
            } catch (Exception $e) {
                $this->fail(
                    "$label - Unexpected exception  ("
                    . get_class($e) .') :'. $e->getMessage()
                );
            }

            if (isset($test['expect'])) {
                $this->assertSame(
                    $test['expect'],
                    $result,
                    "$label - expected result"
                );
            }
        }

        // test roundtrip
        $testString = 'http://search.perforce.com/search?q=deleted%20files&site=kb!@#$%^&*()-_=+,<.>/?;:\'"[{]}\\';
        $this->assertSame(
            $testString,
            P4Cms_Categorization_Dir::decodeEntryId(P4Cms_Categorization_Dir::encodeEntryId($testString)),
            'Expected round trip to work.'
        );
    }

    /**
     * Test entry handling.
     */
    public function testEntryHandling()
    {
        // create several content entries.
        $content = $this->_makeContent(
            array(
                'test1' => 'One',
                'test2' => 'Two',
                'test3' => 'Three',
                'test4' => 'Four',
                'test5' => 'Five',
                'test6' => 'Six',
            )
        );

        $dir = new P4Cms_Categorization_Dir;
        $dir->setId('category')
            ->setTitle('Test Category')
            ->setDescription('Test Category Description')
            ->save();
        $this->assertFalse($dir->hasEntries(), 'Expect to have no entries at start');
        $this->assertEquals(0, count($dir->getEntries()), 'Expect entry count to be 0 at start');

        // add an entry
        $dir->addEntry('test1');
        $this->assertTrue($dir->hasEntries(), 'Expect to have entries after adding one');
        $entries = $dir->getEntries();
        $this->assertSame('test1', $entries[0], 'Expected entry');

        // add a couple more entries
        $dir->addEntries(array('test2', 'test3'));
        $this->assertTrue($dir->hasEntries(), 'Expect to have entries after adding two and three');
        $entries = $dir->getEntries();
        $this->assertSame(
            array('test1', 'test3', 'test2'),
            $entries,
            'Expected entries after add test2, test3'
        );

        // add an object
        $dir->addEntry($content[3]);
        $entries = $dir->getEntries();
        $this->assertSame(
            array('test4', 'test1', 'test3', 'test2'),
            $entries,
            'Expected entries after add test4'
        );

        // try adding an object that does not have a getId() method
        try {
            $object = new stdClass;
            $dir->addEntry($object);
            $this->fail('Unexpected success adding object without getId()');
        } catch (PHPUnit_Framework_AssertionFailedError $e) {
            $this->fail($e->getMessage());
        } catch (InvalidArgumentException $e) {
            $this->assertSame(
                "Cannot add entries; all entries must either be strings or known entry types.",
                $e->getMessage(),
                'Expected error adding object without getId()'
            );
        }

        // add a couple more objects
        $dir->addEntries(array($content[4], $content[5]));
        $entries = $dir->getEntries();
        $this->assertSame(
            array('test5', 'test4', 'test1', 'test6', 'test3', 'test2'),
            $entries,
            'Expected entries after add test5, test6'
        );

        // try to add entries with a non-array
        try {
            $dir->addEntries('not an array');
            $this->fail('Unexpected success adding a non-list.');
        } catch (PHPUnit_Framework_AssertionFailedError $e) {
            $this->fail($e->getMessage());
        } catch (InvalidArgumentException $e) {
            $this->assertSame(
                'Cannot add entries; you must provide an array of entries.',
                $e->getMessage(),
                'Expected error adding a non-list.'
            );
        } catch (Exception $e) {
            $this->fail(
                'Unexpected exception adding a non-list ('
                . get_class($e) .') :'. $e->getMessage()
            );
        }

        // delete an entry
        $dir->deleteEntry('test1');
        $entries = $dir->getEntries();
        $this->assertSame(
            array('test5', 'test4', 'test6', 'test3', 'test2'),
            $entries,
            'Expected entries after delete one'
        );

        // delete entries
        $dir->deleteEntries(array('test2', $content[4]));
        $entries = $dir->getEntries();
        $this->assertSame(
            array('test4', 'test6', 'test3'),
            $entries,
            'Expected entries after delete test2, test5'
        );

        // try to delete a non-list
        try {
            $dir->deleteEntries('not a list');
            $this->fail('Unexpected success deleting a non-list.');
        } catch (PHPUnit_Framework_AssertionFailedError $e) {
            $this->fail($e->getMessage());
        } catch (InvalidArgumentException $e) {
            $this->assertSame(
                'Cannot delete entries; you must provide an array of entries.',
                $e->getMessage(),
                'Expected error deleting a non-list.'
            );
        } catch (Exception $e) {
            $this->fail(
                'Unexpected exception deleting a non-list ('
                . get_class($e) .') :'. $e->getMessage()
            );
        }

    }

    /**
     * Test various methods when category not setup.
     */
    public function testCategoryWithoutSetup()
    {
        $dir = new P4Cms_Categorization_Dir;

        $tests = array(
            array(
                'label' => __LINE__ .': getEntries',
                'method'    => 'getEntries',
                'params'    => array(),
                'error'     => 'Cannot get entries; category id is not set.',
            ),
            array(
                'label' => __LINE__ .': getEntries (recursive)',
                'method'    => 'getEntries',
                'params'    => array('recursive' => true),
                'error'     => 'Cannot get entries; category id is not set.',
            ),
            array(
                'label' => __LINE__ .': deleteEntries',
                'method'    => 'deleteEntries',
                'params'    => array('1', '2', '3'),
                'error'     => 'Cannot delete entries; category id is not set.',
            ),
            array(
                'label' => __LINE__ .': addEntry',
                'method'    => 'addEntry',
                'params'    => 'entry',
                'error'     => 'Cannot add entries; category id is not set.',
            ),
            array(
                'label' => __LINE__ .': hasChildren',
                'method'    => 'hasChildren',
                'params'    => null,
                'error'     => 'Cannot get children; category id is not set.',
            ),
            array(
                'label' => __LINE__ .': delete',
                'method'    => 'delete',
                'params'    => null,
                'error'     => 'Cannot delete category; category id is not set.',
            ),
        );

        foreach ($tests as $test) {
            $label = $test['label'];
            $method = $test['method'];
            try {
                $dir->$method($test['params']);
                $this->fail("$label - Unexpected success.");
            } catch (PHPUnit_Framework_AssertionFailedError $e) {
                $this->fail($e->getMessage());
            } catch (P4Cms_Categorization_Exception $e) {
                $this->assertSame(
                    $test['error'],
                    $e->getMessage(),
                    "$label - Expected error."
                );
            } catch (Exception $e) {
                $this->fail(
                    "$label - Unexpected exception ("
                    . get_class($e) .') :'. $e->getMessage()
                );
            }
        }

    }

    /**
     * Test getEntries with $dereference set.
     */
    public function testGetEntriesMakeObjects()
    {
        $content = $this->_makeContent(
            array(
                'test1' => 'One',
            )
        );

        // setup friend tests, where dereferenceEntry has not been implemented.
        $index = P4Cms_Categorization_Dir::CATEGORY_FILENAME;
        $file1 = new P4_File;
        $file1->setFilespec("//depot/friends/test/$index")
              ->add()
              ->setLocalContents('')
              ->submit("added friend test/$index");

        $encodedId = P4Cms_Categorization_Dir::encodeEntryId('test1');
        $file2 = new P4_File;
        $file2->setFilespec("//depot/friends/test/$encodedId")
              ->add()
              ->setLocalContents('')
              ->submit('added friend test/test1');

        // verify that the friend class won't create objects via getEntries
        $friend = new P4Cms_Categorization_Friend;
        $friend->setId('test');
        $entries = $friend->getEntries(array('dereference' => true));
        $this->assertSame(
            array('test1'),
            $entries,
            "Expected getEntries() return list of entry ids even if tried to dereference."
        );

        // setup dir tests, where dereference has been implemented.
        $file3 = new P4_File;
        $file3->setFilespec("//depot/folders/test/$index")
              ->add()
              ->setLocalContents('')
              ->submit("added dir test/$index");

        $encodedId = P4Cms_Categorization_Dir::encodeEntryId('test1');
        $file4 = new P4_File;
        $file4->setFilespec("//depot/folders/test/$encodedId")
              ->add()
              ->setLocalContents('')
              ->submit('added dir test/test1');

        // verify that the dir class can create objects via getEntries
        $dir = new P4Cms_Categorization_Dir;
        $entries = $dir->setId('test')->getEntries(array('dereference' => true));
        $this->assertEquals(1, count($entries), 'Expected entries count.');
        $this->assertSame('test1', $entries['test1']->id, 'Expected object id');
    }

    /**
     * Test dereferenceEntries.
     */
    public function testDereferenceEntries()
    {
        // create entries
        $create = array(
            'deref1' => 'One',
            'deref2' => 'Two',
            'deref3' => 'Three',
        );
        $content = $this->_makeContent($create);

        // verify that the friend class won't dereference entries.
        $friend = new P4Cms_Categorization_Friend;
        $friend->setId('test')
               ->addEntries(array_keys($create));
        $entries = $friend->getEntries(array('paths' => array(1, 2, 3)));
        $this->assertSame(
            array_keys($create),
            $entries,
            "Expected empty entries list."
        );

        // verify that the dir class can dereference entries.
        $dir = new P4Cms_Categorization_Dir;
        $dir->setId('test')
            ->addEntries(array_keys($create));
        $entries = $dir->getEntries(
            array(
                'dereference' => true,
                'paths'       => array_keys($create)
            )
        );
        $titles  = array();
        foreach ($entries as $entry) {
            $titles[] = $entry->getValue('title');
        }

        // assume entries are sorted by title by default
        $expected = array('One', 'Three', 'Two');
        $this->assertEquals($expected, $titles, 'Expected objects');
    }

    /**
     * Test adding an entry that already exists, and deleting an entry that does not exist.
     */
    public function testAddDupeAndDeleteUnknown()
    {
        $content = $this->_makeContent(
            array(
                'test1' => 'One',
            )
        );

        $dir = new P4Cms_Categorization_Dir;

        $dir->setId('category')
            ->setTitle('Test Category')
            ->setDescription('Test Category Description')
            ->save();
        $this->assertFalse($dir->hasEntries(), 'Expect to have no entries at start.');
        $this->assertEquals(0, count($dir->getEntries()), 'Expect entry count to be 0 at start.');

        // try deleting an entry
        $dir->deleteEntry('test1');

        // add an entry
        $dir->addEntry('test1');
        $this->assertTrue($dir->hasEntries(), 'Expect an entry');
        $this->assertEquals(1, count($dir->getEntries()), 'Expect entry count to be 1 after adding entry.');

        // try adding same entry again
        $dir->addEntry('test1');
        $this->assertEquals(1, count($dir->getEntries()), 'Expect entry count to be 1 after adding dupe.');

        // delete the entry
        $dir->deleteEntry('test1');
        $this->assertFalse($dir->hasEntries(), 'Expect to have no entries at end.');

        // add the entry again to make sure files with delete status have no influence.
        $dir->addEntry('test1');
        $this->assertEquals(1, count($dir->getEntries()), 'Expect entry count to be 1 after adding dupe.');
    }

    /**
     * Test setEntryCategories with bad parameters
     */
    public function testSetEntryCategoriesBadParams()
    {
        $tests = array(
            array(
                'label'         => __LINE__ .': null entry',
                'entry'         => null,
                'categories'    => null,
                'error'         => 'Cannot set categories; the entry must either be a string or known data structure.',
            ),
            array(
                'label'         => __LINE__ .': empty entry',
                'entry'         => '',
                'categories'    => null,
                'error'         => 'Cannot set categories; the entry must either be a string or known data structure.',
            ),
            array(
                'label'         => __LINE__ .': string entry, null categories',
                'entry'         => 'string',
                'categories'    => null,
                'error'         => 'Cannot set categories; categories must be an array.',
            ),
        );

        foreach ($tests as $test) {
            $label = $test['label'];
            try {
                P4Cms_Categorization_Dir::setEntryCategories($test['entry'], 'A', $test['categories']);
                if (isset($test['error'])) {
                    $this->fail("$label - unexpected success");
                }
            } catch (PHPUnit_Framework_AssertionFailedError $e) {
                $this->fail($e->getMessage());
            } catch (InvalidArgumentException $e) {
                if (isset($test['error'])) {
                    $this->assertSame(
                        $test['error'],
                        $e->getMessage(),
                        "$label - Expected error"
                    );
                } else {
                    $this->fail("$label - unexpected argument exception: ". $e->getMessage());
                }
            } catch (Exception $e) {
                $this->fail("$label - Unexpected exception (" . get_class($e) .') :'. $e->getMessage());
            }
        }
    }

    /**
     * Test setEntryCategories behaviour.
     */
    public function testSetEntryCategories()
    {
        $tests = array(
            array(
                'label'         => __LINE__ .': foo, set no cats, with no cats (noop)',
                'entry'         => 'foo',
                'setCats'       => array(),
                'expectedCats'  => array(),
                'expectedSet'   => array(),
            ),
            array(
                'label'         => __LINE__ .': foo, set cats, with no cats (adds/creates)',
                'entry'         => 'foo',
                'setCats'       => array('one', 'one/two'),
                'expectedCats'  => array('one', 'one/two'),
                'expectedSet'   => array('one', 'one/two'),
            ),
            array(
                'label'         => __LINE__ .': bar, set no cats, with some cats (noop)',
                'entry'         => 'bar',
                'setCats'       => array(),
                'expectedCats'  => array('one', 'one/two'),
                'expectedSet'   => array(),
            ),
            array(
                'label'         => __LINE__ .': bar, set some cats, with some cats (adds/creates)',
                'entry'         => 'bar',
                'setCats'       => array('one', 'three'),
                'expectedCats'  => array('one', 'one/two', 'three'),
                'expectedSet'   => array('one', 'three'),
            ),
            array(
                'label'         => __LINE__ .': bar, set same cats, with some cats (noop)',
                'entry'         => 'bar',
                'setCats'       => array('one', 'three'),
                'expectedCats'  => array('one', 'one/two', 'three'),
                'expectedSet'   => array('one', 'three'),
            ),
            array(
                'label'         => __LINE__ .': baz, set some cats, with some cats (adds/creates)',
                'entry'         => 'baz',
                'setCats'       => array('one', 'four'),
                'expectedCats'  => array('four', 'one', 'one/two', 'three'),
                'expectedSet'   => array('four', 'one'),
            ),
            array(
                'label'         => __LINE__ .': baz, set some cats, with some cats (adds/deletes)',
                'entry'         => 'baz',
                'setCats'       => array('three', 'four'),
                'expectedCats'  => array('four', 'one', 'one/two', 'three'),
                'expectedSet'   => array('four', 'three'),
            ),
            array(
                'label'         => __LINE__ .': baz, set no cats, with some cats (deletes)',
                'entry'         => 'baz',
                'setCats'       => array(),
                'expectedCats'  => array('four', 'one', 'one/two', 'three'),
                'expectedSet'   => array(),
            ),
        );

        foreach ($tests as $test) {

            // create categories.
            foreach ($test['setCats'] as $id) {
                $category = new P4Cms_Categorization_Dir;
                $category->setId($id)->save();
            }
            $label = $test['label'];
            P4Cms_Categorization_Dir::setEntryCategories($test['entry'], $test['setCats']);

            // check that categories exist as expected
            $categories = P4Cms_Categorization_Dir::fetchAll();
            $ids = array();
            foreach ($categories as $cat) {
                $ids[] = $cat->getId();
            }
            $this->assertSame($test['expectedCats'], $ids, "$label - Expected category layout");

            // check that the categories associated are as expected
            $categories = P4Cms_Categorization_Dir::fetchAllByEntry($test['entry']);
            $ids = array();
            foreach ($categories as $cat) {
                $ids[] = $cat->getId();
            }
            $this->assertSame($test['expectedSet'], $ids, "$label - Expected category associations");
        }
    }

    /**
     * Test fetchAllByEntry with bad parameters.
     */
    public function testFetchAllByEntryBadParams()
    {
        $tests = array(
            array(
                'label'     => __LINE__ .': null',
                'entry'     => null,
                'error'     => 'Cannot get categories; the entry must either be a string or known entry type.',
            ),
            array(
                'label'     => __LINE__ .': empty string',
                'entry'     => '',
                'error'     => 'Cannot get categories; the entry must either be a string or known entry type.',
            ),
            array(
                'label'     => __LINE__ .': string',
                'entry'     => 'string',
                'error'     => null,
                'expected'  => array(),
            ),
        );

        foreach ($tests as $test) {
            $label = $test['label'];
            try {
                $categories = P4Cms_Categorization_Dir::fetchAllByEntry($test['entry']);
                if (isset($test['error'])) {
                    $this->fail("$label - unexpected success");
                }
            } catch (PHPUnit_Framework_AssertionFailedError $e) {
                $this->fail($e->getMessage());
            } catch (InvalidArgumentException $e) {
                if (isset($test['error'])) {
                    $this->assertSame(
                        $test['error'],
                        $e->getMessage(),
                        "$label - Expected error"
                    );
                } else {
                    $this->fail("$label - unexpected argument exception: ". $e->getMessage());
                }
            } catch (Exception $e) {
                $this->fail("$label - Unexpected exception (" . get_class($e) .') :'. $e->getMessage());
            }

            if (!isset($test['error'])) {
                $ids = array();
                foreach ($categories as $category) {
                    $ids[] = $category->getId();
                }
                $this->assertSame(
                    $test['expected'],
                    $ids,
                    "$label - expected category ids"
                );
            }
        }
    }

    /**
     * Test fetchAllByEntry behaviour.
     */
    public function testFetchAllByEntry()
    {
        // attempt to get categories when none are defined.
        $categories = P4Cms_Categorization_Dir::fetchAllByEntry('test');
        $ids = array();
        foreach ($categories as $category) {
            $ids[] = $category->getId();
        }
        $this->assertEquals(array(), $ids, 'Expect no categories when none defined');

        // make some categories, and try again
        $one = P4Cms_Categorization_Dir::store(array('id' => 'one', 'title' => 'One'));
        $two = P4Cms_Categorization_Dir::store(array('id' => 'one/two', 'title' => 'Two in One'));
        $categories = P4Cms_Categorization_Dir::fetchAllByEntry('test');
        $ids = array();
        foreach ($categories as $category) {
            $ids[] = $category->getId();
        }
        $this->assertEquals(array(), $ids, 'Expect no categories when some defined, but not populated');

        // add some entries, one for the target
        $one->addEntry('entry1');
        $two->addEntry('entry2');
        $two->addEntry('test');
        $categories = P4Cms_Categorization_Dir::fetchAllByEntry('test');
        $ids = array();
        foreach ($categories as $category) {
            $ids[] = $category->getId();
        }
        $this->assertEquals(array('one/two'), $ids, 'Expect one category after initial entry creation');

        // add another target entry.
        $one->addEntry('test');
        $categories = P4Cms_Categorization_Dir::fetchAllByEntry('test');
        $ids = array();
        foreach ($categories as $category) {
            $ids[] = $category->getId();
        }
        $this->assertEquals(
            array($one->getId(), $two->getId()),
            $ids,
            'Expect two category after second entry creation'
        );
    }

    /**
     * Test getDepth
     */
    public function testGetDepth()
    {
        $depths   = range(0, 10);
        $category = new P4Cms_Categorization_Dir;
        foreach ($depths as $depth) {
            $id = 'foo' . str_repeat('/foo', $depth);
            $this->assertSame(
                $depth,
                $category->setId($id)->getDepth(),
                "Expected category depth of $depth."
            );
        }
    }

    /**
     * Test get ancestors
     */
    public function testGetAncestors()
    {
        // make a category tree.
        P4Cms_Categorization_Dir::store(array("id" => "foo"));
        P4Cms_Categorization_Dir::store(array("id" => "foo/bar"));
        P4Cms_Categorization_Dir::store(array("id" => "foo/bar/baz"));
        P4Cms_Categorization_Dir::store(array("id" => "foo/bar/baz/bof"));

        // get leaf and grab ancestry.
        $category  = P4Cms_Categorization_Dir::fetch("foo/bar/baz/bof");
        $ancestors = $category->getAncestors();

        // ensure 3 ancestors.
        $this->assertSame(3, $ancestors->count());

        // ensure expected ids.
        $expect = array('foo', 'foo/bar', 'foo/bar/baz');
        $actual = $ancestors->invoke('getId');
        $this->assertSame($expect, $actual, 'Expected ids from getAncestors');

        // test just the ids
        $ids = $category->getAncestorIds();
        $this->assertSame($expect, $ids, 'Expected ids from getAncestorIds');

        $dir = new P4Cms_Categorization_Dir;
        $this->assertSame(
            array(),
            $dir->setId('one')->getAncestorIds(),
            'Expected ids when category has no ancestor'
        );
    }

    /**
     * Test simple accessor/mutator methods.
     */
    public function testAccessorsMutators()
    {
        $dir = new P4Cms_Categorization_Dir;
        $dir->setId('one/two')
            ->setTitle('title')
            ->setDescription('description');

        $this->assertEquals('one/two',      $dir->getId(),          'expected id');
        $this->assertEquals('two',          $dir->getBaseId(),      'expected base id');
        $this->assertEquals('title',        $dir->getTitle(),       'expected title');
        $this->assertEquals('description',  $dir->getDescription(), 'expected description');
    }

    /**
     * Test helper method to create specified content records.
     *
     * @param   array  $entries  An array of id => title for the content records to create.
     * @return  array  An array of the created content entries.
     */
    protected function _makeContent(array $entries)
    {
        $type = new P4Cms_Content_Type();
        $type->setId("basic-page")
             ->setLabel("Basic Page")
             ->setElements(
                array(
                    "title" => array(
                        "type"      => "text",
                        "options"   => array("label" => "Title", "required" => true)
                    ),
                    "description"   => array(
                        "type"      => "text",
                        "options"   => array("label" => "Description")
                    )
                )
             )
             ->save();

        $created = array();
        foreach ($entries as $id => $title) {
            $entry = new P4Cms_Content;
            $entry->setId($id)
                  ->setValue('contentType', 'basic-page')
                  ->setValue('title', $title)
                  ->save();
            $created[] = $entry;
        }

        return $created;

    }
}
# Change User Description Committed
#1 16170 perforce_software Move Chronicle files to follow new path scheme for branching.
//guest/perforce_software/chronicle/tests/phpunit/P4Cms/Categorization/Test.php
#1 8972 Matt Attaway Initial add of the Chronicle source code