diff --git a/src/wp-includes/capabilities.php b/src/wp-includes/capabilities.php
index fe87bc8..4027379 100644
--- a/src/wp-includes/capabilities.php
+++ b/src/wp-includes/capabilities.php
@@ -242,56 +242,77 @@ function map_meta_cap( $cap, $user_id ) {
 	case 'edit_post_meta':
 	case 'delete_post_meta':
 	case 'add_post_meta':
-		$post = get_post( $args[0] );
-		if ( ! $post ) {
+	case 'edit_comment_meta':
+	case 'delete_comment_meta':
+	case 'add_comment_meta':
+	case 'edit_term_meta':
+	case 'delete_term_meta':
+	case 'add_term_meta':
+	case 'edit_user_meta':
+	case 'delete_user_meta':
+	case 'add_user_meta':
+		list( $_, $object_type, $_ ) = explode( '_', $cap );
+		$object_id = (int) $args[0];
+
+		switch ( $object_type ) {
+			case 'post':
+				$post = get_post( $object_id );
+				if ( ! $post ) {
+					break;
+				}
+
+				$sub_type = get_post_type( $post );
+				break;
+
+			case 'comment':
+				$comment = get_comment( $object_id );
+				if ( ! $comment ) {
+					break;
+				}
+
+				$sub_type = empty( $comment->comment_type ) ? 'comment' : $comment->comment_type;
+				break;
+
+			case 'term':
+				$term = get_term( $object_id );
+				if ( ! $term ) {
+					break;
+				}
+
+				$sub_type = $term->taxonomy;
+				break;
+
+			case 'user':
+				$user = get_user_by( 'id', $object_id );
+				if ( ! $user ) {
+					break;
+				}
+
+				$sub_type = 'user';
+				break;
+		}
+
+		if ( empty( $sub_type ) ) {
 			$caps[] = 'do_not_allow';
 			break;
 		}
 
-		$post_type = get_post_type( $post );
+		$caps = map_meta_cap( "edit_{$object_type}", $user_id, $object_id );
 
-		$caps = map_meta_cap( 'edit_post', $user_id, $post->ID );
+		$meta_key = isset( $args[1] ) ? $args[1] : false;
 
-		$meta_key = isset( $args[ 1 ] ) ? $args[ 1 ] : false;
+		$has_filter = has_filter( "auth_{$object_type}_meta_{$meta_key}" ) || has_filter( "auth_{$object_type}_{$sub_type}_meta_{$meta_key}" );
+		if ( $meta_key && $has_filter ) {
+			/** This filter is documented in wp-includes/meta.php */
+			$allowed = apply_filters( "auth_{$object_type}_meta_{$meta_key}", false, $meta_key, $object_id, $user_id, $cap, $caps );
 
-		if ( $meta_key && ( has_filter( "auth_post_meta_{$meta_key}" ) || has_filter( "auth_post_{$post_type}_meta_{$meta_key}" ) ) ) {
-			/**
-			 * Filters whether the user is allowed to add post meta to a post.
-			 *
-			 * The dynamic portion of the hook name, `$meta_key`, refers to the
-			 * meta key passed to map_meta_cap().
-			 *
-			 * @since 3.3.0
-			 *
-			 * @param bool   $allowed  Whether the user can add the post meta. Default false.
-			 * @param string $meta_key The meta key.
-			 * @param int    $post_id  Post ID.
-			 * @param int    $user_id  User ID.
-			 * @param string $cap      Capability name.
-			 * @param array  $caps     User capabilities.
-			 */
-			$allowed = apply_filters( "auth_post_meta_{$meta_key}", false, $meta_key, $post->ID, $user_id, $cap, $caps );
+			/** This filter is documented in wp-includes/meta.php */
+			$allowed = apply_filters( "auth_{$object_type}_{$sub_type}_meta_{$meta_key}", $allowed, $meta_key, $object_id, $user_id, $cap, $caps );
 
-			/**
-			 * Filters whether the user is allowed to add post meta to a post of a given type.
-			 *
-			 * The dynamic portions of the hook name, `$meta_key` and `$post_type`,
-			 * refer to the meta key passed to map_meta_cap() and the post type, respectively.
-			 *
-			 * @since 4.6.0
-			 *
-			 * @param bool   $allowed  Whether the user can add the post meta. Default false.
-			 * @param string $meta_key The meta key.
-			 * @param int    $post_id  Post ID.
-			 * @param int    $user_id  User ID.
-			 * @param string $cap      Capability name.
-			 * @param array  $caps     User capabilities.
-			 */
-			$allowed = apply_filters( "auth_post_{$post_type}_meta_{$meta_key}", $allowed, $meta_key, $post->ID, $user_id, $cap, $caps );
-
-			if ( ! $allowed )
+			if ( ! $allowed ) {
 				$caps[] = $cap;
-		} elseif ( $meta_key && is_protected_meta( $meta_key, 'post' ) ) {
+			}
+		} elseif ( $meta_key && is_protected_meta( $meta_key, $object_type ) ) {
 			$caps[] = $cap;
 		}
 		break;
diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php
index e6f7920..255667e 100644
--- a/src/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php
+++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php
@@ -631,7 +631,7 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
 			if ( ! $change ) {
 				return new WP_Error( 'rest_comment_failed_edit', __( 'Updating comment status failed.' ), array( 'status' => 500 ) );
 			}
-		} else {
+		} elseif ( ! empty( $prepared_args ) ) {
 			if ( is_wp_error( $prepared_args ) ) {
 				return $prepared_args;
 			}
diff --git a/src/wp-includes/rest-api/fields/class-wp-rest-meta-fields.php b/src/wp-includes/rest-api/fields/class-wp-rest-meta-fields.php
index 2c765ca..e48a307 100644
--- a/src/wp-includes/rest-api/fields/class-wp-rest-meta-fields.php
+++ b/src/wp-includes/rest-api/fields/class-wp-rest-meta-fields.php
@@ -122,7 +122,6 @@ abstract class WP_REST_Meta_Fields {
 	 */
 	public function update_value( $request, $object_id ) {
 		$fields = $this->get_registered_fields();
-
 		foreach ( $fields as $name => $args ) {
 			if ( ! array_key_exists( $name, $request ) ) {
 				continue;
@@ -159,7 +158,8 @@ abstract class WP_REST_Meta_Fields {
 	 * @return bool|WP_Error True if meta field is deleted, WP_Error otherwise.
 	 */
 	protected function delete_meta_value( $object_id, $name ) {
-		if ( ! current_user_can( 'delete_post_meta', $object_id, $name ) ) {
+		$meta_type = $this->get_meta_type();
+		if ( ! current_user_can( "delete_{$meta_type}_meta", $object_id, $name ) ) {
 			return new WP_Error(
 				'rest_cannot_delete',
 				sprintf( __( 'You do not have permission to edit the %s custom field.' ), $name ),
@@ -167,7 +167,7 @@ abstract class WP_REST_Meta_Fields {
 			);
 		}
 
-		if ( ! delete_metadata( $this->get_meta_type(), $object_id, wp_slash( $name ) ) ) {
+		if ( ! delete_metadata( $meta_type, $object_id, wp_slash( $name ) ) ) {
 			return new WP_Error(
 				'rest_meta_database_error',
 				__( 'Could not delete meta value from database.' ),
@@ -192,7 +192,8 @@ abstract class WP_REST_Meta_Fields {
 	 * @return bool|WP_Error True if meta fields are updated, WP_Error otherwise.
 	 */
 	protected function update_multi_meta_value( $object_id, $name, $values ) {
-		if ( ! current_user_can( 'edit_post_meta', $object_id, $name ) ) {
+		$meta_type = $this->get_meta_type();
+		if ( ! current_user_can( "edit_{$meta_type}_meta", $object_id, $name ) ) {
 			return new WP_Error(
 				'rest_cannot_update',
 				sprintf( __( 'You do not have permission to edit the %s custom field.' ), $name ),
@@ -200,7 +201,7 @@ abstract class WP_REST_Meta_Fields {
 			);
 		}
 
-		$current = get_metadata( $this->get_meta_type(), $object_id, $name, false );
+		$current = get_metadata( $meta_type, $object_id, $name, false );
 
 		$to_remove = $current;
 		$to_add    = $values;
@@ -227,7 +228,7 @@ abstract class WP_REST_Meta_Fields {
 		$to_remove = array_unique( $to_remove );
 
 		foreach ( $to_remove as $value ) {
-			if ( ! delete_metadata( $this->get_meta_type(), $object_id, wp_slash( $name ), wp_slash( $value ) ) ) {
+			if ( ! delete_metadata( $meta_type, $object_id, wp_slash( $name ), wp_slash( $value ) ) ) {
 				return new WP_Error(
 					'rest_meta_database_error',
 					__( 'Could not update meta value in database.' ),
@@ -237,7 +238,7 @@ abstract class WP_REST_Meta_Fields {
 		}
 
 		foreach ( $to_add as $value ) {
-			if ( ! add_metadata( $this->get_meta_type(), $object_id, wp_slash( $name ), wp_slash( $value ) ) ) {
+			if ( ! add_metadata( $meta_type, $object_id, wp_slash( $name ), wp_slash( $value ) ) ) {
 				return new WP_Error(
 					'rest_meta_database_error',
 					__( 'Could not update meta value in database.' ),
@@ -261,7 +262,8 @@ abstract class WP_REST_Meta_Fields {
 	 * @return bool|WP_Error True if the meta field was updated, WP_Error otherwise.
 	 */
 	protected function update_meta_value( $object_id, $name, $value ) {
-		if ( ! current_user_can( 'edit_post_meta', $object_id, $name ) ) {
+		$meta_type = $this->get_meta_type();
+		if ( ! current_user_can(  "edit_{$meta_type}_meta", $object_id, $name ) ) {
 			return new WP_Error(
 				'rest_cannot_update',
 				sprintf( __( 'You do not have permission to edit the %s custom field.' ), $name ),
@@ -269,7 +271,6 @@ abstract class WP_REST_Meta_Fields {
 			);
 		}
 
-		$meta_type  = $this->get_meta_type();
 		$meta_key   = wp_slash( $name );
 		$meta_value = wp_slash( $value );
 
diff --git a/tests/phpunit/tests/comment/capabilities.php b/tests/phpunit/tests/comment/capabilities.php
new file mode 100644
index 0000000..7d82b18
--- /dev/null
+++ b/tests/phpunit/tests/comment/capabilities.php
@@ -0,0 +1,77 @@
+<?php
+
+/**
+ * @group comment
+ * @group capabilities
+ */
+
+class Tests_Comment_Capabilities extends WP_UnitTestCase {
+	protected static $admin;
+	protected static $u1;
+	protected static $u2;
+	protected static $post_id;
+	protected static $comment_id;
+
+	public static function wpSetUpBeforeClass( $factory ) {
+		self::$admin = $factory->user->create( array( 'role' => 'administrator' ) );
+		self::$u1 = $factory->user->create( array( 'role' => 'author' ) );
+		self::$u2 = $factory->user->create( array( 'role' => 'author' ) );
+
+		self::$post_id = $factory->post->create( array( 'post_author' => self::$u1 ) );
+		self::$comment_id = $factory->comment->create( array( 'comment_post_ID' => self::$post_id ) );
+	}
+
+	public static function wpTearDownAfterClass() {
+		self::delete_user( self::$u1 );
+		self::delete_user( self::$u2 );
+		self::delete_user( self::$admin );
+
+		wp_delete_post( self::$post_id, true );
+		wp_delete_comment( self::$comment_id, true );
+	}
+
+	public function test_no_one_can_edit_comment_meta_for_non_existent_comment() {
+		wp_set_current_user( self::$admin );
+		$this->assertFalse( current_user_can( 'edit_comment_meta', 999999, 'foo' ) );
+	}
+
+	public function test_user_can_edit_comment_meta() {
+		wp_set_current_user( self::$u1 );
+		$this->assertTrue( current_user_can( 'edit_comment_meta', self::$comment_id, 'foo' ) );
+	}
+
+	public function test_user_cannot_edit_comment_meta() {
+		wp_set_current_user( self::$u2 );
+		$this->assertFalse( current_user_can( 'edit_comment_meta', self::$comment_id, 'foo' ) );
+	}
+
+	public function test_no_one_can_delete_comment_meta_for_non_existent_comment() {
+		wp_set_current_user( self::$admin );
+		$this->assertFalse( current_user_can( 'delete_comment_meta', 999999, 'foo' ) );
+	}
+
+	public function test_user_can_delete_comment_meta() {
+		wp_set_current_user( self::$u1 );
+		$this->assertTrue( current_user_can( 'delete_comment_meta', self::$comment_id, 'foo' ) );
+	}
+
+	public function test_user_cannot_delete_comment_meta() {
+		wp_set_current_user( self::$u2 );
+		$this->assertFalse( current_user_can( 'delete_comment_meta', self::$comment_id, 'foo' ) );
+	}
+
+	public function test_no_one_can_add_comment_meta_for_non_existent_comment() {
+		wp_set_current_user( self::$admin );
+		$this->assertFalse( current_user_can( 'add_comment_meta', 999999, 'foo' ) );
+	}
+
+	public function test_user_can_add_comment_meta() {
+		wp_set_current_user( self::$u1 );
+		$this->assertTrue( current_user_can( 'add_comment_meta', self::$comment_id, 'foo' ) );
+	}
+
+	public function test_user_cannot_add_comment_meta() {
+		wp_set_current_user( self::$u2 );
+		$this->assertFalse( current_user_can( 'add_comment_meta', self::$comment_id, 'foo' ) );
+	}
+}
diff --git a/tests/phpunit/tests/post/capabilities.php b/tests/phpunit/tests/post/capabilities.php
new file mode 100644
index 0000000..9e6503e
--- /dev/null
+++ b/tests/phpunit/tests/post/capabilities.php
@@ -0,0 +1,74 @@
+<?php
+
+/**
+ * @group post
+ * @group capabilities
+ */
+
+class Tests_Post_Capabilities extends WP_UnitTestCase {
+	protected static $admin;
+	protected static $u1;
+	protected static $u2;
+	protected static $post_id;
+
+	public static function wpSetUpBeforeClass( $factory ) {
+		self::$admin = $factory->user->create( array( 'role' => 'administrator' ) );
+		self::$u1 = $factory->user->create( array( 'role' => 'author' ) );
+		self::$u2 = $factory->user->create( array( 'role' => 'author' ) );
+
+		self::$post_id = $factory->post->create( array( 'post_author' => self::$u1 ) );
+	}
+
+	public static function wpTearDownAfterClass() {
+		self::delete_user( self::$u1 );
+		self::delete_user( self::$u2 );
+		self::delete_user( self::$admin );
+
+		wp_delete_post( self::$post_id, true );
+	}
+
+	public function test_no_one_can_edit_post_meta_for_non_existent_post() {
+		wp_set_current_user( self::$admin );
+		$this->assertFalse( current_user_can( 'edit_post_meta', 999999, 'foo' ) );
+	}
+
+	public function test_user_can_edit_post_meta() {
+		wp_set_current_user( self::$u1 );
+		$this->assertTrue( current_user_can( 'edit_post_meta', self::$post_id, 'foo' ) );
+	}
+
+	public function test_user_cannot_edit_post_meta() {
+		wp_set_current_user( self::$u2 );
+		$this->assertFalse( current_user_can( 'edit_post_meta', self::$post_id, 'foo' ) );
+	}
+
+	public function test_no_one_can_delete_post_meta_for_non_existent_post() {
+		wp_set_current_user( self::$admin );
+		$this->assertFalse( current_user_can( 'delete_post_meta', 999999, 'foo' ) );
+	}
+
+	public function test_user_can_delete_post_meta() {
+		wp_set_current_user( self::$u1 );
+		$this->assertTrue( current_user_can( 'delete_post_meta', self::$post_id, 'foo' ) );
+	}
+
+	public function test_user_cannot_delete_post_meta() {
+		wp_set_current_user( self::$u2 );
+		$this->assertFalse( current_user_can( 'delete_post_meta', self::$post_id, 'foo' ) );
+	}
+
+	public function test_no_one_can_add_post_meta_for_non_existent_post() {
+		wp_set_current_user( self::$admin );
+		$this->assertFalse( current_user_can( 'add_post_meta', 999999, 'foo' ) );
+	}
+
+	public function test_user_can_add_post_meta() {
+		wp_set_current_user( self::$u1 );
+		$this->assertTrue( current_user_can( 'add_post_meta', self::$post_id, 'foo' ) );
+	}
+
+	public function test_user_cannot_add_post_meta() {
+		wp_set_current_user( self::$u2 );
+		$this->assertFalse( current_user_can( 'add_post_meta', self::$post_id, 'foo' ) );
+	}
+}
diff --git a/tests/phpunit/tests/rest-api/rest-comment-meta-fields.php b/tests/phpunit/tests/rest-api/rest-comment-meta-fields.php
new file mode 100644
index 0000000..c7ec51c
--- /dev/null
+++ b/tests/phpunit/tests/rest-api/rest-comment-meta-fields.php
@@ -0,0 +1,644 @@
+<?php
+/**
+ * Unit tests covering WP_REST_Posts meta functionality.
+ *
+ * @package WordPress
+ * @subpackage REST API
+ */
+
+ /**
+  * @group restapi
+  */
+class WP_Test_REST_Comment_Meta_Fields extends WP_Test_REST_TestCase {
+	protected static $editor_id;
+	protected static $post_id;
+	protected static $comment_id;
+
+	public static function wpSetUpBeforeClass( $factory ) {
+		// Ensure we have write permission.
+		self::$editor_id = $factory->user->create( array(
+			'role' => 'editor',
+		) );
+		self::$post_id = $factory->post->create();
+		self::$comment_id = $factory->comment->create_post_comments( self::$post_id, 1 )[0];
+	}
+
+	public static function wpTearDownAfterClass() {
+		wp_delete_comment( self::$comment_id );
+		wp_delete_post( self::$post_id );
+	}
+
+	public function setUp() {
+		parent::setUp();
+
+		register_meta( 'comment', 'test_single', array(
+			'show_in_rest' => true,
+			'single' => true,
+		));
+		register_meta( 'comment', 'test_multi', array(
+			'show_in_rest' => true,
+			'single' => false,
+		));
+		register_meta( 'comment', 'test_bad_auth', array(
+			'show_in_rest' => true,
+			'single' => true,
+			'auth_callback' => '__return_false',
+		));
+		register_meta( 'comment', 'test_bad_auth_multi', array(
+			'show_in_rest' => true,
+			'single' => false,
+			'auth_callback' => '__return_false',
+		));
+		register_meta( 'comment', 'test_no_rest', array() );
+		register_meta( 'comment', 'test_rest_disabled', array(
+			'show_in_rest' => false,
+		));
+		register_meta( 'comment', 'test_custom_schema', array(
+			'single' => true,
+			'type' => 'integer',
+			'show_in_rest' => array(
+				'schema' => array(
+					'type' => 'number',
+				),
+			),
+		));
+		register_meta( 'comment', 'test_invalid_type', array(
+			'single' => true,
+			'type' => false,
+			'show_in_rest' => true,
+		));
+
+		/** @var WP_REST_Server $wp_rest_server */
+		global $wp_rest_server;
+		$this->server = $wp_rest_server = new Spy_REST_Server;
+		do_action( 'rest_api_init' );
+	}
+
+	public function test_get_value() {
+		add_comment_meta( self::$comment_id, 'test_single', 'testvalue' );
+
+		$request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%d', self::$comment_id ) );
+		$response = $this->server->dispatch( $request );
+
+		$this->assertEquals( 200, $response->get_status() );
+
+		$data = $response->get_data();
+		$this->assertArrayHasKey( 'meta', $data );
+
+		$meta = (array) $data['meta'];
+		$this->assertArrayHasKey( 'test_single', $meta );
+		$this->assertEquals( 'testvalue', $meta['test_single'] );
+	}
+
+	/**
+	 * @depends test_get_value
+	 */
+	public function test_get_multi_value() {
+		add_comment_meta( self::$comment_id, 'test_multi', 'value1' );
+		$request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%d', self::$comment_id ) );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertEquals( 200, $response->get_status() );
+
+		$data = $response->get_data();
+		$meta = (array) $data['meta'];
+		$this->assertArrayHasKey( 'test_multi', $meta );
+		$this->assertInternalType( 'array', $meta['test_multi'] );
+		$this->assertContains( 'value1', $meta['test_multi'] );
+
+		// Check after an update.
+		add_comment_meta( self::$comment_id, 'test_multi', 'value2' );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertEquals( 200, $response->get_status() );
+		$data = $response->get_data();
+		$meta = (array) $data['meta'];
+		$this->assertContains( 'value1', $meta['test_multi'] );
+		$this->assertContains( 'value2', $meta['test_multi'] );
+	}
+
+	/**
+	 * @depends test_get_value
+	 */
+	public function test_get_unregistered() {
+		add_comment_meta( self::$comment_id, 'test_unregistered', 'value1' );
+		$request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%d', self::$comment_id ) );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertEquals( 200, $response->get_status() );
+
+		$data = $response->get_data();
+		$meta = (array) $data['meta'];
+		$this->assertArrayNotHasKey( 'test_unregistered', $meta );
+	}
+
+	/**
+	 * @depends test_get_value
+	 */
+	public function test_get_registered_no_api_access() {
+		add_comment_meta( self::$comment_id, 'test_no_rest', 'for_the_wicked' );
+		$request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%d', self::$comment_id ) );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertEquals( 200, $response->get_status() );
+
+		$data = $response->get_data();
+		$meta = (array) $data['meta'];
+		$this->assertArrayNotHasKey( 'test_no_rest', $meta );
+	}
+
+	/**
+	 * @depends test_get_value
+	 */
+	public function test_get_registered_api_disabled() {
+		add_comment_meta( self::$comment_id, 'test_rest_disabled', 'sleepless_nights' );
+		$request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%d', self::$comment_id ) );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertEquals( 200, $response->get_status() );
+
+		$data = $response->get_data();
+		$meta = (array) $data['meta'];
+		$this->assertArrayNotHasKey( 'test_rest_disabled', $meta );
+	}
+
+	public function test_get_value_types() {
+		register_meta( 'comment', 'test_string', array(
+			'show_in_rest' => true,
+			'single' => true,
+			'type' => 'string',
+		));
+		register_meta( 'comment', 'test_number', array(
+			'show_in_rest' => true,
+			'single' => true,
+			'type' => 'number',
+		));
+		register_meta( 'comment', 'test_bool', array(
+			'show_in_rest' => true,
+			'single' => true,
+			'type' => 'boolean',
+		));
+
+		/** @var WP_REST_Server $wp_rest_server */
+		global $wp_rest_server;
+		$this->server = $wp_rest_server = new Spy_REST_Server;
+		do_action( 'rest_api_init' );
+
+		add_comment_meta( self::$comment_id, 'test_string', 42 );
+		add_comment_meta( self::$comment_id, 'test_number', '42' );
+		add_comment_meta( self::$comment_id, 'test_bool', 1 );
+
+		$request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%d', self::$comment_id ) );
+		$response = $this->server->dispatch( $request );
+		$this->assertEquals( 200, $response->get_status() );
+
+		$data = $response->get_data();
+		$meta = (array) $data['meta'];
+
+		$this->assertArrayHasKey( 'test_string', $meta );
+		$this->assertInternalType( 'string', $meta['test_string'] );
+		$this->assertSame( '42', $meta['test_string'] );
+
+		$this->assertArrayHasKey( 'test_number', $meta );
+		$this->assertInternalType( 'float', $meta['test_number'] );
+		$this->assertSame( 42.0, $meta['test_number'] );
+
+		$this->assertArrayHasKey( 'test_bool', $meta );
+		$this->assertInternalType( 'boolean', $meta['test_bool'] );
+		$this->assertSame( true, $meta['test_bool'] );
+	}
+
+	/**
+	 * @depends test_get_value
+	 */
+	public function test_set_value() {
+		// Ensure no data exists currently.
+		$values = get_comment_meta( self::$comment_id, 'test_single', false );
+		$this->assertEmpty( $values );
+
+		wp_set_current_user( self::$editor_id );
+
+		$data = array(
+			'meta' => array(
+				'test_single' => 'test_value',
+			),
+		);
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/comments/%d', self::$comment_id ) );
+		$request->set_body_params( $data );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertEquals( 200, $response->get_status() );
+
+		$meta = get_comment_meta( self::$comment_id, 'test_single', false );
+		$this->assertNotEmpty( $meta );
+		$this->assertCount( 1, $meta );
+		$this->assertEquals( 'test_value', $meta[0] );
+
+		$data = $response->get_data();
+		$meta = (array) $data['meta'];
+		$this->assertArrayHasKey( 'test_single', $meta );
+		$this->assertEquals( 'test_value', $meta['test_single'] );
+	}
+
+	/**
+	 * @depends test_get_value
+	 */
+	public function test_set_duplicate_single_value() {
+		// Start with an existing metakey and value.
+		$values = update_comment_meta( self::$comment_id, 'test_single', 'test_value' );
+		$this->assertEquals( 'test_value', get_comment_meta( self::$comment_id, 'test_single', true ) );
+
+		wp_set_current_user( self::$editor_id );
+
+		$data = array(
+			'meta' => array(
+				'test_single' => 'test_value',
+			),
+		);
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/comments/%d', self::$comment_id ) );
+		$request->set_body_params( $data );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertEquals( 200, $response->get_status() );
+
+		$meta = get_comment_meta( self::$comment_id, 'test_single', true );
+		$this->assertNotEmpty( $meta );
+		$this->assertEquals( 'test_value', $meta );
+
+		$data = $response->get_data();
+		$meta = (array) $data['meta'];
+		$this->assertArrayHasKey( 'test_single', $meta );
+		$this->assertEquals( 'test_value', $meta['test_single'] );
+	}
+
+	/**
+	 * @depends test_set_value
+	 */
+	public function test_set_value_unauthenticated() {
+		$data = array(
+			'meta' => array(
+				'test_single' => 'test_value',
+			),
+		);
+
+		wp_set_current_user( 0 );
+
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/comments/%d', self::$comment_id ) );
+		$request->set_body_params( $data );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertErrorResponse( 'rest_cannot_edit', $response, 401 );
+
+		// Check that the value wasn't actually updated.
+		$this->assertEmpty( get_comment_meta( self::$comment_id, 'test_single', false ) );
+	}
+
+	/**
+	 * @depends test_set_value
+	 */
+	public function test_set_value_blocked() {
+		$data = array(
+			'meta' => array(
+				'test_bad_auth' => 'test_value',
+			),
+		);
+
+		wp_set_current_user( self::$editor_id );
+
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/comments/%d', self::$comment_id ) );
+		$request->set_body_params( $data );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertErrorResponse( 'rest_cannot_update', $response, 403 );
+		$this->assertEmpty( get_comment_meta( self::$comment_id, 'test_bad_auth', false ) );
+	}
+
+	/**
+	 * @depends test_set_value
+	 */
+	public function test_set_value_db_error() {
+		$data = array(
+			'meta' => array(
+				'test_single' => 'test_value',
+			),
+		);
+
+		wp_set_current_user( self::$editor_id );
+
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/comments/%d', self::$comment_id ) );
+		$request->set_body_params( $data );
+
+		/**
+		 * Disable showing error as the below is going to intentionally
+		 * trigger a DB error.
+		 */
+		global $wpdb;
+		$wpdb->suppress_errors = true;
+		add_filter( 'query', array( $this, 'error_insert_query' ) );
+
+		$response = $this->server->dispatch( $request );
+		remove_filter( 'query', array( $this, 'error_insert_query' ) );
+		$wpdb->show_errors = true;
+	}
+
+	public function test_set_value_multiple() {
+		// Ensure no data exists currently.
+		$values = get_comment_meta( self::$comment_id, 'test_multi', false );
+		$this->assertEmpty( $values );
+
+		wp_set_current_user( self::$editor_id );
+
+		$data = array(
+			'meta' => array(
+				'test_multi' => array( 'val1' ),
+			),
+		);
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/comments/%d', self::$comment_id ) );
+		$request->set_body_params( $data );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertEquals( 200, $response->get_status() );
+
+		$meta = get_comment_meta( self::$comment_id, 'test_multi', false );
+		$this->assertNotEmpty( $meta );
+		$this->assertCount( 1, $meta );
+		$this->assertEquals( 'val1', $meta[0] );
+
+		// Add another value.
+		$data = array(
+			'meta' => array(
+				'test_multi' => array( 'val1', 'val2' ),
+			),
+		);
+		$request->set_body_params( $data );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertEquals( 200, $response->get_status() );
+
+		$meta = get_comment_meta( self::$comment_id, 'test_multi', false );
+		$this->assertNotEmpty( $meta );
+		$this->assertCount( 2, $meta );
+		$this->assertContains( 'val1', $meta );
+		$this->assertContains( 'val2', $meta );
+	}
+
+	/**
+	 * Test removing only one item with duplicate items.
+	 */
+	public function test_set_value_remove_one() {
+		add_comment_meta( self::$comment_id, 'test_multi', 'c' );
+		add_comment_meta( self::$comment_id, 'test_multi', 'n' );
+		add_comment_meta( self::$comment_id, 'test_multi', 'n' );
+
+		wp_set_current_user( self::$editor_id );
+
+		$data = array(
+			'meta' => array(
+				'test_multi' => array( 'c', 'n' ),
+			),
+		);
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/comments/%d', self::$comment_id ) );
+		$request->set_body_params( $data );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertEquals( 200, $response->get_status() );
+
+		$meta = get_comment_meta( self::$comment_id, 'test_multi', false );
+		$this->assertNotEmpty( $meta );
+		$this->assertCount( 2, $meta );
+		$this->assertContains( 'c', $meta );
+		$this->assertContains( 'n', $meta );
+	}
+
+	/**
+	 * @depends test_set_value_multiple
+	 */
+	public function test_set_value_multiple_unauthenticated() {
+		// Ensure no data exists currently.
+		$values = get_comment_meta( self::$comment_id, 'test_multi', false );
+		$this->assertEmpty( $values );
+
+		wp_set_current_user( 0 );
+
+		$data = array(
+			'meta' => array(
+				'test_multi' => array( 'val1' ),
+			),
+		);
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/comments/%d', self::$comment_id ) );
+		$request->set_body_params( $data );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertErrorResponse( 'rest_cannot_edit', $response, 401 );
+
+		$meta = get_comment_meta( self::$comment_id, 'test_multi', false );
+		$this->assertEmpty( $meta );
+	}
+
+	/**
+	 * @depends test_set_value_multiple
+	 */
+	public function test_set_value_multiple_blocked() {
+		$data = array(
+			'meta' => array(
+				'test_bad_auth_multi' => array( 'test_value' ),
+			),
+		);
+
+		wp_set_current_user( self::$editor_id );
+
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/comments/%d', self::$comment_id ) );
+		$request->set_body_params( $data );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertErrorResponse( 'rest_cannot_update', $response, 403 );
+		$this->assertEmpty( get_comment_meta( self::$comment_id, 'test_bad_auth_multi', false ) );
+	}
+
+	public function test_add_multi_value_db_error() {
+		// Ensure no data exists currently.
+		$values = get_comment_meta( self::$comment_id, 'test_multi', false );
+		$this->assertEmpty( $values );
+
+		wp_set_current_user( self::$editor_id );
+
+		$data = array(
+			'meta' => array(
+				'test_multi' => array( 'val1' ),
+			),
+		);
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/comments/%d', self::$comment_id ) );
+		$request->set_body_params( $data );
+
+		/**
+		 * Disable showing error as the below is going to intentionally
+		 * trigger a DB error.
+		 */
+		global $wpdb;
+		$wpdb->suppress_errors = true;
+		add_filter( 'query', array( $this, 'error_insert_query' ) );
+
+		$response = $this->server->dispatch( $request );
+		remove_filter( 'query', array( $this, 'error_insert_query' ) );
+		$wpdb->show_errors = true;
+
+		$this->assertErrorResponse( 'rest_meta_database_error', $response, 500 );
+	}
+
+	public function test_remove_multi_value_db_error() {
+		add_comment_meta( self::$comment_id, 'test_multi', 'val1' );
+		$values = get_comment_meta( self::$comment_id, 'test_multi', false );
+		$this->assertEquals( array( 'val1' ), $values );
+
+		wp_set_current_user( self::$editor_id );
+
+		$data = array(
+			'meta' => array(
+				'test_multi' => array(),
+			),
+		);
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/comments/%d', self::$comment_id ) );
+		$request->set_body_params( $data );
+
+		/**
+		 * Disable showing error as the below is going to intentionally
+		 * trigger a DB error.
+		 */
+		global $wpdb;
+		$wpdb->suppress_errors = true;
+		add_filter( 'query', array( $this, 'error_delete_query' ) );
+
+		$response = $this->server->dispatch( $request );
+		remove_filter( 'query', array( $this, 'error_delete_query' ) );
+		$wpdb->show_errors = true;
+
+		$this->assertErrorResponse( 'rest_meta_database_error', $response, 500 );
+	}
+
+	public function test_delete_value() {
+		add_comment_meta( self::$comment_id, 'test_single', 'val1' );
+		$current = get_comment_meta( self::$comment_id, 'test_single', true );
+		$this->assertEquals( 'val1', $current );
+
+		wp_set_current_user( self::$editor_id );
+
+		$data = array(
+			'meta' => array(
+				'test_single' => null,
+			),
+		);
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/comments/%d', self::$comment_id ) );
+		$request->set_body_params( $data );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertEquals( 200, $response->get_status() );
+
+		$meta = get_comment_meta( self::$comment_id, 'test_single', false );
+		$this->assertEmpty( $meta );
+	}
+
+	/**
+	 * @depends test_delete_value
+	 */
+	public function test_delete_value_blocked() {
+		add_comment_meta( self::$comment_id, 'test_bad_auth', 'val1' );
+		$current = get_comment_meta( self::$comment_id, 'test_bad_auth', true );
+		$this->assertEquals( 'val1', $current );
+
+		wp_set_current_user( self::$editor_id );
+
+		$data = array(
+			'meta' => array(
+				'test_bad_auth' => null,
+			),
+		);
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/comments/%d', self::$comment_id ) );
+		$request->set_body_params( $data );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertErrorResponse( 'rest_cannot_delete', $response, 403 );
+
+		$meta = get_comment_meta( self::$comment_id, 'test_bad_auth', true );
+		$this->assertEquals( 'val1', $meta );
+	}
+
+	/**
+	 * @depends test_delete_value
+	 */
+	public function test_delete_value_db_error() {
+		add_comment_meta( self::$comment_id, 'test_single', 'val1' );
+		$current = get_comment_meta( self::$comment_id, 'test_single', true );
+		$this->assertEquals( 'val1', $current );
+
+		wp_set_current_user( self::$editor_id );
+
+		$data = array(
+			'meta' => array(
+				'test_single' => null,
+			),
+		);
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/comments/%d', self::$comment_id ) );
+		$request->set_body_params( $data );
+		/**
+		 * Disable showing error as the below is going to intentionally
+		 * trigger a DB error.
+		 */
+		global $wpdb;
+		$wpdb->suppress_errors = true;
+		add_filter( 'query', array( $this, 'error_delete_query' ) );
+
+		$response = $this->server->dispatch( $request );
+		remove_filter( 'query', array( $this, 'error_delete_query' ) );
+		$wpdb->show_errors = true;
+
+		$this->assertErrorResponse( 'rest_meta_database_error', $response, 500 );
+	}
+
+	public function test_get_schema() {
+		$request = new WP_REST_Request( 'OPTIONS', sprintf( '/wp/v2/comments/%d', self::$comment_id ) );
+		$response = $this->server->dispatch( $request );
+
+		$data = $response->get_data();
+		$schema = $data['schema'];
+
+		$this->assertArrayHasKey( 'meta', $schema['properties'] );
+		$meta_schema = $schema['properties']['meta']['properties'];
+
+		$this->assertArrayHasKey( 'test_single', $meta_schema );
+		$this->assertEquals( 'string', $meta_schema['test_single']['type'] );
+
+		$this->assertArrayHasKey( 'test_multi', $meta_schema );
+		$this->assertEquals( 'array', $meta_schema['test_multi']['type'] );
+		$this->assertArrayHasKey( 'items', $meta_schema['test_multi'] );
+		$this->assertEquals( 'string', $meta_schema['test_multi']['items']['type'] );
+
+		$this->assertArrayHasKey( 'test_custom_schema', $meta_schema );
+		$this->assertEquals( 'number', $meta_schema['test_custom_schema']['type'] );
+
+		$this->assertArrayNotHasKey( 'test_no_rest', $meta_schema );
+		$this->assertArrayNotHasKey( 'test_rest_disabled', $meta_schema );
+		$this->assertArrayNotHasKey( 'test_invalid_type', $meta_schema );
+	}
+
+	/**
+	 * Internal function used to disable an insert query which
+	 * will trigger a wpdb error for testing purposes.
+	 */
+	public function error_insert_query( $query ) {
+		if ( strpos( $query, 'INSERT' ) === 0 ) {
+			$query = '],';
+		}
+		return $query;
+	}
+
+	/**
+	 * Internal function used to disable an insert query which
+	 * will trigger a wpdb error for testing purposes.
+	 */
+	public function error_delete_query( $query ) {
+		if ( strpos( $query, 'DELETE' ) === 0 ) {
+			$query = '],';
+		}
+		return $query;
+	}
+}
diff --git a/tests/phpunit/tests/rest-api/rest-term-meta-fields.php b/tests/phpunit/tests/rest-api/rest-term-meta-fields.php
new file mode 100644
index 0000000..bfc3418
--- /dev/null
+++ b/tests/phpunit/tests/rest-api/rest-term-meta-fields.php
@@ -0,0 +1,642 @@
+<?php
+/**
+ * Unit tests covering WP_REST_Taxonomies terms meta functionality.
+ *
+ * @package WordPress
+ * @subpackage REST API
+ */
+
+ /**
+  * @group restapi
+  */
+class WP_Test_REST_Term_Meta_Fields extends WP_Test_REST_TestCase {
+	protected static $administrator_id;
+	protected static $term_id;
+
+	public static function wpSetUpBeforeClass( $factory ) {
+		self::$administrator_id = $factory->user->create( array(
+			'role' => 'administrator',
+		) );
+
+		register_taxonomy( 'wptests_tax', 'post', array( 'show_in_rest' => true ) );
+		self::$term_id = $factory->term->create( array( 'taxonomy' => 'wptests_tax' ) );
+	}
+
+	public function setUp() {
+		parent::setUp();
+
+		register_meta( 'term', 'test_single', array(
+			'show_in_rest' => true,
+			'single' => true,
+		) );
+		register_meta( 'term', 'test_multi', array(
+			'show_in_rest' => true,
+			'single' => false,
+		) );
+		register_meta( 'term', 'test_bad_auth', array(
+			'show_in_rest' => true,
+			'single' => true,
+			'auth_callback' => '__return_false',
+		) );
+		register_meta( 'term', 'test_bad_auth_multi', array(
+			'show_in_rest' => true,
+			'single' => false,
+			'auth_callback' => '__return_false',
+		) );
+		register_meta( 'term', 'test_no_rest', array() );
+		register_meta( 'term', 'test_rest_disabled', array(
+			'show_in_rest' => false,
+		) );
+		register_meta( 'term', 'test_custom_schema', array(
+			'single' => true,
+			'type' => 'integer',
+			'show_in_rest' => array(
+				'schema' => array(
+					'type' => 'number',
+				),
+			),
+		) );
+		register_meta( 'term', 'test_invalid_type', array(
+			'single' => true,
+			'type' => false,
+			'show_in_rest' => true,
+		) );
+
+		/** @var WP_REST_Server $wp_rest_server */
+		global $wp_rest_server;
+		$this->server = $wp_rest_server = new Spy_REST_Server;
+		do_action( 'rest_api_init' );
+
+		register_taxonomy( 'wptests_tax', 'post', array( 'show_in_rest' => true ) );
+		$controller = new WP_REST_Terms_Controller( 'wptests_tax' );
+		$controller->register_routes();
+	}
+
+	public function test_get_value() {
+		add_term_meta( self::$term_id, 'test_single', 'testvalue' );
+		$request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/wptests_tax/%d', self::$term_id ) );
+
+		$response = $this->server->dispatch( $request );
+
+		$this->assertEquals( 200, $response->get_status() );
+
+		$data = $response->get_data();
+		$this->assertArrayHasKey( 'meta', $data );
+
+		$meta = (array) $data['meta'];
+		$this->assertArrayHasKey( 'test_single', $meta );
+		$this->assertEquals( 'testvalue', $meta['test_single'] );
+	}
+
+	/**
+	 * @depends test_get_value
+	 */
+	public function test_get_multi_value() {
+		add_term_meta( self::$term_id, 'test_multi', 'value1' );
+		$request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/wptests_tax/%d', self::$term_id ) );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertEquals( 200, $response->get_status() );
+
+		$data = $response->get_data();
+		$meta = (array) $data['meta'];
+		$this->assertArrayHasKey( 'test_multi', $meta );
+		$this->assertInternalType( 'array', $meta['test_multi'] );
+		$this->assertContains( 'value1', $meta['test_multi'] );
+
+		// Check after an update.
+		add_term_meta( self::$term_id, 'test_multi', 'value2' );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertEquals( 200, $response->get_status() );
+		$data = $response->get_data();
+		$meta = (array) $data['meta'];
+		$this->assertContains( 'value1', $meta['test_multi'] );
+		$this->assertContains( 'value2', $meta['test_multi'] );
+	}
+
+	/**
+	 * @depends test_get_value
+	 */
+	public function test_get_unregistered() {
+		add_term_meta( self::$term_id, 'test_unregistered', 'value1' );
+		$request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/wptests_tax/%d', self::$term_id ) );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertEquals( 200, $response->get_status() );
+
+		$data = $response->get_data();
+		$meta = (array) $data['meta'];
+		$this->assertArrayNotHasKey( 'test_unregistered', $meta );
+	}
+
+	/**
+	 * @depends test_get_value
+	 */
+	public function test_get_registered_no_api_access() {
+		add_term_meta( self::$term_id, 'test_no_rest', 'for_the_wicked' );
+		$request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/wptests_tax/%d', self::$term_id ) );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertEquals( 200, $response->get_status() );
+
+		$data = $response->get_data();
+		$meta = (array) $data['meta'];
+		$this->assertArrayNotHasKey( 'test_no_rest', $meta );
+	}
+
+	/**
+	 * @depends test_get_value
+	 */
+	public function test_get_registered_api_disabled() {
+		add_term_meta( self::$term_id, 'test_rest_disabled', 'sleepless_nights' );
+		$request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/wptests_tax/%d', self::$term_id ) );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertEquals( 200, $response->get_status() );
+
+		$data = $response->get_data();
+		$meta = (array) $data['meta'];
+		$this->assertArrayNotHasKey( 'test_rest_disabled', $meta );
+	}
+
+	public function test_get_value_types() {
+		register_meta( 'term', 'test_string', array(
+			'show_in_rest' => true,
+			'single' => true,
+			'type' => 'string',
+		));
+		register_meta( 'term', 'test_number', array(
+			'show_in_rest' => true,
+			'single' => true,
+			'type' => 'number',
+		));
+		register_meta( 'term', 'test_bool', array(
+			'show_in_rest' => true,
+			'single' => true,
+			'type' => 'boolean',
+		));
+
+		/** @var WP_REST_Server $wp_rest_server */
+		global $wp_rest_server;
+		$this->server = $wp_rest_server = new Spy_REST_Server;
+		do_action( 'rest_api_init' );
+
+		add_term_meta( self::$term_id, 'test_string', 42 );
+		add_term_meta( self::$term_id, 'test_number', '42' );
+		add_term_meta( self::$term_id, 'test_bool', 1 );
+
+		$request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/wptests_tax/%d', self::$term_id ) );
+		$response = $this->server->dispatch( $request );
+		$this->assertEquals( 200, $response->get_status() );
+
+		$data = $response->get_data();
+		$meta = (array) $data['meta'];
+
+		$this->assertArrayHasKey( 'test_string', $meta );
+		$this->assertInternalType( 'string', $meta['test_string'] );
+		$this->assertSame( '42', $meta['test_string'] );
+
+		$this->assertArrayHasKey( 'test_number', $meta );
+		$this->assertInternalType( 'float', $meta['test_number'] );
+		$this->assertSame( 42.0, $meta['test_number'] );
+
+		$this->assertArrayHasKey( 'test_bool', $meta );
+		$this->assertInternalType( 'boolean', $meta['test_bool'] );
+		$this->assertSame( true, $meta['test_bool'] );
+	}
+
+	/**
+	 * @depends test_get_value
+	 */
+	public function test_set_value() {
+		// Ensure no data exists currently.
+		$values = get_term_meta( self::$term_id, 'test_single', false );
+		$this->assertEmpty( $values );
+
+		wp_set_current_user( self::$administrator_id );
+
+		$data = array(
+			'meta' => array(
+				'test_single' => 'test_value',
+			),
+		);
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/wptests_tax/%d', self::$term_id ) );
+		$request->set_body_params( $data );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertEquals( 200, $response->get_status() );
+
+		$meta = get_term_meta( self::$term_id, 'test_single', false );
+		$this->assertNotEmpty( $meta );
+		$this->assertCount( 1, $meta );
+		$this->assertEquals( 'test_value', $meta[0] );
+
+		$data = $response->get_data();
+		$meta = (array) $data['meta'];
+		$this->assertArrayHasKey( 'test_single', $meta );
+		$this->assertEquals( 'test_value', $meta['test_single'] );
+	}
+
+	/**
+	 * @depends test_get_value
+	 */
+	public function test_set_duplicate_single_value() {
+		// Start with an existing metakey and value.
+		$values = update_term_meta( self::$term_id, 'test_single', 'test_value' );
+		$this->assertEquals( 'test_value', get_term_meta( self::$term_id, 'test_single', true ) );
+
+		wp_set_current_user( self::$administrator_id );
+
+		$data = array(
+			'meta' => array(
+				'test_single' => 'test_value',
+			),
+		);
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/wptests_tax/%d', self::$term_id ) );
+		$request->set_body_params( $data );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertEquals( 200, $response->get_status() );
+
+		$meta = get_term_meta( self::$term_id, 'test_single', true );
+		$this->assertNotEmpty( $meta );
+		$this->assertEquals( 'test_value', $meta );
+
+		$data = $response->get_data();
+		$meta = (array) $data['meta'];
+		$this->assertArrayHasKey( 'test_single', $meta );
+		$this->assertEquals( 'test_value', $meta['test_single'] );
+	}
+
+	/**
+	 * @depends test_set_value
+	 */
+	public function test_set_value_unauthenticated() {
+		$data = array(
+			'meta' => array(
+				'test_single' => 'test_value',
+			),
+		);
+
+		wp_set_current_user( 0 );
+
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/wptests_tax/%d', self::$term_id ) );
+		$request->set_body_params( $data );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertErrorResponse( 'rest_cannot_update', $response, 401 );
+
+		// Check that the value wasn't actually updated.
+		$this->assertEmpty( get_term_meta( self::$term_id, 'test_single', false ) );
+	}
+
+	/**
+	 * @depends test_set_value
+	 */
+	public function test_set_value_blocked() {
+		$data = array(
+			'meta' => array(
+				'test_bad_auth' => 'test_value',
+			),
+		);
+
+		wp_set_current_user( self::$administrator_id );
+
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/wptests_tax/%d', self::$term_id ) );
+		$request->set_body_params( $data );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertErrorResponse( 'rest_cannot_update', $response, 403 );
+		$this->assertEmpty( get_term_meta( self::$term_id, 'test_bad_auth', false ) );
+	}
+
+	/**
+	 * @depends test_set_value
+	 */
+	public function test_set_value_db_error() {
+		$data = array(
+			'meta' => array(
+				'test_single' => 'test_value',
+			),
+		);
+
+		wp_set_current_user( self::$administrator_id );
+
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/wptests_tax/%d', self::$term_id ) );
+		$request->set_body_params( $data );
+
+		/**
+		 * Disable showing error as the below is going to intentionally
+		 * trigger a DB error.
+		 */
+		global $wpdb;
+		$wpdb->suppress_errors = true;
+		add_filter( 'query', array( $this, 'error_insert_query' ) );
+
+		$response = $this->server->dispatch( $request );
+		remove_filter( 'query', array( $this, 'error_insert_query' ) );
+		$wpdb->show_errors = true;
+	}
+
+	public function test_set_value_multiple() {
+		// Ensure no data exists currently.
+		$values = get_term_meta( self::$term_id, 'test_multi', false );
+		$this->assertEmpty( $values );
+
+		wp_set_current_user( self::$administrator_id );
+
+		$data = array(
+			'meta' => array(
+				'test_multi' => array( 'val1' ),
+			),
+		);
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/wptests_tax/%d', self::$term_id ) );
+		$request->set_body_params( $data );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertEquals( 200, $response->get_status() );
+
+		$meta = get_term_meta( self::$term_id, 'test_multi', false );
+		$this->assertNotEmpty( $meta );
+		$this->assertCount( 1, $meta );
+		$this->assertEquals( 'val1', $meta[0] );
+
+		// Add another value.
+		$data = array(
+			'meta' => array(
+				'test_multi' => array( 'val1', 'val2' ),
+			),
+		);
+		$request->set_body_params( $data );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertEquals( 200, $response->get_status() );
+
+		$meta = get_term_meta( self::$term_id, 'test_multi', false );
+		$this->assertNotEmpty( $meta );
+		$this->assertCount( 2, $meta );
+		$this->assertContains( 'val1', $meta );
+		$this->assertContains( 'val2', $meta );
+	}
+
+	/**
+	 * Test removing only one item with duplicate items.
+	 */
+	public function test_set_value_remove_one() {
+		add_term_meta( self::$term_id, 'test_multi', 'c' );
+		add_term_meta( self::$term_id, 'test_multi', 'n' );
+		add_term_meta( self::$term_id, 'test_multi', 'n' );
+
+		wp_set_current_user( self::$administrator_id );
+
+		$data = array(
+			'meta' => array(
+				'test_multi' => array( 'c', 'n' ),
+			),
+		);
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/wptests_tax/%d', self::$term_id ) );
+		$request->set_body_params( $data );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertEquals( 200, $response->get_status() );
+
+		$meta = get_term_meta( self::$term_id, 'test_multi', false );
+		$this->assertNotEmpty( $meta );
+		$this->assertCount( 2, $meta );
+		$this->assertContains( 'c', $meta );
+		$this->assertContains( 'n', $meta );
+	}
+
+	/**
+	 * @depends test_set_value_multiple
+	 */
+	public function test_set_value_multiple_unauthenticated() {
+		// Ensure no data exists currently.
+		$values = get_term_meta( self::$term_id, 'test_multi', false );
+		$this->assertEmpty( $values );
+
+		wp_set_current_user( 0 );
+
+		$data = array(
+			'meta' => array(
+				'test_multi' => array( 'val1' ),
+			),
+		);
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/wptests_tax/%d', self::$term_id ) );
+		$request->set_body_params( $data );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertErrorResponse( 'rest_cannot_update', $response, 401 );
+
+		$meta = get_term_meta( self::$term_id, 'test_multi', false );
+		$this->assertEmpty( $meta );
+	}
+
+	/**
+	 * @depends test_set_value_multiple
+	 */
+	public function test_set_value_multiple_blocked() {
+		$data = array(
+			'meta' => array(
+				'test_bad_auth_multi' => array( 'test_value' ),
+			),
+		);
+
+		wp_set_current_user( self::$administrator_id );
+
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/wptests_tax/%d', self::$term_id ) );
+		$request->set_body_params( $data );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertErrorResponse( 'rest_cannot_update', $response, 403 );
+		$this->assertEmpty( get_term_meta( self::$term_id, 'test_bad_auth_multi', false ) );
+	}
+
+	public function test_add_multi_value_db_error() {
+		// Ensure no data exists currently.
+		$values = get_term_meta( self::$term_id, 'test_multi', false );
+		$this->assertEmpty( $values );
+
+		wp_set_current_user( self::$administrator_id );
+
+		$data = array(
+			'meta' => array(
+				'test_multi' => array( 'val1' ),
+			),
+		);
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/wptests_tax/%d', self::$term_id ) );
+		$request->set_body_params( $data );
+
+		/**
+		 * Disable showing error as the below is going to intentionally
+		 * trigger a DB error.
+		 */
+		global $wpdb;
+		$wpdb->suppress_errors = true;
+		add_filter( 'query', array( $this, 'error_insert_query' ) );
+
+		$response = $this->server->dispatch( $request );
+		remove_filter( 'query', array( $this, 'error_insert_query' ) );
+		$wpdb->show_errors = true;
+
+		$this->assertErrorResponse( 'rest_meta_database_error', $response, 500 );
+	}
+
+	public function test_remove_multi_value_db_error() {
+		add_term_meta( self::$term_id, 'test_multi', 'val1' );
+		$values = get_term_meta( self::$term_id, 'test_multi', false );
+		$this->assertEquals( array( 'val1' ), $values );
+
+		wp_set_current_user( self::$administrator_id );
+
+		$data = array(
+			'meta' => array(
+				'test_multi' => array(),
+			),
+		);
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/wptests_tax/%d', self::$term_id ) );
+		$request->set_body_params( $data );
+
+		/**
+		 * Disable showing error as the below is going to intentionally
+		 * trigger a DB error.
+		 */
+		global $wpdb;
+		$wpdb->suppress_errors = true;
+		add_filter( 'query', array( $this, 'error_delete_query' ) );
+
+		$response = $this->server->dispatch( $request );
+		remove_filter( 'query', array( $this, 'error_delete_query' ) );
+		$wpdb->show_errors = true;
+
+		$this->assertErrorResponse( 'rest_meta_database_error', $response, 500 );
+	}
+
+	public function test_delete_value() {
+		add_term_meta( self::$term_id, 'test_single', 'val1' );
+		$current = get_term_meta( self::$term_id, 'test_single', true );
+		$this->assertEquals( 'val1', $current );
+
+		wp_set_current_user( self::$administrator_id );
+
+		$data = array(
+			'meta' => array(
+				'test_single' => null,
+			),
+		);
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/wptests_tax/%d', self::$term_id ) );
+		$request->set_body_params( $data );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertEquals( 200, $response->get_status() );
+
+		$meta = get_term_meta( self::$term_id, 'test_single', false );
+		$this->assertEmpty( $meta );
+	}
+
+	/**
+	 * @depends test_delete_value
+	 */
+	public function test_delete_value_blocked() {
+		add_term_meta( self::$term_id, 'test_bad_auth', 'val1' );
+		$current = get_term_meta( self::$term_id, 'test_bad_auth', true );
+		$this->assertEquals( 'val1', $current );
+
+		wp_set_current_user( self::$administrator_id );
+
+		$data = array(
+			'meta' => array(
+				'test_bad_auth' => null,
+			),
+		);
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/wptests_tax/%d', self::$term_id ) );
+		$request->set_body_params( $data );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertErrorResponse( 'rest_cannot_delete', $response, 403 );
+
+		$meta = get_term_meta( self::$term_id, 'test_bad_auth', true );
+		$this->assertEquals( 'val1', $meta );
+	}
+
+	/**
+	 * @depends test_delete_value
+	 */
+	public function test_delete_value_db_error() {
+		add_term_meta( self::$term_id, 'test_single', 'val1' );
+		$current = get_term_meta( self::$term_id, 'test_single', true );
+		$this->assertEquals( 'val1', $current );
+
+		wp_set_current_user( self::$administrator_id );
+
+		$data = array(
+			'meta' => array(
+				'test_single' => null,
+			),
+		);
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/wptests_tax/%d', self::$term_id ) );
+		$request->set_body_params( $data );
+		/**
+		 * Disable showing error as the below is going to intentionally
+		 * trigger a DB error.
+		 */
+		global $wpdb;
+		$wpdb->suppress_errors = true;
+		add_filter( 'query', array( $this, 'error_delete_query' ) );
+
+		$response = $this->server->dispatch( $request );
+		remove_filter( 'query', array( $this, 'error_delete_query' ) );
+		$wpdb->show_errors = true;
+
+		$this->assertErrorResponse( 'rest_meta_database_error', $response, 500 );
+	}
+
+	public function test_get_schema() {
+		$request = new WP_REST_Request( 'OPTIONS', sprintf( '/wp/v2/wptests_tax/%d', self::$term_id ) );
+		$response = $this->server->dispatch( $request );
+
+		$data = $response->get_data();
+		$schema = $data['schema'];
+
+		$this->assertArrayHasKey( 'meta', $schema['properties'] );
+		$meta_schema = $schema['properties']['meta']['properties'];
+
+		$this->assertArrayHasKey( 'test_single', $meta_schema );
+		$this->assertEquals( 'string', $meta_schema['test_single']['type'] );
+
+		$this->assertArrayHasKey( 'test_multi', $meta_schema );
+		$this->assertEquals( 'array', $meta_schema['test_multi']['type'] );
+		$this->assertArrayHasKey( 'items', $meta_schema['test_multi'] );
+		$this->assertEquals( 'string', $meta_schema['test_multi']['items']['type'] );
+
+		$this->assertArrayHasKey( 'test_custom_schema', $meta_schema );
+		$this->assertEquals( 'number', $meta_schema['test_custom_schema']['type'] );
+
+		$this->assertArrayNotHasKey( 'test_no_rest', $meta_schema );
+		$this->assertArrayNotHasKey( 'test_rest_disabled', $meta_schema );
+		$this->assertArrayNotHasKey( 'test_invalid_type', $meta_schema );
+	}
+
+	/**
+	 * Internal function used to disable an insert query which
+	 * will trigger a wpdb error for testing purposes.
+	 */
+	public function error_insert_query( $query ) {
+		if ( strpos( $query, 'INSERT' ) === 0 ) {
+			$query = '],';
+		}
+		return $query;
+	}
+
+	/**
+	 * Internal function used to disable an insert query which
+	 * will trigger a wpdb error for testing purposes.
+	 */
+	public function error_delete_query( $query ) {
+		if ( strpos( $query, 'DELETE' ) === 0 ) {
+			$query = '],';
+		}
+		return $query;
+	}
+}
diff --git a/tests/phpunit/tests/rest-api/rest-user-meta-fields.php b/tests/phpunit/tests/rest-api/rest-user-meta-fields.php
new file mode 100644
index 0000000..a52898b
--- /dev/null
+++ b/tests/phpunit/tests/rest-api/rest-user-meta-fields.php
@@ -0,0 +1,676 @@
+<?php
+/**
+ * Unit tests covering WP_REST_Users meta functionality.
+ *
+ * @package WordPress
+ * @subpackage REST API
+ */
+
+ /**
+  * @group restapi
+  */
+class WP_Test_REST_User_Meta_Fields extends WP_Test_REST_TestCase {
+	protected static $administrator_id;
+	protected static $user;
+
+	public static function wpSetUpBeforeClass( $factory ) {
+		self::$administrator_id = $factory->user->create( array(
+			'role' => 'administrator',
+		) );
+		self::$user = $factory->user->create( array(
+			'role' => 'author',
+		) );
+	}
+
+	public static function wpTearDownAfterClass() {
+		wp_delete_user( self::$administrator_id );
+		wp_delete_user( self::$user );
+	}
+
+	public function setUp() {
+		parent::setUp();
+
+		// Editing users requires 'manage_network_users' on multisite.
+		if ( is_multisite() ) {
+			add_filter( 'user_has_cap', array( $this, 'grant_manage_network_users_to_admin' ), 10, 4 );
+		}
+
+		register_meta( 'user', 'test_single', array(
+			'show_in_rest' => true,
+			'single' => true,
+		) );
+		register_meta( 'user', 'test_multi', array(
+			'show_in_rest' => true,
+			'single' => false,
+		) );
+		register_meta( 'user', 'test_bad_auth', array(
+			'show_in_rest' => true,
+			'single' => true,
+			'auth_callback' => '__return_false',
+		) );
+		register_meta( 'user', 'test_bad_auth_multi', array(
+			'show_in_rest' => true,
+			'single' => false,
+			'auth_callback' => '__return_false',
+		) );
+		register_meta( 'user', 'test_no_rest', array() );
+		register_meta( 'user', 'test_rest_disabled', array(
+			'show_in_rest' => false,
+		) );
+		register_meta( 'user', 'test_custom_schema', array(
+			'single' => true,
+			'type' => 'integer',
+			'show_in_rest' => array(
+				'schema' => array(
+					'type' => 'number',
+				),
+			),
+		) );
+		register_meta( 'user', 'test_invalid_type', array(
+			'single' => true,
+			'type' => false,
+			'show_in_rest' => true,
+		) );
+
+		/** @var WP_REST_Server $wp_rest_server */
+		global $wp_rest_server;
+		$this->server = $wp_rest_server = new Spy_REST_Server;
+		do_action( 'rest_api_init' );
+	}
+
+	public function tearDown() {
+		if ( is_multisite() ) {
+			remove_filter( 'user_has_cap', array( $this, 'grant_manage_network_users_to_admin' ), 10, 4 );
+		}
+	}
+
+	public function grant_manage_network_users_to_admin( $allcaps, $caps, $args, WP_User $user ) {
+		if ( array( 'manage_network_users' ) == $caps && self::$administrator_id === $user->ID ) {
+			$allcaps['manage_network_users'] = 1;
+		}
+
+		return $allcaps;
+
+	}
+
+	public function test_get_value() {
+		add_user_meta( self::$user, 'test_single', 'testvalue' );
+		$request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/users/%d', self::$user ) );
+
+		wp_set_current_user( self::$administrator_id );
+
+		$response = $this->server->dispatch( $request );
+
+		$this->assertEquals( 200, $response->get_status() );
+
+		$data = $response->get_data();
+		$this->assertArrayHasKey( 'meta', $data );
+
+		$meta = (array) $data['meta'];
+		$this->assertArrayHasKey( 'test_single', $meta );
+		$this->assertEquals( 'testvalue', $meta['test_single'] );
+	}
+
+	/**
+	 * @depends test_get_value
+	 */
+	public function test_get_multi_value() {
+		add_user_meta( self::$user, 'test_multi', 'value1' );
+		$request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/users/%d', self::$user ) );
+
+		wp_set_current_user( self::$administrator_id );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertEquals( 200, $response->get_status() );
+
+		$data = $response->get_data();
+		$meta = (array) $data['meta'];
+		$this->assertArrayHasKey( 'test_multi', $meta );
+		$this->assertInternalType( 'array', $meta['test_multi'] );
+		$this->assertContains( 'value1', $meta['test_multi'] );
+
+		// Check after an update.
+		add_user_meta( self::$user, 'test_multi', 'value2' );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertEquals( 200, $response->get_status() );
+		$data = $response->get_data();
+		$meta = (array) $data['meta'];
+		$this->assertContains( 'value1', $meta['test_multi'] );
+		$this->assertContains( 'value2', $meta['test_multi'] );
+	}
+
+	/**
+	 * @depends test_get_value
+	 */
+	public function test_get_unregistered() {
+		add_user_meta( self::$user, 'test_unregistered', 'value1' );
+		$request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/users/%d', self::$user ) );
+
+		wp_set_current_user( self::$administrator_id );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertEquals( 200, $response->get_status() );
+
+		$data = $response->get_data();
+		$meta = (array) $data['meta'];
+		$this->assertArrayNotHasKey( 'test_unregistered', $meta );
+	}
+
+	/**
+	 * @depends test_get_value
+	 */
+	public function test_get_registered_no_api_access() {
+		add_user_meta( self::$user, 'test_no_rest', 'for_the_wicked' );
+		$request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/users/%d', self::$user ) );
+
+		wp_set_current_user( self::$administrator_id );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertEquals( 200, $response->get_status() );
+
+		$data = $response->get_data();
+		$meta = (array) $data['meta'];
+		$this->assertArrayNotHasKey( 'test_no_rest', $meta );
+	}
+
+	/**
+	 * @depends test_get_value
+	 */
+	public function test_get_registered_api_disabled() {
+		add_user_meta( self::$user, 'test_rest_disabled', 'sleepless_nights' );
+		$request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/users/%d', self::$user ) );
+
+		wp_set_current_user( self::$administrator_id );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertEquals( 200, $response->get_status() );
+
+		$data = $response->get_data();
+		$meta = (array) $data['meta'];
+		$this->assertArrayNotHasKey( 'test_rest_disabled', $meta );
+	}
+
+	public function test_get_value_types() {
+		register_meta( 'user', 'test_string', array(
+			'show_in_rest' => true,
+			'single' => true,
+			'type' => 'string',
+		));
+		register_meta( 'user', 'test_number', array(
+			'show_in_rest' => true,
+			'single' => true,
+			'type' => 'number',
+		));
+		register_meta( 'user', 'test_bool', array(
+			'show_in_rest' => true,
+			'single' => true,
+			'type' => 'boolean',
+		));
+
+		/** @var WP_REST_Server $wp_rest_server */
+		global $wp_rest_server;
+		$this->server = $wp_rest_server = new Spy_REST_Server;
+		do_action( 'rest_api_init' );
+
+		add_user_meta( self::$user, 'test_string', 42 );
+		add_user_meta( self::$user, 'test_number', '42' );
+		add_user_meta( self::$user, 'test_bool', 1 );
+
+		$request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/users/%d', self::$user ) );
+
+		wp_set_current_user( self::$administrator_id );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertEquals( 200, $response->get_status() );
+
+		$data = $response->get_data();
+		$meta = (array) $data['meta'];
+
+		$this->assertArrayHasKey( 'test_string', $meta );
+		$this->assertInternalType( 'string', $meta['test_string'] );
+		$this->assertSame( '42', $meta['test_string'] );
+
+		$this->assertArrayHasKey( 'test_number', $meta );
+		$this->assertInternalType( 'float', $meta['test_number'] );
+		$this->assertSame( 42.0, $meta['test_number'] );
+
+		$this->assertArrayHasKey( 'test_bool', $meta );
+		$this->assertInternalType( 'boolean', $meta['test_bool'] );
+		$this->assertSame( true, $meta['test_bool'] );
+	}
+
+	/**
+	 * @depends test_get_value
+	 */
+	public function test_set_value() {
+		// Ensure no data exists currently.
+		$values = get_user_meta( self::$user, 'test_single', false );
+		$this->assertEmpty( $values );
+
+		wp_set_current_user( self::$administrator_id );
+
+		$data = array(
+			'meta' => array(
+				'test_single' => 'test_value',
+			),
+		);
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/users/%d', self::$user ) );
+		$request->set_body_params( $data );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertEquals( 200, $response->get_status() );
+
+		$meta = get_user_meta( self::$user, 'test_single', false );
+		$this->assertNotEmpty( $meta );
+		$this->assertCount( 1, $meta );
+		$this->assertEquals( 'test_value', $meta[0] );
+
+		$data = $response->get_data();
+		$meta = (array) $data['meta'];
+		$this->assertArrayHasKey( 'test_single', $meta );
+		$this->assertEquals( 'test_value', $meta['test_single'] );
+	}
+
+	/**
+	 * @depends test_get_value
+	 */
+	public function test_set_duplicate_single_value() {
+		// Start with an existing metakey and value.
+		$values = update_user_meta( self::$user, 'test_single', 'test_value' );
+		$this->assertEquals( 'test_value', get_user_meta( self::$user, 'test_single', true ) );
+
+		wp_set_current_user( self::$administrator_id );
+
+		$data = array(
+			'meta' => array(
+				'test_single' => 'test_value',
+			),
+		);
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/users/%d', self::$user ) );
+		$request->set_body_params( $data );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertEquals( 200, $response->get_status() );
+
+		$meta = get_user_meta( self::$user, 'test_single', true );
+		$this->assertNotEmpty( $meta );
+		$this->assertEquals( 'test_value', $meta );
+
+		$data = $response->get_data();
+		$meta = (array) $data['meta'];
+		$this->assertArrayHasKey( 'test_single', $meta );
+		$this->assertEquals( 'test_value', $meta['test_single'] );
+	}
+
+	/**
+	 * @depends test_set_value
+	 */
+	public function test_set_value_unauthenticated() {
+		$data = array(
+			'meta' => array(
+				'test_single' => 'test_value',
+			),
+		);
+
+		wp_set_current_user( 0 );
+
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/users/%d', self::$user ) );
+		$request->set_body_params( $data );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertErrorResponse( 'rest_cannot_edit', $response, 401 );
+
+		// Check that the value wasn't actually updated.
+		$this->assertEmpty( get_user_meta( self::$user, 'test_single', false ) );
+	}
+
+	/**
+	 * @depends test_set_value
+	 */
+	public function test_set_value_blocked() {
+		$data = array(
+			'meta' => array(
+				'test_bad_auth' => 'test_value',
+			),
+		);
+
+		wp_set_current_user( self::$administrator_id );
+
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/users/%d', self::$user ) );
+		$request->set_body_params( $data );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertErrorResponse( 'rest_cannot_update', $response, 403 );
+		$this->assertEmpty( get_user_meta( self::$user, 'test_bad_auth', false ) );
+	}
+
+	/**
+	 * @depends test_set_value
+	 */
+	public function test_set_value_db_error() {
+		$data = array(
+			'meta' => array(
+				'test_single' => 'test_value',
+			),
+		);
+
+		wp_set_current_user( self::$administrator_id );
+
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/users/%d', self::$user ) );
+		$request->set_body_params( $data );
+
+		/**
+		 * Disable showing error as the below is going to intentionally
+		 * trigger a DB error.
+		 */
+		global $wpdb;
+		$wpdb->suppress_errors = true;
+		add_filter( 'query', array( $this, 'error_insert_query' ) );
+
+		$response = $this->server->dispatch( $request );
+		remove_filter( 'query', array( $this, 'error_insert_query' ) );
+		$wpdb->show_errors = true;
+	}
+
+	public function test_set_value_multiple() {
+		// Ensure no data exists currently.
+		$values = get_user_meta( self::$user, 'test_multi', false );
+		$this->assertEmpty( $values );
+
+		wp_set_current_user( self::$administrator_id );
+
+		$data = array(
+			'meta' => array(
+				'test_multi' => array( 'val1' ),
+			),
+		);
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/users/%d', self::$user ) );
+		$request->set_body_params( $data );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertEquals( 200, $response->get_status() );
+
+		$meta = get_user_meta( self::$user, 'test_multi', false );
+		$this->assertNotEmpty( $meta );
+		$this->assertCount( 1, $meta );
+		$this->assertEquals( 'val1', $meta[0] );
+
+		// Add another value.
+		$data = array(
+			'meta' => array(
+				'test_multi' => array( 'val1', 'val2' ),
+			),
+		);
+		$request->set_body_params( $data );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertEquals( 200, $response->get_status() );
+
+		$meta = get_user_meta( self::$user, 'test_multi', false );
+		$this->assertNotEmpty( $meta );
+		$this->assertCount( 2, $meta );
+		$this->assertContains( 'val1', $meta );
+		$this->assertContains( 'val2', $meta );
+	}
+
+	/**
+	 * Test removing only one item with duplicate items.
+	 */
+	public function test_set_value_remove_one() {
+		add_user_meta( self::$user, 'test_multi', 'c' );
+		add_user_meta( self::$user, 'test_multi', 'n' );
+		add_user_meta( self::$user, 'test_multi', 'n' );
+
+		wp_set_current_user( self::$administrator_id );
+
+		$data = array(
+			'meta' => array(
+				'test_multi' => array( 'c', 'n' ),
+			),
+		);
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/users/%d', self::$user ) );
+		$request->set_body_params( $data );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertEquals( 200, $response->get_status() );
+
+		$meta = get_user_meta( self::$user, 'test_multi', false );
+		$this->assertNotEmpty( $meta );
+		$this->assertCount( 2, $meta );
+		$this->assertContains( 'c', $meta );
+		$this->assertContains( 'n', $meta );
+	}
+
+	/**
+	 * @depends test_set_value_multiple
+	 */
+	public function test_set_value_multiple_unauthenticated() {
+		// Ensure no data exists currently.
+		$values = get_user_meta( self::$user, 'test_multi', false );
+		$this->assertEmpty( $values );
+
+		wp_set_current_user( 0 );
+
+		$data = array(
+			'meta' => array(
+				'test_multi' => array( 'val1' ),
+			),
+		);
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/users/%d', self::$user ) );
+		$request->set_body_params( $data );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertErrorResponse( 'rest_cannot_edit', $response, 401 );
+
+		$meta = get_user_meta( self::$user, 'test_multi', false );
+		$this->assertEmpty( $meta );
+	}
+
+	/**
+	 * @depends test_set_value_multiple
+	 */
+	public function test_set_value_multiple_blocked() {
+		$data = array(
+			'meta' => array(
+				'test_bad_auth_multi' => array( 'test_value' ),
+			),
+		);
+
+		wp_set_current_user( self::$administrator_id );
+
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/users/%d', self::$user ) );
+		$request->set_body_params( $data );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertErrorResponse( 'rest_cannot_update', $response, 403 );
+		$this->assertEmpty( get_user_meta( self::$user, 'test_bad_auth_multi', false ) );
+	}
+
+	public function test_add_multi_value_db_error() {
+		// Ensure no data exists currently.
+		$values = get_user_meta( self::$user, 'test_multi', false );
+		$this->assertEmpty( $values );
+
+		wp_set_current_user( self::$administrator_id );
+
+		$data = array(
+			'meta' => array(
+				'test_multi' => array( 'val1' ),
+			),
+		);
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/users/%d', self::$user ) );
+		$request->set_body_params( $data );
+
+		/**
+		 * Disable showing error as the below is going to intentionally
+		 * trigger a DB error.
+		 */
+		global $wpdb;
+		$wpdb->suppress_errors = true;
+		add_filter( 'query', array( $this, 'error_insert_query' ) );
+
+		$response = $this->server->dispatch( $request );
+		remove_filter( 'query', array( $this, 'error_insert_query' ) );
+		$wpdb->show_errors = true;
+
+		$this->assertErrorResponse( 'rest_meta_database_error', $response, 500 );
+	}
+
+	public function test_remove_multi_value_db_error() {
+		add_user_meta( self::$user, 'test_multi', 'val1' );
+		$values = get_user_meta( self::$user, 'test_multi', false );
+		$this->assertEquals( array( 'val1' ), $values );
+
+		wp_set_current_user( self::$administrator_id );
+
+		$data = array(
+			'meta' => array(
+				'test_multi' => array(),
+			),
+		);
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/users/%d', self::$user ) );
+		$request->set_body_params( $data );
+
+		/**
+		 * Disable showing error as the below is going to intentionally
+		 * trigger a DB error.
+		 */
+		global $wpdb;
+		$wpdb->suppress_errors = true;
+		add_filter( 'query', array( $this, 'error_delete_query' ) );
+
+		$response = $this->server->dispatch( $request );
+		remove_filter( 'query', array( $this, 'error_delete_query' ) );
+		$wpdb->show_errors = true;
+
+		$this->assertErrorResponse( 'rest_meta_database_error', $response, 500 );
+	}
+
+	public function test_delete_value() {
+		add_user_meta( self::$user, 'test_single', 'val1' );
+		$current = get_user_meta( self::$user, 'test_single', true );
+		$this->assertEquals( 'val1', $current );
+
+		wp_set_current_user( self::$administrator_id );
+
+		$data = array(
+			'meta' => array(
+				'test_single' => null,
+			),
+		);
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/users/%d', self::$user ) );
+		$request->set_body_params( $data );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertEquals( 200, $response->get_status() );
+
+		$meta = get_user_meta( self::$user, 'test_single', false );
+		$this->assertEmpty( $meta );
+	}
+
+	/**
+	 * @depends test_delete_value
+	 */
+	public function test_delete_value_blocked() {
+		add_user_meta( self::$user, 'test_bad_auth', 'val1' );
+		$current = get_user_meta( self::$user, 'test_bad_auth', true );
+		$this->assertEquals( 'val1', $current );
+
+		wp_set_current_user( self::$administrator_id );
+
+		$data = array(
+			'meta' => array(
+				'test_bad_auth' => null,
+			),
+		);
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/users/%d', self::$user ) );
+		$request->set_body_params( $data );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertErrorResponse( 'rest_cannot_delete', $response, 403 );
+
+		$meta = get_user_meta( self::$user, 'test_bad_auth', true );
+		$this->assertEquals( 'val1', $meta );
+	}
+
+	/**
+	 * @depends test_delete_value
+	 */
+	public function test_delete_value_db_error() {
+		add_user_meta( self::$user, 'test_single', 'val1' );
+		$current = get_user_meta( self::$user, 'test_single', true );
+		$this->assertEquals( 'val1', $current );
+
+		wp_set_current_user( self::$administrator_id );
+
+		$data = array(
+			'meta' => array(
+				'test_single' => null,
+			),
+		);
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/users/%d', self::$user ) );
+		$request->set_body_params( $data );
+		/**
+		 * Disable showing error as the below is going to intentionally
+		 * trigger a DB error.
+		 */
+		global $wpdb;
+		$wpdb->suppress_errors = true;
+		add_filter( 'query', array( $this, 'error_delete_query' ) );
+
+		$response = $this->server->dispatch( $request );
+		remove_filter( 'query', array( $this, 'error_delete_query' ) );
+		$wpdb->show_errors = true;
+
+		$this->assertErrorResponse( 'rest_meta_database_error', $response, 500 );
+	}
+
+	public function test_get_schema() {
+		$request = new WP_REST_Request( 'OPTIONS', sprintf( '/wp/v2/users/%d', self::$user ) );
+		$response = $this->server->dispatch( $request );
+
+		$data = $response->get_data();
+		$schema = $data['schema'];
+
+		$this->assertArrayHasKey( 'meta', $schema['properties'] );
+		$meta_schema = $schema['properties']['meta']['properties'];
+
+		$this->assertArrayHasKey( 'test_single', $meta_schema );
+		$this->assertEquals( 'string', $meta_schema['test_single']['type'] );
+
+		$this->assertArrayHasKey( 'test_multi', $meta_schema );
+		$this->assertEquals( 'array', $meta_schema['test_multi']['type'] );
+		$this->assertArrayHasKey( 'items', $meta_schema['test_multi'] );
+		$this->assertEquals( 'string', $meta_schema['test_multi']['items']['type'] );
+
+		$this->assertArrayHasKey( 'test_custom_schema', $meta_schema );
+		$this->assertEquals( 'number', $meta_schema['test_custom_schema']['type'] );
+
+		$this->assertArrayNotHasKey( 'test_no_rest', $meta_schema );
+		$this->assertArrayNotHasKey( 'test_rest_disabled', $meta_schema );
+		$this->assertArrayNotHasKey( 'test_invalid_type', $meta_schema );
+	}
+
+	/**
+	 * Internal function used to disable an insert query which
+	 * will trigger a wpdb error for testing purposes.
+	 */
+	public function error_insert_query( $query ) {
+		if ( strpos( $query, 'INSERT' ) === 0 ) {
+			$query = '],';
+		}
+		return $query;
+	}
+
+	/**
+	 * Internal function used to disable an insert query which
+	 * will trigger a wpdb error for testing purposes.
+	 */
+	public function error_delete_query( $query ) {
+		if ( strpos( $query, 'DELETE' ) === 0 ) {
+			$query = '],';
+		}
+		return $query;
+	}
+}
diff --git a/tests/phpunit/tests/term/capabilities.php b/tests/phpunit/tests/term/capabilities.php
new file mode 100644
index 0000000..b6b4c2f
--- /dev/null
+++ b/tests/phpunit/tests/term/capabilities.php
@@ -0,0 +1,92 @@
+<?php
+
+/**
+ * @group taxonomy
+ * @group capabilities
+ */
+
+class Tests_Term_Capabilities extends WP_UnitTestCase {
+	protected static $admin;
+	protected static $u1;
+	protected static $u2;
+	protected static $tag;
+
+	public static function wpSetUpBeforeClass( $factory ) {
+		self::$admin = $factory->user->create( array( 'role' => 'administrator' ) );
+		self::$u1 = $factory->user->create( array( 'role' => 'subscriber' ) );
+		self::$u2 = $factory->user->create( array( 'role' => 'subscriber' ) );
+
+		self::$tag = $factory->tag->create();
+	}
+
+	public static function wpTearDownAfterClass() {
+		self::delete_user( self::$admin );
+		self::delete_user( self::$u1 );
+		self::delete_user( self::$u2 );
+
+		wp_delete_term( self::$tag, 'post_tag' );
+	}
+
+	public function setUp() {
+		parent::setUp();
+		add_filter( 'user_has_cap', array( $this, 'grant_edit_term_to_u1' ), 10, 4 );
+	}
+
+	public function tearDown() {
+		parent::tearDown();
+		remove_filter( 'user_has_cap', array( $this, 'grant_edit_term_to_u1' ), 10, 4 );
+	}
+
+	public function grant_edit_term_to_u1( $allcaps, $caps, $args, WP_User $user ) {
+		if ( array( 'manage_categories' ) == $caps && self::$u1 === $user->ID ) {
+			$allcaps['manage_categories'] = 1;
+		}
+
+		return $allcaps;
+	}
+
+	public function test_no_one_can_edit_term_meta_for_non_existent_term() {
+		wp_set_current_user( self::$admin );
+		$this->assertFalse( current_user_can( 'edit_term_meta', 999999 ) );
+	}
+
+	public function test_user_can_edit_term_meta() {
+		wp_set_current_user( self::$u1 );
+		$this->assertTrue( current_user_can( 'edit_term_meta', self::$tag, 'foo' ) );
+	}
+
+	public function test_user_cannot_edit_term_meta() {
+		wp_set_current_user( self::$u2 );
+		$this->assertFalse( current_user_can( 'edit_term_meta', self::$tag, 'foo' ) );
+	}
+
+	public function test_no_one_can_delete_term_meta_for_non_existent_term() {
+		wp_set_current_user( self::$admin );
+		$this->assertFalse( current_user_can( 'delete_term_meta', 999999, 'foo' ) );
+	}
+
+	public function test_user_can_delete_term_meta() {
+		wp_set_current_user( self::$u1 );
+		$this->assertTrue( current_user_can( 'delete_term_meta', self::$tag, 'foo' ) );
+	}
+
+	public function test_user_cannot_delete_term_meta() {
+		wp_set_current_user( self::$u2 );
+		$this->assertFalse( current_user_can( 'delete_term_meta', self::$tag, 'foo' ) );
+	}
+
+	public function test_no_one_can_add_term_meta_for_non_existent_term() {
+		wp_set_current_user( self::$admin );
+		$this->assertFalse( current_user_can( 'add_term_meta', 999999, 'foo' ) );
+	}
+
+	public function test_user_can_add_term_meta() {
+		wp_set_current_user( self::$u1 );
+		$this->assertTrue( current_user_can( 'add_term_meta', self::$tag, 'foo' ) );
+	}
+
+	public function test_user_cannot_add_term_meta() {
+		wp_set_current_user( self::$u2 );
+		$this->assertFalse( current_user_can( 'add_term_meta', self::$tag, 'foo' ) );
+	}
+}
diff --git a/tests/phpunit/tests/user/capabilities.php b/tests/phpunit/tests/user/capabilities.php
index 8ed2c65..b440e67 100644
--- a/tests/phpunit/tests/user/capabilities.php
+++ b/tests/phpunit/tests/user/capabilities.php
@@ -431,10 +431,19 @@ class Tests_User_Capabilities extends WP_UnitTestCase {
 			$expected['delete_post_meta'],
 			$expected['add_post_meta'],
 			$expected['edit_comment'],
+			$expected['edit_comment_meta'],
+			$expected['delete_comment_meta'],
+			$expected['add_comment_meta'],
 			$expected['edit_term'],
 			$expected['delete_term'],
 			$expected['assign_term'],
-			$expected['delete_user']
+			$expected['edit_term_meta'],
+			$expected['delete_term_meta'],
+			$expected['add_term_meta'],
+			$expected['delete_user'],
+			$expected['edit_user_meta'],
+			$expected['delete_user_meta'],
+			$expected['add_user_meta']
 		);
 
 		$expected = array_keys( $expected );
@@ -1663,4 +1672,85 @@ class Tests_User_Capabilities extends WP_UnitTestCase {
 		$wp_roles = new WP_Roles();
 		$wp_roles->reinit();
 	}
+
+	/**
+	 * @ticket 38412
+	 */
+	public function test_no_one_can_edit_user_meta_for_non_existent_term() {
+		wp_set_current_user( self::$super_admin->ID );
+		$this->assertFalse( current_user_can( 'edit_user_meta', 999999 ) );
+	}
+
+	/**
+	 * @ticket 38412
+	 */
+	public function test_user_can_edit_user_meta() {
+		wp_set_current_user( self::$users['administrator']->ID );
+		if ( is_multisite() ) {
+			grant_super_admin( self::$users['administrator']->ID );
+		}
+		$this->assertTrue( current_user_can( 'edit_user_meta', self::$users['subscriber']->ID, 'foo' ) );
+	}
+
+	/**
+	 * @ticket 38412
+	 */
+	public function test_user_cannot_edit_user_meta() {
+		wp_set_current_user( self::$users['editor']->ID );
+		$this->assertFalse( current_user_can( 'edit_user_meta', self::$users['subscriber']->ID, 'foo' ) );
+	}
+
+	/**
+	 * @ticket 38412
+	 */
+	public function test_no_one_can_delete_user_meta_for_non_existent_term() {
+		wp_set_current_user( self::$super_admin->ID );
+		$this->assertFalse( current_user_can( 'delete_user_meta', 999999, 'foo' ) );
+	}
+
+	/**
+	 * @ticket 38412
+	 */
+	public function test_user_can_delete_user_meta() {
+		wp_set_current_user( self::$users['administrator']->ID );
+		if ( is_multisite() ) {
+			grant_super_admin( self::$users['administrator']->ID );
+		}
+		$this->assertTrue( current_user_can( 'delete_user_meta', self::$users['subscriber']->ID, 'foo' ) );
+	}
+
+	/**
+	 * @ticket 38412
+	 */
+	public function test_user_cannot_delete_user_meta() {
+		wp_set_current_user( self::$users['editor']->ID );
+		$this->assertFalse( current_user_can( 'delete_user_meta', self::$users['subscriber']->ID, 'foo' ) );
+	}
+
+	/**
+	 * @ticket 38412
+	 */
+	public function test_no_one_can_add_user_meta_for_non_existent_term() {
+		wp_set_current_user( self::$super_admin->ID );
+		$this->assertFalse( current_user_can( 'add_user_meta', 999999, 'foo' ) );
+	}
+
+	/**
+	 * @ticket 38412
+	 */
+	public function test_user_can_add_user_meta() {
+		wp_set_current_user( self::$users['administrator']->ID );
+		if ( is_multisite() ) {
+			grant_super_admin( self::$users['administrator']->ID );
+		}
+		$this->assertTrue( current_user_can( 'add_user_meta', self::$users['subscriber']->ID, 'foo' ) );
+	}
+
+	/**
+	 * @ticket 38412
+	 */
+	public function test_user_cannot_add_user_meta() {
+		wp_set_current_user( self::$users['editor']->ID );
+		$this->assertFalse( current_user_can( 'add_user_meta', self::$users['subscriber']->ID, 'foo' ) );
+	}
 }
