From 7341efcf15311c9b6602d2f3a0934d7cd1fd22ce Mon Sep 17 00:00:00 2001 From: Stephenie Harris Date: Thu, 3 Feb 2022 14:29:12 -0700 Subject: [PATCH 1/7] Add method to fetch a single comment with ID. --- WordPressKit/CommentServiceRemote.h | 8 ++++++++ WordPressKit/CommentServiceRemoteREST.m | 22 ++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/WordPressKit/CommentServiceRemote.h b/WordPressKit/CommentServiceRemote.h index 3eb17bf7..a07a3dfb 100644 --- a/WordPressKit/CommentServiceRemote.h +++ b/WordPressKit/CommentServiceRemote.h @@ -31,6 +31,14 @@ typedef enum { success:(void (^)(NSArray *posts))success failure:(void (^)(NSError *error))failure; + +/** + Loads the specified comment associated with a blog + */ +- (void)getCommentWithID:(NSNumber *)commentID + success:(void (^)(RemoteComment *comment))success + failure:(void (^)(NSError *))failure; + /** Publishes a new comment */ diff --git a/WordPressKit/CommentServiceRemoteREST.m b/WordPressKit/CommentServiceRemoteREST.m index 157aba86..47bcdb54 100644 --- a/WordPressKit/CommentServiceRemoteREST.m +++ b/WordPressKit/CommentServiceRemoteREST.m @@ -76,6 +76,28 @@ - (NSString *)parameterForCommentStatus:(NSNumber *)status } } +- (void)getCommentWithID:(NSNumber *)commentID + success:(void (^)(RemoteComment *comment))success + failure:(void (^)(NSError *))failure +{ + NSString *path = [NSString stringWithFormat:@"sites/%@/comments/%@", self.siteID, commentID]; + NSString *requestUrl = [self pathForEndpoint:path + withVersion:ServiceRemoteWordPressComRESTApiVersion_1_1]; + + [self.wordPressComRestApi GET:requestUrl + parameters:nil + success:^(id responseObject, NSHTTPURLResponse *httpResponse) { + RemoteComment *comment = [self remoteCommentFromJSONDictionary:responseObject]; + if (success) { + success(comment); + } + } failure:^(NSError *error, NSHTTPURLResponse *httpResponse) { + if (failure) { + failure(error); + } + }]; +} + - (void)createComment:(RemoteComment *)comment success:(void (^)(RemoteComment *comment))success failure:(void (^)(NSError *))failure From c0754b3e41f8c3956ac0bcc1754ab7a0deef39ad Mon Sep 17 00:00:00 2001 From: Stephenie Harris Date: Thu, 3 Feb 2022 14:29:29 -0700 Subject: [PATCH 2/7] Add tests for fetching a single comment. --- WordPressKit.xcodeproj/project.pbxproj | 8 ++++ .../CommentServiceRemoteRESTTests.swift | 47 +++++++++++++++++++ .../CommentServiceRemoteXMLRPCTests.swift | 38 +++++++++++++++ .../Mock Data/site-comment-success.json | 38 +++++++++++++++ .../Mock Data/xmlrpc-site-comment-success.xml | 25 ++++++++++ 5 files changed, 156 insertions(+) create mode 100644 WordPressKitTests/Mock Data/site-comment-success.json create mode 100644 WordPressKitTests/Mock Data/xmlrpc-site-comment-success.xml diff --git a/WordPressKit.xcodeproj/project.pbxproj b/WordPressKit.xcodeproj/project.pbxproj index c70272ba..328fe1c5 100644 --- a/WordPressKit.xcodeproj/project.pbxproj +++ b/WordPressKit.xcodeproj/project.pbxproj @@ -447,6 +447,8 @@ 984E34F422EF9465005C3F92 /* stats-file-downloads.json in Resources */ = {isa = PBXBuildFile; fileRef = 984E34F322EF9464005C3F92 /* stats-file-downloads.json */; }; 9856BE962630B5C200C12FEB /* RemoteUser+Likes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9856BE952630B5C200C12FEB /* RemoteUser+Likes.swift */; }; 98DC787522BAEBF200267279 /* StatsAllAnnualInsight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98DC787422BAEBF100267279 /* StatsAllAnnualInsight.swift */; }; + 98E1A60B27AB604600C61A7F /* site-comment-success.json in Resources */ = {isa = PBXBuildFile; fileRef = 98E1A60A27AB604600C61A7F /* site-comment-success.json */; }; + 98E1A60D27AB621200C61A7F /* xmlrpc-site-comment-success.xml in Resources */ = {isa = PBXBuildFile; fileRef = 98E1A60C27AB621200C61A7F /* xmlrpc-site-comment-success.xml */; }; 98EA910526BC96B8004098A1 /* xmlrpc-site-comments-success.xml in Resources */ = {isa = PBXBuildFile; fileRef = 98EA910426BC96B8004098A1 /* xmlrpc-site-comments-success.xml */; }; 98F884D426BC6445009ADF57 /* CommentServiceRemoteRESTTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F884D326BC6445009ADF57 /* CommentServiceRemoteRESTTests.swift */; }; 98F884D626BC6909009ADF57 /* site-comments-success.json in Resources */ = {isa = PBXBuildFile; fileRef = 98F884D526BC6909009ADF57 /* site-comments-success.json */; }; @@ -1087,6 +1089,8 @@ 984E34F322EF9464005C3F92 /* stats-file-downloads.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-file-downloads.json"; sourceTree = ""; }; 9856BE952630B5C200C12FEB /* RemoteUser+Likes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "RemoteUser+Likes.swift"; sourceTree = ""; }; 98DC787422BAEBF100267279 /* StatsAllAnnualInsight.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatsAllAnnualInsight.swift; sourceTree = ""; }; + 98E1A60A27AB604600C61A7F /* site-comment-success.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "site-comment-success.json"; sourceTree = ""; }; + 98E1A60C27AB621200C61A7F /* xmlrpc-site-comment-success.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = "xmlrpc-site-comment-success.xml"; sourceTree = ""; }; 98EA910426BC96B8004098A1 /* xmlrpc-site-comments-success.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = "xmlrpc-site-comments-success.xml"; sourceTree = ""; }; 98F884D326BC6445009ADF57 /* CommentServiceRemoteRESTTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentServiceRemoteRESTTests.swift; sourceTree = ""; }; 98F884D526BC6909009ADF57 /* site-comments-success.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "site-comments-success.json"; sourceTree = ""; }; @@ -2103,6 +2107,7 @@ 74C473C61EF334D4009918F2 /* site-active-purchases-none-active-success.json */, 74C473C41EF33242009918F2 /* site-active-purchases-two-active-success.json */, 98F884D526BC6909009ADF57 /* site-comments-success.json */, + 98E1A60A27AB604600C61A7F /* site-comment-success.json */, 731BA83921DECE93000FDFCD /* site-creation-success.json */, 74C473B21EF3204B009918F2 /* site-delete-auth-failure.json */, 74C473B41EF320CC009918F2 /* site-delete-bad-json-failure.json */, @@ -2207,6 +2212,7 @@ 93F50A451F227F3600B5BEBA /* xmlrpc-response-getprofile.xml */, 93F50A461F227F3600B5BEBA /* xmlrpc-response-valid-but-unexpected-dictionary.xml */, 98EA910426BC96B8004098A1 /* xmlrpc-site-comments-success.xml */, + 98E1A60C27AB621200C61A7F /* xmlrpc-site-comment-success.xml */, 740B23DE1F17FB4200067A2A /* xmlrpc-wp-getpost-bad-xml-failure.xml */, 740B23DF1F17FB4200067A2A /* xmlrpc-wp-getpost-invalid-id-failure.xml */, 740B23E01F17FB4200067A2A /* xmlrpc-wp-getpost-success.xml */, @@ -2684,8 +2690,10 @@ 404057DC221C9FD80060250C /* stats-referrer-data.json in Resources */, FA87FE0924EB3FEF003FBEE3 /* reader-post-comments-subscription-status-success.json in Resources */, E6B0461325E5B6F500DF6F4F /* sites-invites-links-disable.json in Resources */, + 98E1A60D27AB621200C61A7F /* xmlrpc-site-comment-success.xml in Resources */, 74C473C71EF334D4009918F2 /* site-active-purchases-none-active-success.json in Resources */, FA87FE0D24EB4450003FBEE3 /* reader-post-comments-unsubscribe-success.json in Resources */, + 98E1A60B27AB604600C61A7F /* site-comment-success.json in Resources */, 829BA4321FACF187003ADEEA /* activity-rewind-status-restore-finished.json in Resources */, F3FF8A29279C991B00E5C90F /* site-email-followers-get-success-more-pages.json in Resources */, 74D67F181F15C2D70010C5ED /* site-users-update-role-unknown-site-failure.json in Resources */, diff --git a/WordPressKitTests/CommentServiceRemoteRESTTests.swift b/WordPressKitTests/CommentServiceRemoteRESTTests.swift index 6de8cb67..a06bf161 100644 --- a/WordPressKitTests/CommentServiceRemoteRESTTests.swift +++ b/WordPressKitTests/CommentServiceRemoteRESTTests.swift @@ -5,13 +5,19 @@ import XCTest final class CommentServiceRemoteRESTTests: RemoteTestCase, RESTTestable { private let fetchCommentsSuccessFilename = "site-comments-success.json" + private let fetchCommentSuccessFilename = "site-comment-success.json" private let siteId = 0 + private let commentId = 1 private var remote: CommentServiceRemoteREST! private var siteCommentsEndpoint: String { return "sites/\(siteId)/comments" } + private var siteCommentEndpoint: String { + return "sites/\(siteId)/comments/\(commentId)" + } + override func setUp() { super.setUp() remote = CommentServiceRemoteREST(wordPressComRestApi: getRestApi(), siteID: NSNumber(value: siteId)) @@ -64,4 +70,45 @@ final class CommentServiceRemoteRESTTests: RemoteTestCase, RESTTestable { waitForExpectations(timeout: timeout, handler: nil) } + + func testGetSingleCommentSucceeds() { + let expect = expectation(description: "Fetching a single site comment should succeed") + + stubRemoteResponse(siteCommentEndpoint, + filename: fetchCommentSuccessFilename, + contentType: .ApplicationJSON) + + + remote.getCommentWithID(NSNumber(value: commentId), + success: { comment in + + guard let comment = comment else { + XCTFail("Failed to retrieve mock site comment") + return + } + + XCTAssertEqual(comment.authorID, NSNumber(value: 12345)) + XCTAssertEqual(comment.author, "Comment Author") + XCTAssertEqual(comment.authorEmail, "author@email.com") + XCTAssertEqual(comment.authorUrl, "author URL") + XCTAssertEqual(comment.authorIP, "000.0.00.000") + XCTAssertEqual(comment.date, NSDate(wordPressComJSONString: "2021-08-04T07:58:49+00:00") as Date) + XCTAssertEqual(comment.link, "comment URL") + XCTAssertEqual(comment.parentID, nil) + XCTAssertEqual(comment.postID, NSNumber(value: 1)) + XCTAssertEqual(comment.postTitle, "Post title") + XCTAssertEqual(comment.status, "approve") + XCTAssertEqual(comment.type, "comment") + XCTAssertEqual(comment.isLiked, false) + XCTAssertEqual(comment.likeCount, NSNumber(value: 0)) + XCTAssertEqual(comment.canModerate, true) + XCTAssertEqual(comment.content, "I am comment content") + XCTAssertEqual(comment.rawContent, "I am comment raw content") + expect.fulfill() + }, failure: { _ in + XCTFail("This callback shouldn't get called") + }) + + waitForExpectations(timeout: timeout, handler: nil) + } } diff --git a/WordPressKitTests/CommentServiceRemoteXMLRPCTests.swift b/WordPressKitTests/CommentServiceRemoteXMLRPCTests.swift index b0d40c68..27bc0dac 100644 --- a/WordPressKitTests/CommentServiceRemoteXMLRPCTests.swift +++ b/WordPressKitTests/CommentServiceRemoteXMLRPCTests.swift @@ -6,6 +6,7 @@ import wpxmlrpc class CommentServiceRemoteXMLRPCTests: RemoteTestCase, XMLRPCTestable { private var remote: Any? private let getSiteCommentsSuccessMockFilename = "xmlrpc-site-comments-success.xml" + private let getSiteCommentSuccessMockFilename = "xmlrpc-site-comment-success.xml" override func setUp() { super.setUp() @@ -59,4 +60,41 @@ class CommentServiceRemoteXMLRPCTests: RemoteTestCase, XMLRPCTestable { } } + func testGetSingleCommentSucceeds() { + let expect = expectation(description: "Fetching a single site comment should succeed") + + stubRemoteResponse(XMLRPCTestableConstants.xmlRpcUrl, + filename: getSiteCommentSuccessMockFilename, + contentType: .XML) + + if let remoteInstance = remote as? CommentServiceRemote { + + remoteInstance.getCommentWithID(NSNumber(value: 1), + success: { comment in + + guard let comment = comment else { + XCTFail("Failed to retrieve mock site comment") + return + } + + XCTAssertEqual(comment.author, "Comment Author") + XCTAssertEqual(comment.authorEmail, "author@email.com") + XCTAssertEqual(comment.authorUrl, "author URL") + XCTAssertEqual(comment.authorIP, "000.0.00.000") + XCTAssertEqual(comment.link, "comment URL") + XCTAssertEqual(comment.parentID, NSNumber(value: 1)) + XCTAssertEqual(comment.postID, NSNumber(value: 2)) + XCTAssertEqual(comment.postTitle, "Post title") + XCTAssertEqual(comment.status, "approve") + XCTAssertEqual(comment.type, "comment") + XCTAssertEqual(comment.content, "I am comment content") + XCTAssertEqual(comment.rawContent, nil) + expect.fulfill() + }, failure: { _ in + XCTFail("This callback shouldn't get called") + }) + + waitForExpectations(timeout: timeout, handler: nil) + } + } } diff --git a/WordPressKitTests/Mock Data/site-comment-success.json b/WordPressKitTests/Mock Data/site-comment-success.json new file mode 100644 index 00000000..91edad8a --- /dev/null +++ b/WordPressKitTests/Mock Data/site-comment-success.json @@ -0,0 +1,38 @@ +{ + "ID": 1, + "post": { + "ID": 1, + "title": "Post title", + "type": "post", + "link": "post URL" + }, + "author": { + "ID": 12345, + "login": "", + "email": "author@email.com", + "name": "Comment Author", + "first_name": "", + "last_name": "", + "nice_name": "", + "URL": "author URL", + "avatar_URL": "avatar URL", + "profile_URL": "profile URL", + "ip_address": "000.0.00.000" + }, + "date": "2021-08-04T07:58:49+00:00", + "URL": "comment URL", + "short_URL": "short URL", + "content": "I am comment content", + "raw_content": "I am comment raw content", + "status": "approved", + "parent": false, + "type": "comment", + "like_count": 0, + "i_like": false, + "meta": { + "links": { + } + }, + "can_moderate": true, + "i_replied": false +} diff --git a/WordPressKitTests/Mock Data/xmlrpc-site-comment-success.xml b/WordPressKitTests/Mock Data/xmlrpc-site-comment-success.xml new file mode 100644 index 00000000..366c0f39 --- /dev/null +++ b/WordPressKitTests/Mock Data/xmlrpc-site-comment-success.xml @@ -0,0 +1,25 @@ + + + + + + + date_created_gmt20210804T21:01:08 + user_id1 + comment_id1 + parent1 + statusapprove + contentI am comment content + linkcomment URL + post_id2 + post_titlePost title + authorComment Author + author_urlauthor URL + author_emailauthor@email.com + author_ip000.0.00.000 + typecomment + + + + + From 2bfea2bf2614b925477a343fa6ec14162b57c8bf Mon Sep 17 00:00:00 2001 From: Stephenie Harris Date: Thu, 3 Feb 2022 14:29:36 -0700 Subject: [PATCH 3/7] Bump pod version. --- WordPressKit.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPressKit.podspec b/WordPressKit.podspec index c043ce4c..f687e0e5 100644 --- a/WordPressKit.podspec +++ b/WordPressKit.podspec @@ -2,7 +2,7 @@ Pod::Spec.new do |s| s.name = 'WordPressKit' - s.version = '4.47.0-beta.4' + s.version = '4.47.0-beta.5' s.summary = 'WordPressKit offers a clean and simple WordPress.com and WordPress.org API.' s.description = <<-DESC From 26b975baef76ca79e262d6f287f4895aed05536f Mon Sep 17 00:00:00 2001 From: Stephenie Harris Date: Thu, 3 Feb 2022 14:36:36 -0700 Subject: [PATCH 4/7] Fix whitespace. --- .../CommentServiceRemoteRESTTests.swift | 15 +++++++-------- .../CommentServiceRemoteXMLRPCTests.swift | 10 +++++----- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/WordPressKitTests/CommentServiceRemoteRESTTests.swift b/WordPressKitTests/CommentServiceRemoteRESTTests.swift index a06bf161..cc1df856 100644 --- a/WordPressKitTests/CommentServiceRemoteRESTTests.swift +++ b/WordPressKitTests/CommentServiceRemoteRESTTests.swift @@ -17,7 +17,7 @@ final class CommentServiceRemoteRESTTests: RemoteTestCase, RESTTestable { private var siteCommentEndpoint: String { return "sites/\(siteId)/comments/\(commentId)" } - + override func setUp() { super.setUp() remote = CommentServiceRemoteREST(wordPressComRestApi: getRestApi(), siteID: NSNumber(value: siteId)) @@ -70,23 +70,22 @@ final class CommentServiceRemoteRESTTests: RemoteTestCase, RESTTestable { waitForExpectations(timeout: timeout, handler: nil) } - + func testGetSingleCommentSucceeds() { let expect = expectation(description: "Fetching a single site comment should succeed") - + stubRemoteResponse(siteCommentEndpoint, filename: fetchCommentSuccessFilename, contentType: .ApplicationJSON) - - + remote.getCommentWithID(NSNumber(value: commentId), success: { comment in - + guard let comment = comment else { XCTFail("Failed to retrieve mock site comment") return } - + XCTAssertEqual(comment.authorID, NSNumber(value: 12345)) XCTAssertEqual(comment.author, "Comment Author") XCTAssertEqual(comment.authorEmail, "author@email.com") @@ -108,7 +107,7 @@ final class CommentServiceRemoteRESTTests: RemoteTestCase, RESTTestable { }, failure: { _ in XCTFail("This callback shouldn't get called") }) - + waitForExpectations(timeout: timeout, handler: nil) } } diff --git a/WordPressKitTests/CommentServiceRemoteXMLRPCTests.swift b/WordPressKitTests/CommentServiceRemoteXMLRPCTests.swift index 27bc0dac..924ba1d5 100644 --- a/WordPressKitTests/CommentServiceRemoteXMLRPCTests.swift +++ b/WordPressKitTests/CommentServiceRemoteXMLRPCTests.swift @@ -62,21 +62,21 @@ class CommentServiceRemoteXMLRPCTests: RemoteTestCase, XMLRPCTestable { func testGetSingleCommentSucceeds() { let expect = expectation(description: "Fetching a single site comment should succeed") - + stubRemoteResponse(XMLRPCTestableConstants.xmlRpcUrl, filename: getSiteCommentSuccessMockFilename, contentType: .XML) if let remoteInstance = remote as? CommentServiceRemote { - + remoteInstance.getCommentWithID(NSNumber(value: 1), success: { comment in - + guard let comment = comment else { XCTFail("Failed to retrieve mock site comment") return } - + XCTAssertEqual(comment.author, "Comment Author") XCTAssertEqual(comment.authorEmail, "author@email.com") XCTAssertEqual(comment.authorUrl, "author URL") @@ -93,7 +93,7 @@ class CommentServiceRemoteXMLRPCTests: RemoteTestCase, XMLRPCTestable { }, failure: { _ in XCTFail("This callback shouldn't get called") }) - + waitForExpectations(timeout: timeout, handler: nil) } } From 11c294456710beb843eefef66142f74f829944dd Mon Sep 17 00:00:00 2001 From: Stephenie Harris Date: Fri, 4 Feb 2022 11:46:02 -0700 Subject: [PATCH 5/7] Give the error param a name. --- WordPressKit/CommentServiceRemote.h | 2 +- WordPressKit/CommentServiceRemoteREST.m | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/WordPressKit/CommentServiceRemote.h b/WordPressKit/CommentServiceRemote.h index a07a3dfb..1621c57a 100644 --- a/WordPressKit/CommentServiceRemote.h +++ b/WordPressKit/CommentServiceRemote.h @@ -37,7 +37,7 @@ typedef enum { */ - (void)getCommentWithID:(NSNumber *)commentID success:(void (^)(RemoteComment *comment))success - failure:(void (^)(NSError *))failure; + failure:(void (^)(NSError * error))failure; /** Publishes a new comment diff --git a/WordPressKit/CommentServiceRemoteREST.m b/WordPressKit/CommentServiceRemoteREST.m index 47bcdb54..6a34e1ba 100644 --- a/WordPressKit/CommentServiceRemoteREST.m +++ b/WordPressKit/CommentServiceRemoteREST.m @@ -78,7 +78,7 @@ - (NSString *)parameterForCommentStatus:(NSNumber *)status - (void)getCommentWithID:(NSNumber *)commentID success:(void (^)(RemoteComment *comment))success - failure:(void (^)(NSError *))failure + failure:(void (^)(NSError * error))failure { NSString *path = [NSString stringWithFormat:@"sites/%@/comments/%@", self.siteID, commentID]; NSString *requestUrl = [self pathForEndpoint:path From 3835103dc6a628bfa509ac91e773e7b876265a7e Mon Sep 17 00:00:00 2001 From: Stephenie Harris Date: Fri, 4 Feb 2022 11:48:24 -0700 Subject: [PATCH 6/7] Fix whitespace. --- WordPressKitTests/CommentServiceRemoteXMLRPCTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPressKitTests/CommentServiceRemoteXMLRPCTests.swift b/WordPressKitTests/CommentServiceRemoteXMLRPCTests.swift index 924ba1d5..749e49bf 100644 --- a/WordPressKitTests/CommentServiceRemoteXMLRPCTests.swift +++ b/WordPressKitTests/CommentServiceRemoteXMLRPCTests.swift @@ -66,7 +66,7 @@ class CommentServiceRemoteXMLRPCTests: RemoteTestCase, XMLRPCTestable { stubRemoteResponse(XMLRPCTestableConstants.xmlRpcUrl, filename: getSiteCommentSuccessMockFilename, contentType: .XML) - + if let remoteInstance = remote as? CommentServiceRemote { remoteInstance.getCommentWithID(NSNumber(value: 1), From a580849944623bcf407b9998ee2cabcc3c0a60e7 Mon Sep 17 00:00:00 2001 From: Stephenie Harris Date: Fri, 4 Feb 2022 11:49:36 -0700 Subject: [PATCH 7/7] Bump pod version. --- WordPressKit.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPressKit.podspec b/WordPressKit.podspec index f687e0e5..e33589ac 100644 --- a/WordPressKit.podspec +++ b/WordPressKit.podspec @@ -2,7 +2,7 @@ Pod::Spec.new do |s| s.name = 'WordPressKit' - s.version = '4.47.0-beta.5' + s.version = '4.47.0-beta.6' s.summary = 'WordPressKit offers a clean and simple WordPress.com and WordPress.org API.' s.description = <<-DESC