Index: tests/bootstrap.php
===================================================================
--- tests/bootstrap.php	(revision 0)
+++ tests/bootstrap.php	(working copy)
@@ -0,0 +1,19 @@
+<?php
+
+require( dirname( __FILE__ ) . '/includes/define-constants.php' );
+
+if ( ! file_exists( WP_TESTS_DIR . '/includes/functions.php' ) ) {
+	die( "The WordPress PHPUnit test suite could not be found.\n" );
+}
+
+require_once WP_TESTS_DIR . '/includes/functions.php';
+
+function _install_and_load_bbpress() {
+	require BBP_TESTS_DIR . '/includes/loader.php';
+}
+tests_add_filter( 'muplugins_loaded', '_install_and_load_bbpress' );
+
+require WP_TESTS_DIR . '/includes/bootstrap.php';
+
+// Load the BP-specific testing tools
+require BBP_TESTS_DIR . '/includes/testcase.php';
Index: tests/includes/define-constants.php
===================================================================
--- tests/includes/define-constants.php	(revision 0)
+++ tests/includes/define-constants.php	(working copy)
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * Define constants needed by test suite.
+ */
+
+define( 'BBP_PLUGIN_DIR', dirname( dirname( dirname( __FILE__ ) ) ) . '/' );
+
+if ( ! defined( 'BBP_TESTS_DIR' ) ) {
+	define( 'BBP_TESTS_DIR', dirname( dirname( __FILE__ ) ) . '/' );
+}
+
+/**
+ * In the pre-develop.svn WP development environment, an environmental bash
+ * variable would be set to run PHP Unit tests. However, this has been done
+ * away with in a post-develop.svn world. We'll still check if this variable
+ * is set for backwards compat.
+ */
+if ( getenv( 'WP_TESTS_DIR' ) ) {
+	define( 'WP_TESTS_DIR', getenv( 'WP_TESTS_DIR' ) );
+	define( 'WP_ROOT_DIR', WP_TESTS_DIR );
+} else {
+	define( 'WP_ROOT_DIR', dirname( dirname( dirname( dirname( dirname( dirname( __DIR__ ) ) ) ) ) ) );
+	define( 'WP_TESTS_DIR', WP_ROOT_DIR . '/tests/phpunit' );
+}
+
+// Based on the tests directory, look for a config file
+if ( file_exists( WP_ROOT_DIR . '/wp-tests-config.php' ) ) {
+	// Standard develop.svn.wordpress.org setup
+	define( 'WP_TESTS_CONFIG_PATH', WP_ROOT_DIR . '/wp-tests-config.php' );
+
+} else if ( file_exists( WP_TESTS_DIR . '/wp-tests-config.php' ) ) {
+	// Legacy unit-test.svn.wordpress.org setup
+	define( 'WP_TESTS_CONFIG_PATH', WP_TESTS_DIR . '/wp-tests-config.php' );
+
+} else if ( file_exists( dirname( dirname( WP_TESTS_DIR ) ) . '/wp-tests-config.php' ) ) {
+	// Environment variable exists and points to tests/phpunit of
+	// develop.svn.wordpress.org setup
+	define( 'WP_TESTS_CONFIG_PATH', dirname( dirname( WP_TESTS_DIR ) ) . '/wp-tests-config.php' );
+
+} else {
+	die( "wp-tests-config.php could not be found.\n" );
+}
Index: tests/includes/factory.php
===================================================================
--- tests/includes/factory.php	(revision 0)
+++ tests/includes/factory.php	(working copy)
@@ -0,0 +1,165 @@
+<?php
+class BBP_UnitTest_Factory extends WP_UnitTest_Factory {
+	public $activity = null;
+
+	function __construct() {
+		parent::__construct();
+
+		$this->activity = new BBP_UnitTest_Factory_For_Activity( $this );
+		$this->group = new BBP_UnitTest_Factory_For_Group( $this );
+		$this->xprofile_group = new BBP_UnitTest_Factory_For_XProfileGroup( $this );
+		$this->xprofile_field = new BBP_UnitTest_Factory_For_XProfileField( $this );
+		$this->notification = new BBP_UnitTest_Factory_For_Notification( $this );
+	}
+}
+
+class BBP_UnitTest_Factory_For_Activity extends WP_UnitTest_Factory_For_Thing {
+
+	function __construct( $factory = null ) {
+		parent::__construct( $factory );
+
+		$this->default_generation_definitions = array(
+			'action'       => new WP_UnitTest_Generator_Sequence( 'Activity action %s' ),
+			'component'    => buddypress()->activity->id,
+			'content'      => new WP_UnitTest_Generator_Sequence( 'Activity content %s' ),
+			'primary_link' => 'http://example.com',
+			'type'         => 'activity_update',
+			'recorded_time' => bbp_core_current_time(),
+		);
+	}
+
+	function create_object( $args ) {
+		if ( ! isset( $args['user_id'] ) )
+			$args['user_id'] = get_current_user_id();
+
+		return bbp_activity_add( $args );
+	}
+
+	function update_object( $activity_id, $fields ) {
+		$activity = new BBP_Activity_Activity( $activity_id );
+
+		foreach ( $fields as $field_name => $value ) {
+			if ( isset( $activity->$field_name ) )
+				$activity->$field_name = $value;
+		}
+
+		$activity->save();
+		return $activity;
+	}
+
+	function get_object_by_id( $user_id ) {
+		return new BBP_Activity_Activity( $user_id );
+	}
+}
+
+class BBP_UnitTest_Factory_For_Group extends WP_UnitTest_Factory_For_Thing {
+
+	function __construct( $factory = null ) {
+		parent::__construct( $factory );
+
+		$this->default_generation_definitions = array(
+			'name'         => new WP_UnitTest_Generator_Sequence( 'Group %s' ),
+			'description'  => new WP_UnitTest_Generator_Sequence( 'Group description %s' ),
+			'slug'         => new WP_UnitTest_Generator_Sequence( 'group-slug-%s' ),
+			'status'       => 'public',
+			'enable_forum' => true,
+			'date_created' => bbp_core_current_time(),
+		);
+	}
+
+	function create_object( $args ) {
+		if ( ! isset( $args['creator_id'] ) ) {
+			$args['creator_id'] = get_current_user_id();
+		}
+
+		$group_id = groups_create_group( $args );
+
+		groups_update_groupmeta( $group_id, 'total_member_count', 1 );
+
+		$last_activity = isset( $args['last_activity'] ) ? $args['last_activity'] : bbp_core_current_time();
+		groups_update_groupmeta( $group_id, 'last_activity', $last_activity );
+
+		return $group_id;
+	}
+
+	function update_object( $group_id, $fields ) {
+		$group = new BBP_Groups_Group( $group_id );
+
+		foreach ( $fields as $field_name => $value ) {
+			if ( isset( $group->field_name ) )
+				$group->field_name = $value;
+		}
+
+		$group->save();
+		return $group;
+	}
+
+	function get_object_by_id( $group_id ) {
+		return new BBP_Groups_Group( $group_id );
+	}
+}
+
+class BBP_UnitTest_Factory_For_XProfileGroup extends WP_UnitTest_Factory_For_Thing {
+
+	function __construct( $factory = null ) {
+		parent::__construct( $factory );
+
+		$this->default_generation_definitions = array(
+			'name'         => new WP_UnitTest_Generator_Sequence( 'XProfile group %s' ),
+			'description'  => new WP_UnitTest_Generator_Sequence( 'XProfile group description %s' ),
+			'slug'         => new WP_UnitTest_Generator_Sequence( 'xprofile-group-slug-%s' ),
+		);
+	}
+
+	function create_object( $args ) {
+		$group_id = xprofile_insert_field_group( $args );
+		return $this->get_object_by_id( $group_id );
+	}
+
+	function update_object( $group_id, $fields ) {
+	}
+
+	function get_object_by_id( $group_id ) {
+		return new BBP_XProfile_Group( $group_id );
+	}
+}
+
+class BBP_UnitTest_Factory_For_XProfileField extends WP_UnitTest_Factory_For_Thing {
+
+	function __construct( $factory = null ) {
+		parent::__construct( $factory );
+
+		$this->default_generation_definitions = array(
+			'name'         => new WP_UnitTest_Generator_Sequence( 'XProfile field %s' ),
+			'description'  => new WP_UnitTest_Generator_Sequence( 'XProfile field description %s' ),
+		);
+	}
+
+	function create_object( $args ) {
+		$field_id = xprofile_insert_field( $args );
+		return $this->get_object_by_id( $field_id );
+	}
+
+	function update_object( $field_id, $fields ) {
+	}
+
+	function get_object_by_id( $field_id ) {
+		return new BBP_XProfile_Field( $field_id );
+	}
+}
+
+class BBP_UnitTest_Factory_For_Notification extends WP_UnitTest_Factory_For_Thing {
+	public function __construct( $factory = null ) {
+		parent::__construct( $factory );
+	}
+
+	public function create_object( $args ) {
+		return bbp_notifications_add_notification( $args );
+	}
+
+	public function update_object( $id, $fields ) {}
+
+	public function get_object_by_id( $id ) {
+		return new BBP_Notifications_Notification( $id );
+	}
+}
Index: tests/includes/install.php
===================================================================
--- tests/includes/install.php	(revision 0)
+++ tests/includes/install.php	(working copy)
@@ -0,0 +1,54 @@
+<?php
+/**
+ * Installs bbPress for the purpose of the unit-tests
+ *
+ * @todo Reuse the init/load code in init.php
+ * @todo Support MULTIBLOG
+ */
+error_reporting( E_ALL & ~E_DEPRECATED & ~E_STRICT );
+
+$config_file_path = $argv[1];
+$tests_dir_path = $argv[2];
+$multisite = ! empty( $argv[3] );
+
+require_once $config_file_path;
+require_once $tests_dir_path . '/includes/functions.php';
+
+function _load_bbpress() {
+	require dirname( dirname( dirname( __FILE__ ) ) ) . '/bbpress.php';
+}
+tests_add_filter( 'muplugins_loaded', '_load_bbpress' );
+
+define( 'BBP_PLUGIN_DIR', dirname( dirname( dirname( __FILE__ ) ) ) . '/' );
+define( 'BBP_ROOT_BLOG', 1 );
+
+// Always load admin bar
+tests_add_filter( 'show_admin_bar', '__return_true' );
+
+$_SERVER['SERVER_PROTOCOL'] = 'HTTP/1.1';
+$_SERVER['HTTP_HOST'] = WP_TESTS_DOMAIN;
+$PHP_SELF = $GLOBALS['PHP_SELF'] = $_SERVER['PHP_SELF'] = '/index.php';
+
+require_once ABSPATH . '/wp-settings.php';
+
+echo "Installing bbPress...\n";
+
+// Make sure that bbPress has been cleaned from all blogs before reinstalling
+$blogs = is_multisite() ? $wpdb->get_col( "SELECT blog_id FROM {$wpdb->blogs}" ) : array( 1 );
+foreach ( $blogs as $blog ) {
+	if ( is_multisite() ) {
+		switch_to_blog( $blog );
+	}
+
+	$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '%bbp%'" );
+
+	if ( is_multisite() ) {
+		restore_current_blog();
+	}
+}
+
+$wpdb->query( 'SET storage_engine = INNODB' );
+$wpdb->select( DB_NAME, $wpdb->dbh );
+
+// Install BuddyPress
+// bbp_version_updater();
Index: tests/includes/loader.php
===================================================================
--- tests/includes/loader.php	(revision 0)
+++ tests/includes/loader.php	(working copy)
@@ -0,0 +1,9 @@
+<?php
+
+require_once( dirname( __FILE__ ) . '/define-constants.php' );
+
+$multisite = (int) ( defined( 'WP_TESTS_MULTISITE') && WP_TESTS_MULTISITE );
+system( WP_PHP_BINARY . ' ' . escapeshellarg( dirname( __FILE__ ) . '/install.php' ) . ' ' . escapeshellarg( WP_TESTS_CONFIG_PATH ) . ' ' . escapeshellarg( WP_TESTS_DIR ) . ' ' . $multisite );
+
+// Bootstrap BBP
+require dirname( __FILE__ ) . '/../../bbpress.php';
Index: tests/includes/testcase.php
===================================================================
--- tests/includes/testcase.php	(revision 0)
+++ tests/includes/testcase.php	(working copy)
@@ -0,0 +1,336 @@
+<?php
+
+/**
+ * WP's test suite wipes out BBP's directory page mappings with _delete_all_posts()
+ * We must reestablish them before our tests can be successfully run
+ */
+
+require_once dirname( __FILE__ ) . '/factory.php';
+
+class BBP_UnitTestCase extends WP_UnitTestCase {
+
+	protected $temp_has_bbp_moderate = array();
+	protected $cached_SERVER_NAME = null;
+
+	public function setUp() {
+		parent::setUp();
+
+		// Make sure all users are deleted
+		// There's a bug in the multisite tests that causes the
+		// transaction rollback to fail for the first user created,
+		// which busts every other attempt to create users. This is a
+		// hack workaround
+		global $wpdb;
+		$wpdb->query( "TRUNCATE TABLE {$wpdb->users}" );
+
+		// Fake WP mail globals, to avoid errors
+		add_filter( 'wp_mail', array( $this, 'setUp_wp_mail' ) );
+		add_filter( 'wp_mail_from', array( $this, 'tearDown_wp_mail' ) );
+
+		$this->factory = new BBP_UnitTest_Factory;
+	}
+
+	function clean_up_global_scope() {
+		buddypress()->bbp_nav                = buddypress()->bbp_options_nav = buddypress()->action_variables = buddypress()->canonical_stack = buddypress()->unfiltered_uri = $GLOBALS['bbp_unfiltered_uri'] = array();
+		buddypress()->current_component     = buddypress()->current_item = buddypress()->current_action = '';
+		buddypress()->unfiltered_uri_offset = 0;
+		buddypress()->is_single_item        = false;
+		buddypress()->current_user          = new stdClass();
+		buddypress()->displayed_user        = new stdClass();
+		buddypress()->loggedin_user         = new stdClass();
+		buddypress()->avatar                = new stdClass();
+
+		parent::clean_up_global_scope();
+	}
+
+	function assertPreConditions() {
+		parent::assertPreConditions();
+
+		// Reinit some of the globals that might have been cleared by BBP_UnitTestCase::clean_up_global_scope().
+		// This is here because it didn't work in clean_up_global_scope(); I don't know why.
+		do_action( 'bbp_setup_globals' );
+	}
+
+	function go_to( $url ) {
+		// note: the WP and WP_Query classes like to silently fetch parameters
+		// from all over the place (globals, GET, etc), which makes it tricky
+		// to run them more than once without very carefully clearing everything
+		$_GET = $_POST = array();
+		foreach (array('query_string', 'id', 'postdata', 'authordata', 'day', 'currentmonth', 'page', 'pages', 'multipage', 'more', 'numpages', 'pagenow') as $v) {
+			if ( isset( $GLOBALS[$v] ) ) unset( $GLOBALS[$v] );
+		}
+		$parts = parse_url($url);
+		if (isset($parts['scheme'])) {
+			// set the HTTP_HOST
+			$GLOBALS['_SERVER']['HTTP_HOST'] = $parts['host'];
+
+			$req = $parts['path'];
+			if (isset($parts['query'])) {
+				$req .= '?' . $parts['query'];
+				// parse the url query vars into $_GET
+				parse_str($parts['query'], $_GET);
+			}
+		} else {
+			$req = $url;
+		}
+		if ( ! isset( $parts['query'] ) ) {
+			$parts['query'] = '';
+		}
+
+		// Scheme
+		if ( 0 === strpos( $req, '/wp-admin' ) && force_ssl_admin() ) {
+			$_SERVER['HTTPS'] = 'on';
+		} else {
+			unset( $_SERVER['HTTPS'] );
+		}
+
+		// Set this for bbp_core_set_uri_globals()
+		$GLOBALS['_SERVER']['REQUEST_URI'] = $req;
+		unset($_SERVER['PATH_INFO']);
+
+		// setup $current_site and $current_blog globals for multisite based on
+		// REQUEST_URI; mostly copied from /wp-includes/ms-settings.php
+		if ( is_multisite() ) {
+			$domain = addslashes( $_SERVER['HTTP_HOST'] );
+			if ( false !== strpos( $domain, ':' ) ) {
+				if ( substr( $domain, -3 ) == ':80' ) {
+					$domain = substr( $domain, 0, -3 );
+					$_SERVER['HTTP_HOST'] = substr( $_SERVER['HTTP_HOST'], 0, -3 );
+				} elseif ( substr( $domain, -4 ) == ':443' ) {
+					$domain = substr( $domain, 0, -4 );
+					$_SERVER['HTTP_HOST'] = substr( $_SERVER['HTTP_HOST'], 0, -4 );
+				}
+			}
+
+			$domain = rtrim( $domain, '.' );
+			$cookie_domain = $domain;
+			if ( substr( $cookie_domain, 0, 4 ) == 'www.' )
+				$cookie_domain = substr( $cookie_domain, 4 );
+
+			$path = preg_replace( '|([a-z0-9-]+.php.*)|', '', $GLOBALS['_SERVER']['REQUEST_URI'] );
+			$path = str_replace ( '/wp-admin/', '/', $path );
+			$path = preg_replace( '|(/[a-z0-9-]+?/).*|', '$1', $path );
+
+			$GLOBALS['current_site'] = wpmu_current_site();
+			if ( ! isset( $GLOBALS['current_site']->blog_id ) )
+				$GLOBALS['current_site']->blog_id = $wpdb->get_var( $wpdb->prepare( "SELECT blog_id FROM $wpdb->blogs WHERE domain = %s AND path = %s", $GLOBALS['current_site']->domain, $GLOBALS['current_site']->path ) );
+
+			// unit tests only support subdirectory install at the moment
+			// removed object cache references
+			if ( ! is_subdomain_install() ) {
+				$blogname = htmlspecialchars( substr( $GLOBALS['_SERVER']['REQUEST_URI'], strlen( $path ) ) );
+				if ( false !== strpos( $blogname, '/' ) )
+					$blogname = substr( $blogname, 0, strpos( $blogname, '/' ) );
+				if ( false !== strpos( $blogname, '?' ) )
+					$blogname = substr( $blogname, 0, strpos( $blogname, '?' ) );
+				$reserved_blognames = array( 'page', 'comments', 'blog', 'wp-admin', 'wp-includes', 'wp-content', 'files', 'feed' );
+				if ( $blogname != '' && ! in_array( $blogname, $reserved_blognames ) && ! is_file( $blogname ) )
+					$path .= $blogname . '/';
+
+				$GLOBALS['current_blog'] = get_blog_details( array( 'domain' => $domain, 'path' => $path ), false );
+
+				unset($reserved_blognames);
+			}
+
+			$GLOBALS['blog_id'] = $GLOBALS['current_blog']->blog_id;
+		}
+
+		unset($GLOBALS['wp_query'], $GLOBALS['wp_the_query']);
+		$GLOBALS['wp_the_query'] = new WP_Query();
+		$GLOBALS['wp_query'] = $GLOBALS['wp_the_query'];
+		$GLOBALS['wp'] = new WP();
+
+		// clean out globals to stop them polluting wp and wp_query
+		foreach ($GLOBALS['wp']->public_query_vars as $v) {
+			unset($GLOBALS[$v]);
+		}
+		foreach ($GLOBALS['wp']->private_query_vars as $v) {
+			unset($GLOBALS[$v]);
+		}
+
+		$GLOBALS['wp']->main($parts['query']);
+
+		// For bbPress, James.
+		$GLOBALS['bbp']->loggedin_user = NULL;
+		do_action( 'bbp_init' );
+	}
+
+	protected function checkRequirements() {
+		if ( WP_TESTS_FORCE_KNOWN_BUGS )
+			return;
+
+		parent::checkRequirements();
+
+		$tickets = PHPUnit_Util_Test::getTickets( get_class( $this ), $this->getName( false ) );
+		foreach ( $tickets as $ticket ) {
+			if ( 'BBP' == substr( $ticket, 0, 2 ) ) {
+				$ticket = substr( $ticket, 2 );
+				if ( $ticket && is_numeric( $ticket ) )
+					$this->knownBBPBug( $ticket );
+			}
+		}
+	}
+
+	/**
+	 * Skips the current test if there is an open bbPress ticket with id $ticket_id
+	 */
+	function knownBBPBug( $ticket_id ) {
+		if ( WP_TESTS_FORCE_KNOWN_BUGS || in_array( $ticket_id, self::$forced_tickets ) )
+			return;
+
+		if ( ! TracTickets::isTracTicketClosed( 'http://buddypress.trac.wordpress.org', $ticket_id ) )
+			$this->markTestSkipped( sprintf( 'BuddyPress Ticket #%d is not fixed', $ticket_id ) );
+	}
+
+	/**
+	 * WP's core tests use wp_set_current_user() to change the current
+	 * user during tests. BBP caches the current user differently, so we
+	 * have to do a bit more work to change it
+	 *
+	 * @global bbPres $bbp
+	 */
+	function set_current_user( $user_id ) {
+		global $bbp;
+		$bbp->loggedin_user->id = $user_id;
+		$bbp->loggedin_user->fullname       = bbp_core_get_user_displayname( $user_id );
+		$bbp->loggedin_user->is_super_admin = $bbp->loggedin_user->is_site_admin = is_super_admin( $user_id );
+		$bbp->loggedin_user->domain         = bbp_core_get_user_domain( $user_id );
+		$bbp->loggedin_user->userdata       = bbp_core_get_core_userdata( $user_id );
+
+		wp_set_current_user( $user_id );
+	}
+
+	/**
+	 * When creating a new user, it's almost always necessary to have the
+	 * last_activity usermeta set right away, so that the user shows up in
+	 * directory queries. This is a shorthand wrapper for the user factory
+	 * create() method.
+	 *
+	 * Also set a display name
+	 */
+	function create_user( $args = array() ) {
+		$r = wp_parse_args( $args, array(
+			'role' => 'subscriber',
+			'last_activity' => bbp_core_current_time() - 60*60*24*365,
+		) );
+
+		$last_activity = $r['last_activity'];
+		unset( $r['last_activity'] );
+
+		$user_id = $this->factory->user->create( $args );
+
+		bbp_update_user_last_activity( $user_id, $last_activity );
+
+		if ( bbp_is_active( 'xprofile' ) ) {
+			$user = new WP_User( $user_id );
+			xprofile_set_field_data( 1, $user_id, $user->display_name );
+		}
+
+		return $user_id;
+	}
+
+	public static function add_user_to_group( $user_id, $group_id, $args = array() ) {
+		$r = wp_parse_args( $args, array(
+			'date_modified' => bbp_core_current_time(),
+			'is_confirmed' => 1,
+		) );
+
+		$new_member                = new BBP_Groups_Member;
+		$new_member->group_id      = $group_id;
+		$new_member->user_id       = $user_id;
+		$new_member->inviter_id    = 0;
+		$new_member->is_admin      = 0;
+		$new_member->user_title    = '';
+		$new_member->date_modified = $r['date_modified'];
+		$new_member->is_confirmed  = $r['is_confirmed'];
+
+		$new_member->save();
+		return $new_member->id;
+	}
+
+	/**
+	 * We can't use grant_super_admin() because we will need to modify
+	 * the list more than once, and grant_super_admin() can only be run
+	 * once because of its global check
+	 */
+	public function grant_super_admin( $user_id ) {
+		global $super_admins;
+		if ( ! is_multisite() ) {
+			return;
+		}
+
+		$user = get_userdata( $user_id );
+		$super_admins[] = $user->user_login;
+	}
+
+	public function restore_admins() {
+		// We assume that the global can be wiped out
+		// @see grant_super_admin()
+		unset( $GLOBALS['super_admins'] );
+	}
+
+	public function grant_bbp_moderate( $user_id ) {
+		if ( ! isset( $this->temp_has_bbp_moderate[ $user_id ] ) ) {
+			$this->temp_has_bbp_moderate[ $user_id ] = 1;
+		}
+		add_filter( 'bbp_current_user_can', array( $this, 'grant_bbp_moderate_cb' ), 10, 2 );
+	}
+
+	public function revoke_bbp_moderate( $user_id ) {
+		if ( isset( $this->temp_has_bbp_moderate[ $user_id ] ) ) {
+			unset( $this->temp_has_bbp_moderate[ $user_id ] );
+		}
+		remove_filter( 'bbp_current_user_can', array( $this, 'grant_bbp_moderate_cb' ), 10, 2 );
+	}
+
+	public function grant_bbp_moderate_cb( $retval, $capability ) {
+		$current_user = bbp_loggedin_user_id();
+		if ( ! isset( $this->temp_has_bbp_moderate[ $current_user ] ) ) {
+			return $retval;
+		}
+
+		if ( 'bbp_moderate' == $capability ) {
+			$retval = true;
+		}
+
+		return $retval;
+	}
+
+	/**
+	 * Go to the root blog. This helps reset globals after moving between
+	 * blogs.
+	 */
+	public function go_to_root() {
+		$blog_1_url = get_blog_option( 1, 'home' );
+		$this->go_to( str_replace( $blog_1_url, '', trailingslashit( bbp_get_root_domain() ) ) );
+	}
+
+	/**
+	 * Set up globals necessary to avoid errors when using wp_mail()
+	 */
+	public function setUp_wp_mail( $args ) {
+		if ( isset( $_SERVER['SERVER_NAME'] ) ) {
+			$this->cached_SERVER_NAME = $_SERVER['SERVER_NAME'];
+		}
+
+		$_SERVER['SERVER_NAME'] = 'example.com';
+
+		// passthrough
+		return $args;
+	}
+
+	/**
+	 * Tear down globals set up in setUp_wp_mail()
+	 */
+	public function tearDown_wp_mail( $args ) {
+		if ( ! empty( $this->cached_SERVER_NAME ) ) {
+			$_SERVER['SERVER_NAME'] = $this->cached_SERVER_NAME;
+			unset( $this->cached_SERVER_NAME );
+		} else {
+			unset( $_SERVER['SERVER_NAME'] );
+		}
+
+		// passthrough
+		return $args;
+	}
+}
Index: tests/multisite.xml
===================================================================
--- tests/multisite.xml	(revision 0)
+++ tests/multisite.xml	(working copy)
@@ -0,0 +1,17 @@
+<phpunit
+	bootstrap="bootstrap.php"
+	backupGlobals="false"
+	colors="true"
+	convertErrorsToExceptions="true"
+	convertNoticesToExceptions="true"
+	convertWarningsToExceptions="true"
+	>
+	<php>
+		<const name="WP_TESTS_MULTISITE" value="1" />
+	</php>
+	<testsuites>
+		<testsuite>
+			<directory suffix=".php">./testcases/</directory>
+		</testsuite>
+	</testsuites>
+</phpunit>
Index: tests/phpunit.xml
===================================================================
--- tests/phpunit.xml	(revision 0)
+++ tests/phpunit.xml	(working copy)
@@ -0,0 +1,14 @@
+<phpunit
+	bootstrap="bootstrap.php"
+	backupGlobals="false"
+	colors="true"
+	convertErrorsToExceptions="true"
+	convertNoticesToExceptions="true"
+	convertWarningsToExceptions="true"
+	>
+	<testsuites>
+		<testsuite>
+			<directory suffix=".php">./testcases/</directory>
+		</testsuite>
+	</testsuites>
+</phpunit>
Index: tests/bootstrap.php
===================================================================
--- tests/bootstrap.php	(revision 0)
+++ tests/bootstrap.php	(working copy)
@@ -0,0 +1,19 @@
+<?php
+
+require( dirname( __FILE__ ) . '/includes/define-constants.php' );
+
+if ( ! file_exists( WP_TESTS_DIR . '/includes/functions.php' ) ) {
+	die( "The WordPress PHPUnit test suite could not be found.\n" );
+}
+
+require_once WP_TESTS_DIR . '/includes/functions.php';
+
+function _install_and_load_bbpress() {
+	require BBP_TESTS_DIR . '/includes/loader.php';
+}
+tests_add_filter( 'muplugins_loaded', '_install_and_load_bbpress' );
+
+require WP_TESTS_DIR . '/includes/bootstrap.php';
+
+// Load the BP-specific testing tools
+require BBP_TESTS_DIR . '/includes/testcase.php';
Index: tests/includes/define-constants.php
===================================================================
--- tests/includes/define-constants.php	(revision 0)
+++ tests/includes/define-constants.php	(working copy)
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * Define constants needed by test suite.
+ */
+
+define( 'BBP_PLUGIN_DIR', dirname( dirname( dirname( __FILE__ ) ) ) . '/' );
+
+if ( ! defined( 'BBP_TESTS_DIR' ) ) {
+	define( 'BBP_TESTS_DIR', dirname( dirname( __FILE__ ) ) . '/' );
+}
+
+/**
+ * In the pre-develop.svn WP development environment, an environmental bash
+ * variable would be set to run PHP Unit tests. However, this has been done
+ * away with in a post-develop.svn world. We'll still check if this variable
+ * is set for backwards compat.
+ */
+if ( getenv( 'WP_TESTS_DIR' ) ) {
+	define( 'WP_TESTS_DIR', getenv( 'WP_TESTS_DIR' ) );
+	define( 'WP_ROOT_DIR', WP_TESTS_DIR );
+} else {
+	define( 'WP_ROOT_DIR', dirname( dirname( dirname( dirname( dirname( dirname( __DIR__ ) ) ) ) ) ) );
+	define( 'WP_TESTS_DIR', WP_ROOT_DIR . '/tests/phpunit' );
+}
+
+// Based on the tests directory, look for a config file
+if ( file_exists( WP_ROOT_DIR . '/wp-tests-config.php' ) ) {
+	// Standard develop.svn.wordpress.org setup
+	define( 'WP_TESTS_CONFIG_PATH', WP_ROOT_DIR . '/wp-tests-config.php' );
+
+} else if ( file_exists( WP_TESTS_DIR . '/wp-tests-config.php' ) ) {
+	// Legacy unit-test.svn.wordpress.org setup
+	define( 'WP_TESTS_CONFIG_PATH', WP_TESTS_DIR . '/wp-tests-config.php' );
+
+} else if ( file_exists( dirname( dirname( WP_TESTS_DIR ) ) . '/wp-tests-config.php' ) ) {
+	// Environment variable exists and points to tests/phpunit of
+	// develop.svn.wordpress.org setup
+	define( 'WP_TESTS_CONFIG_PATH', dirname( dirname( WP_TESTS_DIR ) ) . '/wp-tests-config.php' );
+
+} else {
+	die( "wp-tests-config.php could not be found.\n" );
+}
Index: tests/includes/factory.php
===================================================================
--- tests/includes/factory.php	(revision 0)
+++ tests/includes/factory.php	(working copy)
@@ -0,0 +1,165 @@
+<?php
+class BBP_UnitTest_Factory extends WP_UnitTest_Factory {
+	public $activity = null;
+
+	function __construct() {
+		parent::__construct();
+
+		$this->activity = new BBP_UnitTest_Factory_For_Activity( $this );
+		$this->group = new BBP_UnitTest_Factory_For_Group( $this );
+		$this->xprofile_group = new BBP_UnitTest_Factory_For_XProfileGroup( $this );
+		$this->xprofile_field = new BBP_UnitTest_Factory_For_XProfileField( $this );
+		$this->notification = new BBP_UnitTest_Factory_For_Notification( $this );
+	}
+}
+
+class BBP_UnitTest_Factory_For_Activity extends WP_UnitTest_Factory_For_Thing {
+
+	function __construct( $factory = null ) {
+		parent::__construct( $factory );
+
+		$this->default_generation_definitions = array(
+			'action'       => new WP_UnitTest_Generator_Sequence( 'Activity action %s' ),
+			'component'    => buddypress()->activity->id,
+			'content'      => new WP_UnitTest_Generator_Sequence( 'Activity content %s' ),
+			'primary_link' => 'http://example.com',
+			'type'         => 'activity_update',
+			'recorded_time' => bbp_core_current_time(),
+		);
+	}
+
+	function create_object( $args ) {
+		if ( ! isset( $args['user_id'] ) )
+			$args['user_id'] = get_current_user_id();
+
+		return bbp_activity_add( $args );
+	}
+
+	function update_object( $activity_id, $fields ) {
+		$activity = new BBP_Activity_Activity( $activity_id );
+
+		foreach ( $fields as $field_name => $value ) {
+			if ( isset( $activity->$field_name ) )
+				$activity->$field_name = $value;
+		}
+
+		$activity->save();
+		return $activity;
+	}
+
+	function get_object_by_id( $user_id ) {
+		return new BBP_Activity_Activity( $user_id );
+	}
+}
+
+class BBP_UnitTest_Factory_For_Group extends WP_UnitTest_Factory_For_Thing {
+
+	function __construct( $factory = null ) {
+		parent::__construct( $factory );
+
+		$this->default_generation_definitions = array(
+			'name'         => new WP_UnitTest_Generator_Sequence( 'Group %s' ),
+			'description'  => new WP_UnitTest_Generator_Sequence( 'Group description %s' ),
+			'slug'         => new WP_UnitTest_Generator_Sequence( 'group-slug-%s' ),
+			'status'       => 'public',
+			'enable_forum' => true,
+			'date_created' => bbp_core_current_time(),
+		);
+	}
+
+	function create_object( $args ) {
+		if ( ! isset( $args['creator_id'] ) ) {
+			$args['creator_id'] = get_current_user_id();
+		}
+
+		$group_id = groups_create_group( $args );
+
+		groups_update_groupmeta( $group_id, 'total_member_count', 1 );
+
+		$last_activity = isset( $args['last_activity'] ) ? $args['last_activity'] : bbp_core_current_time();
+		groups_update_groupmeta( $group_id, 'last_activity', $last_activity );
+
+		return $group_id;
+	}
+
+	function update_object( $group_id, $fields ) {
+		$group = new BBP_Groups_Group( $group_id );
+
+		foreach ( $fields as $field_name => $value ) {
+			if ( isset( $group->field_name ) )
+				$group->field_name = $value;
+		}
+
+		$group->save();
+		return $group;
+	}
+
+	function get_object_by_id( $group_id ) {
+		return new BBP_Groups_Group( $group_id );
+	}
+}
+
+class BBP_UnitTest_Factory_For_XProfileGroup extends WP_UnitTest_Factory_For_Thing {
+
+	function __construct( $factory = null ) {
+		parent::__construct( $factory );
+
+		$this->default_generation_definitions = array(
+			'name'         => new WP_UnitTest_Generator_Sequence( 'XProfile group %s' ),
+			'description'  => new WP_UnitTest_Generator_Sequence( 'XProfile group description %s' ),
+			'slug'         => new WP_UnitTest_Generator_Sequence( 'xprofile-group-slug-%s' ),
+		);
+	}
+
+	function create_object( $args ) {
+		$group_id = xprofile_insert_field_group( $args );
+		return $this->get_object_by_id( $group_id );
+	}
+
+	function update_object( $group_id, $fields ) {
+	}
+
+	function get_object_by_id( $group_id ) {
+		return new BBP_XProfile_Group( $group_id );
+	}
+}
+
+class BBP_UnitTest_Factory_For_XProfileField extends WP_UnitTest_Factory_For_Thing {
+
+	function __construct( $factory = null ) {
+		parent::__construct( $factory );
+
+		$this->default_generation_definitions = array(
+			'name'         => new WP_UnitTest_Generator_Sequence( 'XProfile field %s' ),
+			'description'  => new WP_UnitTest_Generator_Sequence( 'XProfile field description %s' ),
+		);
+	}
+
+	function create_object( $args ) {
+		$field_id = xprofile_insert_field( $args );
+		return $this->get_object_by_id( $field_id );
+	}
+
+	function update_object( $field_id, $fields ) {
+	}
+
+	function get_object_by_id( $field_id ) {
+		return new BBP_XProfile_Field( $field_id );
+	}
+}
+
+class BBP_UnitTest_Factory_For_Notification extends WP_UnitTest_Factory_For_Thing {
+	public function __construct( $factory = null ) {
+		parent::__construct( $factory );
+	}
+
+	public function create_object( $args ) {
+		return bbp_notifications_add_notification( $args );
+	}
+
+	public function update_object( $id, $fields ) {}
+
+	public function get_object_by_id( $id ) {
+		return new BBP_Notifications_Notification( $id );
+	}
+}
Index: tests/includes/install.php
===================================================================
--- tests/includes/install.php	(revision 0)
+++ tests/includes/install.php	(working copy)
@@ -0,0 +1,54 @@
+<?php
+/**
+ * Installs bbPress for the purpose of the unit-tests
+ *
+ * @todo Reuse the init/load code in init.php
+ * @todo Support MULTIBLOG
+ */
+error_reporting( E_ALL & ~E_DEPRECATED & ~E_STRICT );
+
+$config_file_path = $argv[1];
+$tests_dir_path = $argv[2];
+$multisite = ! empty( $argv[3] );
+
+require_once $config_file_path;
+require_once $tests_dir_path . '/includes/functions.php';
+
+function _load_bbpress() {
+	require dirname( dirname( dirname( __FILE__ ) ) ) . '/bbpress.php';
+}
+tests_add_filter( 'muplugins_loaded', '_load_bbpress' );
+
+define( 'BBP_PLUGIN_DIR', dirname( dirname( dirname( __FILE__ ) ) ) . '/' );
+define( 'BBP_ROOT_BLOG', 1 );
+
+// Always load admin bar
+tests_add_filter( 'show_admin_bar', '__return_true' );
+
+$_SERVER['SERVER_PROTOCOL'] = 'HTTP/1.1';
+$_SERVER['HTTP_HOST'] = WP_TESTS_DOMAIN;
+$PHP_SELF = $GLOBALS['PHP_SELF'] = $_SERVER['PHP_SELF'] = '/index.php';
+
+require_once ABSPATH . '/wp-settings.php';
+
+echo "Installing bbPress...\n";
+
+// Make sure that bbPress has been cleaned from all blogs before reinstalling
+$blogs = is_multisite() ? $wpdb->get_col( "SELECT blog_id FROM {$wpdb->blogs}" ) : array( 1 );
+foreach ( $blogs as $blog ) {
+	if ( is_multisite() ) {
+		switch_to_blog( $blog );
+	}
+
+	$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '%bbp%'" );
+
+	if ( is_multisite() ) {
+		restore_current_blog();
+	}
+}
+
+$wpdb->query( 'SET storage_engine = INNODB' );
+$wpdb->select( DB_NAME, $wpdb->dbh );
+
+// Install BuddyPress
+// bbp_version_updater();
Index: tests/includes/loader.php
===================================================================
--- tests/includes/loader.php	(revision 0)
+++ tests/includes/loader.php	(working copy)
@@ -0,0 +1,9 @@
+<?php
+
+require_once( dirname( __FILE__ ) . '/define-constants.php' );
+
+$multisite = (int) ( defined( 'WP_TESTS_MULTISITE') && WP_TESTS_MULTISITE );
+system( WP_PHP_BINARY . ' ' . escapeshellarg( dirname( __FILE__ ) . '/install.php' ) . ' ' . escapeshellarg( WP_TESTS_CONFIG_PATH ) . ' ' . escapeshellarg( WP_TESTS_DIR ) . ' ' . $multisite );
+
+// Bootstrap BBP
+require dirname( __FILE__ ) . '/../../bbpress.php';
Index: tests/includes/testcase.php
===================================================================
--- tests/includes/testcase.php	(revision 0)
+++ tests/includes/testcase.php	(working copy)
@@ -0,0 +1,336 @@
+<?php
+
+/**
+ * WP's test suite wipes out BBP's directory page mappings with _delete_all_posts()
+ * We must reestablish them before our tests can be successfully run
+ */
+
+require_once dirname( __FILE__ ) . '/factory.php';
+
+class BBP_UnitTestCase extends WP_UnitTestCase {
+
+	protected $temp_has_bbp_moderate = array();
+	protected $cached_SERVER_NAME = null;
+
+	public function setUp() {
+		parent::setUp();
+
+		// Make sure all users are deleted
+		// There's a bug in the multisite tests that causes the
+		// transaction rollback to fail for the first user created,
+		// which busts every other attempt to create users. This is a
+		// hack workaround
+		global $wpdb;
+		$wpdb->query( "TRUNCATE TABLE {$wpdb->users}" );
+
+		// Fake WP mail globals, to avoid errors
+		add_filter( 'wp_mail', array( $this, 'setUp_wp_mail' ) );
+		add_filter( 'wp_mail_from', array( $this, 'tearDown_wp_mail' ) );
+
+		$this->factory = new BBP_UnitTest_Factory;
+	}
+
+	function clean_up_global_scope() {
+		buddypress()->bbp_nav                = buddypress()->bbp_options_nav = buddypress()->action_variables = buddypress()->canonical_stack = buddypress()->unfiltered_uri = $GLOBALS['bbp_unfiltered_uri'] = array();
+		buddypress()->current_component     = buddypress()->current_item = buddypress()->current_action = '';
+		buddypress()->unfiltered_uri_offset = 0;
+		buddypress()->is_single_item        = false;
+		buddypress()->current_user          = new stdClass();
+		buddypress()->displayed_user        = new stdClass();
+		buddypress()->loggedin_user         = new stdClass();
+		buddypress()->avatar                = new stdClass();
+
+		parent::clean_up_global_scope();
+	}
+
+	function assertPreConditions() {
+		parent::assertPreConditions();
+
+		// Reinit some of the globals that might have been cleared by BBP_UnitTestCase::clean_up_global_scope().
+		// This is here because it didn't work in clean_up_global_scope(); I don't know why.
+		do_action( 'bbp_setup_globals' );
+	}
+
+	function go_to( $url ) {
+		// note: the WP and WP_Query classes like to silently fetch parameters
+		// from all over the place (globals, GET, etc), which makes it tricky
+		// to run them more than once without very carefully clearing everything
+		$_GET = $_POST = array();
+		foreach (array('query_string', 'id', 'postdata', 'authordata', 'day', 'currentmonth', 'page', 'pages', 'multipage', 'more', 'numpages', 'pagenow') as $v) {
+			if ( isset( $GLOBALS[$v] ) ) unset( $GLOBALS[$v] );
+		}
+		$parts = parse_url($url);
+		if (isset($parts['scheme'])) {
+			// set the HTTP_HOST
+			$GLOBALS['_SERVER']['HTTP_HOST'] = $parts['host'];
+
+			$req = $parts['path'];
+			if (isset($parts['query'])) {
+				$req .= '?' . $parts['query'];
+				// parse the url query vars into $_GET
+				parse_str($parts['query'], $_GET);
+			}
+		} else {
+			$req = $url;
+		}
+		if ( ! isset( $parts['query'] ) ) {
+			$parts['query'] = '';
+		}
+
+		// Scheme
+		if ( 0 === strpos( $req, '/wp-admin' ) && force_ssl_admin() ) {
+			$_SERVER['HTTPS'] = 'on';
+		} else {
+			unset( $_SERVER['HTTPS'] );
+		}
+
+		// Set this for bbp_core_set_uri_globals()
+		$GLOBALS['_SERVER']['REQUEST_URI'] = $req;
+		unset($_SERVER['PATH_INFO']);
+
+		// setup $current_site and $current_blog globals for multisite based on
+		// REQUEST_URI; mostly copied from /wp-includes/ms-settings.php
+		if ( is_multisite() ) {
+			$domain = addslashes( $_SERVER['HTTP_HOST'] );
+			if ( false !== strpos( $domain, ':' ) ) {
+				if ( substr( $domain, -3 ) == ':80' ) {
+					$domain = substr( $domain, 0, -3 );
+					$_SERVER['HTTP_HOST'] = substr( $_SERVER['HTTP_HOST'], 0, -3 );
+				} elseif ( substr( $domain, -4 ) == ':443' ) {
+					$domain = substr( $domain, 0, -4 );
+					$_SERVER['HTTP_HOST'] = substr( $_SERVER['HTTP_HOST'], 0, -4 );
+				}
+			}
+
+			$domain = rtrim( $domain, '.' );
+			$cookie_domain = $domain;
+			if ( substr( $cookie_domain, 0, 4 ) == 'www.' )
+				$cookie_domain = substr( $cookie_domain, 4 );
+
+			$path = preg_replace( '|([a-z0-9-]+.php.*)|', '', $GLOBALS['_SERVER']['REQUEST_URI'] );
+			$path = str_replace ( '/wp-admin/', '/', $path );
+			$path = preg_replace( '|(/[a-z0-9-]+?/).*|', '$1', $path );
+
+			$GLOBALS['current_site'] = wpmu_current_site();
+			if ( ! isset( $GLOBALS['current_site']->blog_id ) )
+				$GLOBALS['current_site']->blog_id = $wpdb->get_var( $wpdb->prepare( "SELECT blog_id FROM $wpdb->blogs WHERE domain = %s AND path = %s", $GLOBALS['current_site']->domain, $GLOBALS['current_site']->path ) );
+
+			// unit tests only support subdirectory install at the moment
+			// removed object cache references
+			if ( ! is_subdomain_install() ) {
+				$blogname = htmlspecialchars( substr( $GLOBALS['_SERVER']['REQUEST_URI'], strlen( $path ) ) );
+				if ( false !== strpos( $blogname, '/' ) )
+					$blogname = substr( $blogname, 0, strpos( $blogname, '/' ) );
+				if ( false !== strpos( $blogname, '?' ) )
+					$blogname = substr( $blogname, 0, strpos( $blogname, '?' ) );
+				$reserved_blognames = array( 'page', 'comments', 'blog', 'wp-admin', 'wp-includes', 'wp-content', 'files', 'feed' );
+				if ( $blogname != '' && ! in_array( $blogname, $reserved_blognames ) && ! is_file( $blogname ) )
+					$path .= $blogname . '/';
+
+				$GLOBALS['current_blog'] = get_blog_details( array( 'domain' => $domain, 'path' => $path ), false );
+
+				unset($reserved_blognames);
+			}
+
+			$GLOBALS['blog_id'] = $GLOBALS['current_blog']->blog_id;
+		}
+
+		unset($GLOBALS['wp_query'], $GLOBALS['wp_the_query']);
+		$GLOBALS['wp_the_query'] = new WP_Query();
+		$GLOBALS['wp_query'] = $GLOBALS['wp_the_query'];
+		$GLOBALS['wp'] = new WP();
+
+		// clean out globals to stop them polluting wp and wp_query
+		foreach ($GLOBALS['wp']->public_query_vars as $v) {
+			unset($GLOBALS[$v]);
+		}
+		foreach ($GLOBALS['wp']->private_query_vars as $v) {
+			unset($GLOBALS[$v]);
+		}
+
+		$GLOBALS['wp']->main($parts['query']);
+
+		// For bbPress, James.
+		$GLOBALS['bbp']->loggedin_user = NULL;
+		do_action( 'bbp_init' );
+	}
+
+	protected function checkRequirements() {
+		if ( WP_TESTS_FORCE_KNOWN_BUGS )
+			return;
+
+		parent::checkRequirements();
+
+		$tickets = PHPUnit_Util_Test::getTickets( get_class( $this ), $this->getName( false ) );
+		foreach ( $tickets as $ticket ) {
+			if ( 'BBP' == substr( $ticket, 0, 2 ) ) {
+				$ticket = substr( $ticket, 2 );
+				if ( $ticket && is_numeric( $ticket ) )
+					$this->knownBBPBug( $ticket );
+			}
+		}
+	}
+
+	/**
+	 * Skips the current test if there is an open bbPress ticket with id $ticket_id
+	 */
+	function knownBBPBug( $ticket_id ) {
+		if ( WP_TESTS_FORCE_KNOWN_BUGS || in_array( $ticket_id, self::$forced_tickets ) )
+			return;
+
+		if ( ! TracTickets::isTracTicketClosed( 'http://buddypress.trac.wordpress.org', $ticket_id ) )
+			$this->markTestSkipped( sprintf( 'BuddyPress Ticket #%d is not fixed', $ticket_id ) );
+	}
+
+	/**
+	 * WP's core tests use wp_set_current_user() to change the current
+	 * user during tests. BBP caches the current user differently, so we
+	 * have to do a bit more work to change it
+	 *
+	 * @global bbPres $bbp
+	 */
+	function set_current_user( $user_id ) {
+		global $bbp;
+		$bbp->loggedin_user->id = $user_id;
+		$bbp->loggedin_user->fullname       = bbp_core_get_user_displayname( $user_id );
+		$bbp->loggedin_user->is_super_admin = $bbp->loggedin_user->is_site_admin = is_super_admin( $user_id );
+		$bbp->loggedin_user->domain         = bbp_core_get_user_domain( $user_id );
+		$bbp->loggedin_user->userdata       = bbp_core_get_core_userdata( $user_id );
+
+		wp_set_current_user( $user_id );
+	}
+
+	/**
+	 * When creating a new user, it's almost always necessary to have the
+	 * last_activity usermeta set right away, so that the user shows up in
+	 * directory queries. This is a shorthand wrapper for the user factory
+	 * create() method.
+	 *
+	 * Also set a display name
+	 */
+	function create_user( $args = array() ) {
+		$r = wp_parse_args( $args, array(
+			'role' => 'subscriber',
+			'last_activity' => bbp_core_current_time() - 60*60*24*365,
+		) );
+
+		$last_activity = $r['last_activity'];
+		unset( $r['last_activity'] );
+
+		$user_id = $this->factory->user->create( $args );
+
+		bbp_update_user_last_activity( $user_id, $last_activity );
+
+		if ( bbp_is_active( 'xprofile' ) ) {
+			$user = new WP_User( $user_id );
+			xprofile_set_field_data( 1, $user_id, $user->display_name );
+		}
+
+		return $user_id;
+	}
+
+	public static function add_user_to_group( $user_id, $group_id, $args = array() ) {
+		$r = wp_parse_args( $args, array(
+			'date_modified' => bbp_core_current_time(),
+			'is_confirmed' => 1,
+		) );
+
+		$new_member                = new BBP_Groups_Member;
+		$new_member->group_id      = $group_id;
+		$new_member->user_id       = $user_id;
+		$new_member->inviter_id    = 0;
+		$new_member->is_admin      = 0;
+		$new_member->user_title    = '';
+		$new_member->date_modified = $r['date_modified'];
+		$new_member->is_confirmed  = $r['is_confirmed'];
+
+		$new_member->save();
+		return $new_member->id;
+	}
+
+	/**
+	 * We can't use grant_super_admin() because we will need to modify
+	 * the list more than once, and grant_super_admin() can only be run
+	 * once because of its global check
+	 */
+	public function grant_super_admin( $user_id ) {
+		global $super_admins;
+		if ( ! is_multisite() ) {
+			return;
+		}
+
+		$user = get_userdata( $user_id );
+		$super_admins[] = $user->user_login;
+	}
+
+	public function restore_admins() {
+		// We assume that the global can be wiped out
+		// @see grant_super_admin()
+		unset( $GLOBALS['super_admins'] );
+	}
+
+	public function grant_bbp_moderate( $user_id ) {
+		if ( ! isset( $this->temp_has_bbp_moderate[ $user_id ] ) ) {
+			$this->temp_has_bbp_moderate[ $user_id ] = 1;
+		}
+		add_filter( 'bbp_current_user_can', array( $this, 'grant_bbp_moderate_cb' ), 10, 2 );
+	}
+
+	public function revoke_bbp_moderate( $user_id ) {
+		if ( isset( $this->temp_has_bbp_moderate[ $user_id ] ) ) {
+			unset( $this->temp_has_bbp_moderate[ $user_id ] );
+		}
+		remove_filter( 'bbp_current_user_can', array( $this, 'grant_bbp_moderate_cb' ), 10, 2 );
+	}
+
+	public function grant_bbp_moderate_cb( $retval, $capability ) {
+		$current_user = bbp_loggedin_user_id();
+		if ( ! isset( $this->temp_has_bbp_moderate[ $current_user ] ) ) {
+			return $retval;
+		}
+
+		if ( 'bbp_moderate' == $capability ) {
+			$retval = true;
+		}
+
+		return $retval;
+	}
+
+	/**
+	 * Go to the root blog. This helps reset globals after moving between
+	 * blogs.
+	 */
+	public function go_to_root() {
+		$blog_1_url = get_blog_option( 1, 'home' );
+		$this->go_to( str_replace( $blog_1_url, '', trailingslashit( bbp_get_root_domain() ) ) );
+	}
+
+	/**
+	 * Set up globals necessary to avoid errors when using wp_mail()
+	 */
+	public function setUp_wp_mail( $args ) {
+		if ( isset( $_SERVER['SERVER_NAME'] ) ) {
+			$this->cached_SERVER_NAME = $_SERVER['SERVER_NAME'];
+		}
+
+		$_SERVER['SERVER_NAME'] = 'example.com';
+
+		// passthrough
+		return $args;
+	}
+
+	/**
+	 * Tear down globals set up in setUp_wp_mail()
+	 */
+	public function tearDown_wp_mail( $args ) {
+		if ( ! empty( $this->cached_SERVER_NAME ) ) {
+			$_SERVER['SERVER_NAME'] = $this->cached_SERVER_NAME;
+			unset( $this->cached_SERVER_NAME );
+		} else {
+			unset( $_SERVER['SERVER_NAME'] );
+		}
+
+		// passthrough
+		return $args;
+	}
+}
Index: tests/includes/define-constants.php
===================================================================
--- tests/includes/define-constants.php	(revision 0)
+++ tests/includes/define-constants.php	(working copy)
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * Define constants needed by test suite.
+ */
+
+define( 'BBP_PLUGIN_DIR', dirname( dirname( dirname( __FILE__ ) ) ) . '/' );
+
+if ( ! defined( 'BBP_TESTS_DIR' ) ) {
+	define( 'BBP_TESTS_DIR', dirname( dirname( __FILE__ ) ) . '/' );
+}
+
+/**
+ * In the pre-develop.svn WP development environment, an environmental bash
+ * variable would be set to run PHP Unit tests. However, this has been done
+ * away with in a post-develop.svn world. We'll still check if this variable
+ * is set for backwards compat.
+ */
+if ( getenv( 'WP_TESTS_DIR' ) ) {
+	define( 'WP_TESTS_DIR', getenv( 'WP_TESTS_DIR' ) );
+	define( 'WP_ROOT_DIR', WP_TESTS_DIR );
+} else {
+	define( 'WP_ROOT_DIR', dirname( dirname( dirname( dirname( dirname( dirname( __DIR__ ) ) ) ) ) ) );
+	define( 'WP_TESTS_DIR', WP_ROOT_DIR . '/tests/phpunit' );
+}
+
+// Based on the tests directory, look for a config file
+if ( file_exists( WP_ROOT_DIR . '/wp-tests-config.php' ) ) {
+	// Standard develop.svn.wordpress.org setup
+	define( 'WP_TESTS_CONFIG_PATH', WP_ROOT_DIR . '/wp-tests-config.php' );
+
+} else if ( file_exists( WP_TESTS_DIR . '/wp-tests-config.php' ) ) {
+	// Legacy unit-test.svn.wordpress.org setup
+	define( 'WP_TESTS_CONFIG_PATH', WP_TESTS_DIR . '/wp-tests-config.php' );
+
+} else if ( file_exists( dirname( dirname( WP_TESTS_DIR ) ) . '/wp-tests-config.php' ) ) {
+	// Environment variable exists and points to tests/phpunit of
+	// develop.svn.wordpress.org setup
+	define( 'WP_TESTS_CONFIG_PATH', dirname( dirname( WP_TESTS_DIR ) ) . '/wp-tests-config.php' );
+
+} else {
+	die( "wp-tests-config.php could not be found.\n" );
+}
Index: tests/includes/factory.php
===================================================================
--- tests/includes/factory.php	(revision 0)
+++ tests/includes/factory.php	(working copy)
@@ -0,0 +1,165 @@
+<?php
+class BBP_UnitTest_Factory extends WP_UnitTest_Factory {
+	public $activity = null;
+
+	function __construct() {
+		parent::__construct();
+
+		$this->activity = new BBP_UnitTest_Factory_For_Activity( $this );
+		$this->group = new BBP_UnitTest_Factory_For_Group( $this );
+		$this->xprofile_group = new BBP_UnitTest_Factory_For_XProfileGroup( $this );
+		$this->xprofile_field = new BBP_UnitTest_Factory_For_XProfileField( $this );
+		$this->notification = new BBP_UnitTest_Factory_For_Notification( $this );
+	}
+}
+
+class BBP_UnitTest_Factory_For_Activity extends WP_UnitTest_Factory_For_Thing {
+
+	function __construct( $factory = null ) {
+		parent::__construct( $factory );
+
+		$this->default_generation_definitions = array(
+			'action'       => new WP_UnitTest_Generator_Sequence( 'Activity action %s' ),
+			'component'    => buddypress()->activity->id,
+			'content'      => new WP_UnitTest_Generator_Sequence( 'Activity content %s' ),
+			'primary_link' => 'http://example.com',
+			'type'         => 'activity_update',
+			'recorded_time' => bbp_core_current_time(),
+		);
+	}
+
+	function create_object( $args ) {
+		if ( ! isset( $args['user_id'] ) )
+			$args['user_id'] = get_current_user_id();
+
+		return bbp_activity_add( $args );
+	}
+
+	function update_object( $activity_id, $fields ) {
+		$activity = new BBP_Activity_Activity( $activity_id );
+
+		foreach ( $fields as $field_name => $value ) {
+			if ( isset( $activity->$field_name ) )
+				$activity->$field_name = $value;
+		}
+
+		$activity->save();
+		return $activity;
+	}
+
+	function get_object_by_id( $user_id ) {
+		return new BBP_Activity_Activity( $user_id );
+	}
+}
+
+class BBP_UnitTest_Factory_For_Group extends WP_UnitTest_Factory_For_Thing {
+
+	function __construct( $factory = null ) {
+		parent::__construct( $factory );
+
+		$this->default_generation_definitions = array(
+			'name'         => new WP_UnitTest_Generator_Sequence( 'Group %s' ),
+			'description'  => new WP_UnitTest_Generator_Sequence( 'Group description %s' ),
+			'slug'         => new WP_UnitTest_Generator_Sequence( 'group-slug-%s' ),
+			'status'       => 'public',
+			'enable_forum' => true,
+			'date_created' => bbp_core_current_time(),
+		);
+	}
+
+	function create_object( $args ) {
+		if ( ! isset( $args['creator_id'] ) ) {
+			$args['creator_id'] = get_current_user_id();
+		}
+
+		$group_id = groups_create_group( $args );
+
+		groups_update_groupmeta( $group_id, 'total_member_count', 1 );
+
+		$last_activity = isset( $args['last_activity'] ) ? $args['last_activity'] : bbp_core_current_time();
+		groups_update_groupmeta( $group_id, 'last_activity', $last_activity );
+
+		return $group_id;
+	}
+
+	function update_object( $group_id, $fields ) {
+		$group = new BBP_Groups_Group( $group_id );
+
+		foreach ( $fields as $field_name => $value ) {
+			if ( isset( $group->field_name ) )
+				$group->field_name = $value;
+		}
+
+		$group->save();
+		return $group;
+	}
+
+	function get_object_by_id( $group_id ) {
+		return new BBP_Groups_Group( $group_id );
+	}
+}
+
+class BBP_UnitTest_Factory_For_XProfileGroup extends WP_UnitTest_Factory_For_Thing {
+
+	function __construct( $factory = null ) {
+		parent::__construct( $factory );
+
+		$this->default_generation_definitions = array(
+			'name'         => new WP_UnitTest_Generator_Sequence( 'XProfile group %s' ),
+			'description'  => new WP_UnitTest_Generator_Sequence( 'XProfile group description %s' ),
+			'slug'         => new WP_UnitTest_Generator_Sequence( 'xprofile-group-slug-%s' ),
+		);
+	}
+
+	function create_object( $args ) {
+		$group_id = xprofile_insert_field_group( $args );
+		return $this->get_object_by_id( $group_id );
+	}
+
+	function update_object( $group_id, $fields ) {
+	}
+
+	function get_object_by_id( $group_id ) {
+		return new BBP_XProfile_Group( $group_id );
+	}
+}
+
+class BBP_UnitTest_Factory_For_XProfileField extends WP_UnitTest_Factory_For_Thing {
+
+	function __construct( $factory = null ) {
+		parent::__construct( $factory );
+
+		$this->default_generation_definitions = array(
+			'name'         => new WP_UnitTest_Generator_Sequence( 'XProfile field %s' ),
+			'description'  => new WP_UnitTest_Generator_Sequence( 'XProfile field description %s' ),
+		);
+	}
+
+	function create_object( $args ) {
+		$field_id = xprofile_insert_field( $args );
+		return $this->get_object_by_id( $field_id );
+	}
+
+	function update_object( $field_id, $fields ) {
+	}
+
+	function get_object_by_id( $field_id ) {
+		return new BBP_XProfile_Field( $field_id );
+	}
+}
+
+class BBP_UnitTest_Factory_For_Notification extends WP_UnitTest_Factory_For_Thing {
+	public function __construct( $factory = null ) {
+		parent::__construct( $factory );
+	}
+
+	public function create_object( $args ) {
+		return bbp_notifications_add_notification( $args );
+	}
+
+	public function update_object( $id, $fields ) {}
+
+	public function get_object_by_id( $id ) {
+		return new BBP_Notifications_Notification( $id );
+	}
+}
Index: tests/includes/install.php
===================================================================
--- tests/includes/install.php	(revision 0)
+++ tests/includes/install.php	(working copy)
@@ -0,0 +1,54 @@
+<?php
+/**
+ * Installs bbPress for the purpose of the unit-tests
+ *
+ * @todo Reuse the init/load code in init.php
+ * @todo Support MULTIBLOG
+ */
+error_reporting( E_ALL & ~E_DEPRECATED & ~E_STRICT );
+
+$config_file_path = $argv[1];
+$tests_dir_path = $argv[2];
+$multisite = ! empty( $argv[3] );
+
+require_once $config_file_path;
+require_once $tests_dir_path . '/includes/functions.php';
+
+function _load_bbpress() {
+	require dirname( dirname( dirname( __FILE__ ) ) ) . '/bbpress.php';
+}
+tests_add_filter( 'muplugins_loaded', '_load_bbpress' );
+
+define( 'BBP_PLUGIN_DIR', dirname( dirname( dirname( __FILE__ ) ) ) . '/' );
+define( 'BBP_ROOT_BLOG', 1 );
+
+// Always load admin bar
+tests_add_filter( 'show_admin_bar', '__return_true' );
+
+$_SERVER['SERVER_PROTOCOL'] = 'HTTP/1.1';
+$_SERVER['HTTP_HOST'] = WP_TESTS_DOMAIN;
+$PHP_SELF = $GLOBALS['PHP_SELF'] = $_SERVER['PHP_SELF'] = '/index.php';
+
+require_once ABSPATH . '/wp-settings.php';
+
+echo "Installing bbPress...\n";
+
+// Make sure that bbPress has been cleaned from all blogs before reinstalling
+$blogs = is_multisite() ? $wpdb->get_col( "SELECT blog_id FROM {$wpdb->blogs}" ) : array( 1 );
+foreach ( $blogs as $blog ) {
+	if ( is_multisite() ) {
+		switch_to_blog( $blog );
+	}
+
+	$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '%bbp%'" );
+
+	if ( is_multisite() ) {
+		restore_current_blog();
+	}
+}
+
+$wpdb->query( 'SET storage_engine = INNODB' );
+$wpdb->select( DB_NAME, $wpdb->dbh );
+
+// Install BuddyPress
+// bbp_version_updater();
Index: tests/includes/loader.php
===================================================================
--- tests/includes/loader.php	(revision 0)
+++ tests/includes/loader.php	(working copy)
@@ -0,0 +1,9 @@
+<?php
+
+require_once( dirname( __FILE__ ) . '/define-constants.php' );
+
+$multisite = (int) ( defined( 'WP_TESTS_MULTISITE') && WP_TESTS_MULTISITE );
+system( WP_PHP_BINARY . ' ' . escapeshellarg( dirname( __FILE__ ) . '/install.php' ) . ' ' . escapeshellarg( WP_TESTS_CONFIG_PATH ) . ' ' . escapeshellarg( WP_TESTS_DIR ) . ' ' . $multisite );
+
+// Bootstrap BBP
+require dirname( __FILE__ ) . '/../../bbpress.php';
Index: tests/includes/testcase.php
===================================================================
--- tests/includes/testcase.php	(revision 0)
+++ tests/includes/testcase.php	(working copy)
@@ -0,0 +1,336 @@
+<?php
+
+/**
+ * WP's test suite wipes out BBP's directory page mappings with _delete_all_posts()
+ * We must reestablish them before our tests can be successfully run
+ */
+
+require_once dirname( __FILE__ ) . '/factory.php';
+
+class BBP_UnitTestCase extends WP_UnitTestCase {
+
+	protected $temp_has_bbp_moderate = array();
+	protected $cached_SERVER_NAME = null;
+
+	public function setUp() {
+		parent::setUp();
+
+		// Make sure all users are deleted
+		// There's a bug in the multisite tests that causes the
+		// transaction rollback to fail for the first user created,
+		// which busts every other attempt to create users. This is a
+		// hack workaround
+		global $wpdb;
+		$wpdb->query( "TRUNCATE TABLE {$wpdb->users}" );
+
+		// Fake WP mail globals, to avoid errors
+		add_filter( 'wp_mail', array( $this, 'setUp_wp_mail' ) );
+		add_filter( 'wp_mail_from', array( $this, 'tearDown_wp_mail' ) );
+
+		$this->factory = new BBP_UnitTest_Factory;
+	}
+
+	function clean_up_global_scope() {
+		buddypress()->bbp_nav                = buddypress()->bbp_options_nav = buddypress()->action_variables = buddypress()->canonical_stack = buddypress()->unfiltered_uri = $GLOBALS['bbp_unfiltered_uri'] = array();
+		buddypress()->current_component     = buddypress()->current_item = buddypress()->current_action = '';
+		buddypress()->unfiltered_uri_offset = 0;
+		buddypress()->is_single_item        = false;
+		buddypress()->current_user          = new stdClass();
+		buddypress()->displayed_user        = new stdClass();
+		buddypress()->loggedin_user         = new stdClass();
+		buddypress()->avatar                = new stdClass();
+
+		parent::clean_up_global_scope();
+	}
+
+	function assertPreConditions() {
+		parent::assertPreConditions();
+
+		// Reinit some of the globals that might have been cleared by BBP_UnitTestCase::clean_up_global_scope().
+		// This is here because it didn't work in clean_up_global_scope(); I don't know why.
+		do_action( 'bbp_setup_globals' );
+	}
+
+	function go_to( $url ) {
+		// note: the WP and WP_Query classes like to silently fetch parameters
+		// from all over the place (globals, GET, etc), which makes it tricky
+		// to run them more than once without very carefully clearing everything
+		$_GET = $_POST = array();
+		foreach (array('query_string', 'id', 'postdata', 'authordata', 'day', 'currentmonth', 'page', 'pages', 'multipage', 'more', 'numpages', 'pagenow') as $v) {
+			if ( isset( $GLOBALS[$v] ) ) unset( $GLOBALS[$v] );
+		}
+		$parts = parse_url($url);
+		if (isset($parts['scheme'])) {
+			// set the HTTP_HOST
+			$GLOBALS['_SERVER']['HTTP_HOST'] = $parts['host'];
+
+			$req = $parts['path'];
+			if (isset($parts['query'])) {
+				$req .= '?' . $parts['query'];
+				// parse the url query vars into $_GET
+				parse_str($parts['query'], $_GET);
+			}
+		} else {
+			$req = $url;
+		}
+		if ( ! isset( $parts['query'] ) ) {
+			$parts['query'] = '';
+		}
+
+		// Scheme
+		if ( 0 === strpos( $req, '/wp-admin' ) && force_ssl_admin() ) {
+			$_SERVER['HTTPS'] = 'on';
+		} else {
+			unset( $_SERVER['HTTPS'] );
+		}
+
+		// Set this for bbp_core_set_uri_globals()
+		$GLOBALS['_SERVER']['REQUEST_URI'] = $req;
+		unset($_SERVER['PATH_INFO']);
+
+		// setup $current_site and $current_blog globals for multisite based on
+		// REQUEST_URI; mostly copied from /wp-includes/ms-settings.php
+		if ( is_multisite() ) {
+			$domain = addslashes( $_SERVER['HTTP_HOST'] );
+			if ( false !== strpos( $domain, ':' ) ) {
+				if ( substr( $domain, -3 ) == ':80' ) {
+					$domain = substr( $domain, 0, -3 );
+					$_SERVER['HTTP_HOST'] = substr( $_SERVER['HTTP_HOST'], 0, -3 );
+				} elseif ( substr( $domain, -4 ) == ':443' ) {
+					$domain = substr( $domain, 0, -4 );
+					$_SERVER['HTTP_HOST'] = substr( $_SERVER['HTTP_HOST'], 0, -4 );
+				}
+			}
+
+			$domain = rtrim( $domain, '.' );
+			$cookie_domain = $domain;
+			if ( substr( $cookie_domain, 0, 4 ) == 'www.' )
+				$cookie_domain = substr( $cookie_domain, 4 );
+
+			$path = preg_replace( '|([a-z0-9-]+.php.*)|', '', $GLOBALS['_SERVER']['REQUEST_URI'] );
+			$path = str_replace ( '/wp-admin/', '/', $path );
+			$path = preg_replace( '|(/[a-z0-9-]+?/).*|', '$1', $path );
+
+			$GLOBALS['current_site'] = wpmu_current_site();
+			if ( ! isset( $GLOBALS['current_site']->blog_id ) )
+				$GLOBALS['current_site']->blog_id = $wpdb->get_var( $wpdb->prepare( "SELECT blog_id FROM $wpdb->blogs WHERE domain = %s AND path = %s", $GLOBALS['current_site']->domain, $GLOBALS['current_site']->path ) );
+
+			// unit tests only support subdirectory install at the moment
+			// removed object cache references
+			if ( ! is_subdomain_install() ) {
+				$blogname = htmlspecialchars( substr( $GLOBALS['_SERVER']['REQUEST_URI'], strlen( $path ) ) );
+				if ( false !== strpos( $blogname, '/' ) )
+					$blogname = substr( $blogname, 0, strpos( $blogname, '/' ) );
+				if ( false !== strpos( $blogname, '?' ) )
+					$blogname = substr( $blogname, 0, strpos( $blogname, '?' ) );
+				$reserved_blognames = array( 'page', 'comments', 'blog', 'wp-admin', 'wp-includes', 'wp-content', 'files', 'feed' );
+				if ( $blogname != '' && ! in_array( $blogname, $reserved_blognames ) && ! is_file( $blogname ) )
+					$path .= $blogname . '/';
+
+				$GLOBALS['current_blog'] = get_blog_details( array( 'domain' => $domain, 'path' => $path ), false );
+
+				unset($reserved_blognames);
+			}
+
+			$GLOBALS['blog_id'] = $GLOBALS['current_blog']->blog_id;
+		}
+
+		unset($GLOBALS['wp_query'], $GLOBALS['wp_the_query']);
+		$GLOBALS['wp_the_query'] = new WP_Query();
+		$GLOBALS['wp_query'] = $GLOBALS['wp_the_query'];
+		$GLOBALS['wp'] = new WP();
+
+		// clean out globals to stop them polluting wp and wp_query
+		foreach ($GLOBALS['wp']->public_query_vars as $v) {
+			unset($GLOBALS[$v]);
+		}
+		foreach ($GLOBALS['wp']->private_query_vars as $v) {
+			unset($GLOBALS[$v]);
+		}
+
+		$GLOBALS['wp']->main($parts['query']);
+
+		// For bbPress, James.
+		$GLOBALS['bbp']->loggedin_user = NULL;
+		do_action( 'bbp_init' );
+	}
+
+	protected function checkRequirements() {
+		if ( WP_TESTS_FORCE_KNOWN_BUGS )
+			return;
+
+		parent::checkRequirements();
+
+		$tickets = PHPUnit_Util_Test::getTickets( get_class( $this ), $this->getName( false ) );
+		foreach ( $tickets as $ticket ) {
+			if ( 'BBP' == substr( $ticket, 0, 2 ) ) {
+				$ticket = substr( $ticket, 2 );
+				if ( $ticket && is_numeric( $ticket ) )
+					$this->knownBBPBug( $ticket );
+			}
+		}
+	}
+
+	/**
+	 * Skips the current test if there is an open bbPress ticket with id $ticket_id
+	 */
+	function knownBBPBug( $ticket_id ) {
+		if ( WP_TESTS_FORCE_KNOWN_BUGS || in_array( $ticket_id, self::$forced_tickets ) )
+			return;
+
+		if ( ! TracTickets::isTracTicketClosed( 'http://buddypress.trac.wordpress.org', $ticket_id ) )
+			$this->markTestSkipped( sprintf( 'BuddyPress Ticket #%d is not fixed', $ticket_id ) );
+	}
+
+	/**
+	 * WP's core tests use wp_set_current_user() to change the current
+	 * user during tests. BBP caches the current user differently, so we
+	 * have to do a bit more work to change it
+	 *
+	 * @global bbPres $bbp
+	 */
+	function set_current_user( $user_id ) {
+		global $bbp;
+		$bbp->loggedin_user->id = $user_id;
+		$bbp->loggedin_user->fullname       = bbp_core_get_user_displayname( $user_id );
+		$bbp->loggedin_user->is_super_admin = $bbp->loggedin_user->is_site_admin = is_super_admin( $user_id );
+		$bbp->loggedin_user->domain         = bbp_core_get_user_domain( $user_id );
+		$bbp->loggedin_user->userdata       = bbp_core_get_core_userdata( $user_id );
+
+		wp_set_current_user( $user_id );
+	}
+
+	/**
+	 * When creating a new user, it's almost always necessary to have the
+	 * last_activity usermeta set right away, so that the user shows up in
+	 * directory queries. This is a shorthand wrapper for the user factory
+	 * create() method.
+	 *
+	 * Also set a display name
+	 */
+	function create_user( $args = array() ) {
+		$r = wp_parse_args( $args, array(
+			'role' => 'subscriber',
+			'last_activity' => bbp_core_current_time() - 60*60*24*365,
+		) );
+
+		$last_activity = $r['last_activity'];
+		unset( $r['last_activity'] );
+
+		$user_id = $this->factory->user->create( $args );
+
+		bbp_update_user_last_activity( $user_id, $last_activity );
+
+		if ( bbp_is_active( 'xprofile' ) ) {
+			$user = new WP_User( $user_id );
+			xprofile_set_field_data( 1, $user_id, $user->display_name );
+		}
+
+		return $user_id;
+	}
+
+	public static function add_user_to_group( $user_id, $group_id, $args = array() ) {
+		$r = wp_parse_args( $args, array(
+			'date_modified' => bbp_core_current_time(),
+			'is_confirmed' => 1,
+		) );
+
+		$new_member                = new BBP_Groups_Member;
+		$new_member->group_id      = $group_id;
+		$new_member->user_id       = $user_id;
+		$new_member->inviter_id    = 0;
+		$new_member->is_admin      = 0;
+		$new_member->user_title    = '';
+		$new_member->date_modified = $r['date_modified'];
+		$new_member->is_confirmed  = $r['is_confirmed'];
+
+		$new_member->save();
+		return $new_member->id;
+	}
+
+	/**
+	 * We can't use grant_super_admin() because we will need to modify
+	 * the list more than once, and grant_super_admin() can only be run
+	 * once because of its global check
+	 */
+	public function grant_super_admin( $user_id ) {
+		global $super_admins;
+		if ( ! is_multisite() ) {
+			return;
+		}
+
+		$user = get_userdata( $user_id );
+		$super_admins[] = $user->user_login;
+	}
+
+	public function restore_admins() {
+		// We assume that the global can be wiped out
+		// @see grant_super_admin()
+		unset( $GLOBALS['super_admins'] );
+	}
+
+	public function grant_bbp_moderate( $user_id ) {
+		if ( ! isset( $this->temp_has_bbp_moderate[ $user_id ] ) ) {
+			$this->temp_has_bbp_moderate[ $user_id ] = 1;
+		}
+		add_filter( 'bbp_current_user_can', array( $this, 'grant_bbp_moderate_cb' ), 10, 2 );
+	}
+
+	public function revoke_bbp_moderate( $user_id ) {
+		if ( isset( $this->temp_has_bbp_moderate[ $user_id ] ) ) {
+			unset( $this->temp_has_bbp_moderate[ $user_id ] );
+		}
+		remove_filter( 'bbp_current_user_can', array( $this, 'grant_bbp_moderate_cb' ), 10, 2 );
+	}
+
+	public function grant_bbp_moderate_cb( $retval, $capability ) {
+		$current_user = bbp_loggedin_user_id();
+		if ( ! isset( $this->temp_has_bbp_moderate[ $current_user ] ) ) {
+			return $retval;
+		}
+
+		if ( 'bbp_moderate' == $capability ) {
+			$retval = true;
+		}
+
+		return $retval;
+	}
+
+	/**
+	 * Go to the root blog. This helps reset globals after moving between
+	 * blogs.
+	 */
+	public function go_to_root() {
+		$blog_1_url = get_blog_option( 1, 'home' );
+		$this->go_to( str_replace( $blog_1_url, '', trailingslashit( bbp_get_root_domain() ) ) );
+	}
+
+	/**
+	 * Set up globals necessary to avoid errors when using wp_mail()
+	 */
+	public function setUp_wp_mail( $args ) {
+		if ( isset( $_SERVER['SERVER_NAME'] ) ) {
+			$this->cached_SERVER_NAME = $_SERVER['SERVER_NAME'];
+		}
+
+		$_SERVER['SERVER_NAME'] = 'example.com';
+
+		// passthrough
+		return $args;
+	}
+
+	/**
+	 * Tear down globals set up in setUp_wp_mail()
+	 */
+	public function tearDown_wp_mail( $args ) {
+		if ( ! empty( $this->cached_SERVER_NAME ) ) {
+			$_SERVER['SERVER_NAME'] = $this->cached_SERVER_NAME;
+			unset( $this->cached_SERVER_NAME );
+		} else {
+			unset( $_SERVER['SERVER_NAME'] );
+		}
+
+		// passthrough
+		return $args;
+	}
+}
Index: tests/multisite.xml
===================================================================
--- tests/multisite.xml	(revision 0)
+++ tests/multisite.xml	(working copy)
@@ -0,0 +1,17 @@
+<phpunit
+	bootstrap="bootstrap.php"
+	backupGlobals="false"
+	colors="true"
+	convertErrorsToExceptions="true"
+	convertNoticesToExceptions="true"
+	convertWarningsToExceptions="true"
+	>
+	<php>
+		<const name="WP_TESTS_MULTISITE" value="1" />
+	</php>
+	<testsuites>
+		<testsuite>
+			<directory suffix=".php">./testcases/</directory>
+		</testsuite>
+	</testsuites>
+</phpunit>
Index: tests/phpunit.xml
===================================================================
--- tests/phpunit.xml	(revision 0)
+++ tests/phpunit.xml	(working copy)
@@ -0,0 +1,14 @@
+<phpunit
+	bootstrap="bootstrap.php"
+	backupGlobals="false"
+	colors="true"
+	convertErrorsToExceptions="true"
+	convertNoticesToExceptions="true"
+	convertWarningsToExceptions="true"
+	>
+	<testsuites>
+		<testsuite>
+			<directory suffix=".php">./testcases/</directory>
+		</testsuite>
+	</testsuites>
+</phpunit>
