From dc1c415ac7bcacfd21878fa3c11c9b6c76e9a663 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 22 Dec 2023 13:05:07 +1300 Subject: [PATCH 1/2] Add unit tests for XMLRPC endpoint discovery --- WordPressKit.xcodeproj/project.pbxproj | 12 + .../Mock Data/xmlrpc-response-invalid.html | 7 + .../xmlrpc-response-list-methods.xml | 91 ++++++ ...mlrpc-response-mobile-plugin-redirect.html | 8 + .../WordPressOrgXMLRPCValidatorTests.swift | 300 ++++++++++++++++++ 5 files changed, 418 insertions(+) create mode 100644 WordPressKitTests/Mock Data/xmlrpc-response-invalid.html create mode 100644 WordPressKitTests/Mock Data/xmlrpc-response-list-methods.xml create mode 100644 WordPressKitTests/Mock Data/xmlrpc-response-mobile-plugin-redirect.html diff --git a/WordPressKit.xcodeproj/project.pbxproj b/WordPressKit.xcodeproj/project.pbxproj index 26028053..baf7a909 100644 --- a/WordPressKit.xcodeproj/project.pbxproj +++ b/WordPressKit.xcodeproj/project.pbxproj @@ -139,6 +139,9 @@ 46ABD0E0262EED3D00C7FF24 /* WordPressOrgXMLRPCValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46ABD0DF262EED3D00C7FF24 /* WordPressOrgXMLRPCValidatorTests.swift */; }; 46ABD0E6262EEDAB00C7FF24 /* FakeInfoDictionaryObjectProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46ABD0E5262EEDAB00C7FF24 /* FakeInfoDictionaryObjectProvider.swift */; }; 46ABD0EA262EEE0400C7FF24 /* AppTransportSecuritySettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46ABD0E9262EEE0400C7FF24 /* AppTransportSecuritySettingsTests.swift */; }; + 4A05E7A62B34142200C25E3B /* xmlrpc-response-invalid.html in Resources */ = {isa = PBXBuildFile; fileRef = 4A05E7A52B34142200C25E3B /* xmlrpc-response-invalid.html */; }; + 4A05E7A82B34EAF400C25E3B /* xmlrpc-response-mobile-plugin-redirect.html in Resources */ = {isa = PBXBuildFile; fileRef = 4A05E7A72B34EAF400C25E3B /* xmlrpc-response-mobile-plugin-redirect.html */; }; + 4A05E7AA2B34FC4300C25E3B /* xmlrpc-response-list-methods.xml in Resources */ = {isa = PBXBuildFile; fileRef = 4A05E7A92B34FC4300C25E3B /* xmlrpc-response-list-methods.xml */; }; 4A11239A2B19269A004690CF /* WordPressAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A1123992B19269A004690CF /* WordPressAPIError.swift */; }; 4A11239C2B1926B7004690CF /* HTTPRequestBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A11239B2B1926B7004690CF /* HTTPRequestBuilder.swift */; }; 4A11239E2B1926D1004690CF /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A11239D2B1926D1004690CF /* HTTPClient.swift */; }; @@ -836,6 +839,9 @@ 46ABD0DF262EED3D00C7FF24 /* WordPressOrgXMLRPCValidatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WordPressOrgXMLRPCValidatorTests.swift; sourceTree = ""; }; 46ABD0E5262EEDAB00C7FF24 /* FakeInfoDictionaryObjectProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FakeInfoDictionaryObjectProvider.swift; sourceTree = ""; }; 46ABD0E9262EEE0400C7FF24 /* AppTransportSecuritySettingsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppTransportSecuritySettingsTests.swift; sourceTree = ""; }; + 4A05E7A52B34142200C25E3B /* xmlrpc-response-invalid.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "xmlrpc-response-invalid.html"; sourceTree = ""; }; + 4A05E7A72B34EAF400C25E3B /* xmlrpc-response-mobile-plugin-redirect.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "xmlrpc-response-mobile-plugin-redirect.html"; sourceTree = ""; }; + 4A05E7A92B34FC4300C25E3B /* xmlrpc-response-list-methods.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = "xmlrpc-response-list-methods.xml"; sourceTree = ""; }; 4A1123992B19269A004690CF /* WordPressAPIError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WordPressAPIError.swift; sourceTree = ""; }; 4A11239B2B1926B7004690CF /* HTTPRequestBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPRequestBuilder.swift; sourceTree = ""; }; 4A11239D2B1926D1004690CF /* HTTPClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPClient.swift; sourceTree = ""; }; @@ -2399,6 +2405,9 @@ 740B23DC1F17FB4200067A2A /* xmlrpc-metaweblog-newpost-invalid-posttype-failure.xml */, 740B23DD1F17FB4200067A2A /* xmlrpc-metaweblog-newpost-success.xml */, 74B335E91F06F76B0053A184 /* xmlrpc-response-getpost.xml */, + 4A05E7A92B34FC4300C25E3B /* xmlrpc-response-list-methods.xml */, + 4A05E7A52B34142200C25E3B /* xmlrpc-response-invalid.html */, + 4A05E7A72B34EAF400C25E3B /* xmlrpc-response-mobile-plugin-redirect.html */, 93F50A451F227F3600B5BEBA /* xmlrpc-response-getprofile.xml */, 93F50A461F227F3600B5BEBA /* xmlrpc-response-valid-but-unexpected-dictionary.xml */, 98EA910426BC96B8004098A1 /* xmlrpc-site-comments-success.xml */, @@ -2992,6 +3001,7 @@ 93BD27581EE73442002BB00B /* auth-send-login-email-success.json in Resources */, 74B335E01F06F6290053A184 /* WordPressComRestApiFailInvalidInput.json in Resources */, F3FF8A27279C967200E5C90F /* site-email-followers-get-failure.json in Resources */, + 4A05E7A82B34EAF400C25E3B /* xmlrpc-response-mobile-plugin-redirect.html in Resources */, FE20A6A6282BC68D0025E975 /* blogging-prompts-settings-fetch-success.json in Resources */, FFE247C320C9D749002DF3A2 /* reader-site-search-blog-id-fallback.json in Resources */, C738CAF3286226D6001BE107 /* qrlogin-validate-400.json in Resources */, @@ -3057,6 +3067,7 @@ 826016FC1F9FAF6300533B6C /* activity-log-success-2.json in Resources */, C738CAF928622BB1001BE107 /* qrlogin-authenticate-failed-400.json in Resources */, 74D67F1E1F15C3240010C5ED /* people-send-invitation-failure.json in Resources */, + 4A05E7A62B34142200C25E3B /* xmlrpc-response-invalid.html in Resources */, 7403A3001EF06FEB00DED7DC /* me-settings-success.json in Resources */, F4B0F47C2ACB4B74003ABC61 /* get-all-domains-response.json in Resources */, FEE48EF82A4B3E43008A48E0 /* sites-site-active-features.json in Resources */, @@ -3124,6 +3135,7 @@ BA3F139424A0B783006367A3 /* plugin-modify-malformed-response.json in Resources */, 74C473CB1EF33696009918F2 /* site-active-purchases-auth-failure.json in Resources */, FFE247B520C891E6002DF3A2 /* WordPressComAuthenticateWithIDTokenExistingUserNeedsConnection.json in Resources */, + 4A05E7AA2B34FC4300C25E3B /* xmlrpc-response-list-methods.xml in Resources */, BA8EA71B24A07B2200D5CC9F /* plugin-service-remote-auth-failure.json in Resources */, FA79F1872591730D00D235A9 /* backup-get-backup-status-complete-success.json in Resources */, 74D67F381F15C3740010C5ED /* site-viewers-delete-auth-failure.json in Resources */, diff --git a/WordPressKitTests/Mock Data/xmlrpc-response-invalid.html b/WordPressKitTests/Mock Data/xmlrpc-response-invalid.html new file mode 100644 index 00000000..6c25e2ca --- /dev/null +++ b/WordPressKitTests/Mock Data/xmlrpc-response-invalid.html @@ -0,0 +1,7 @@ + + + + website + + 👋 + diff --git a/WordPressKitTests/Mock Data/xmlrpc-response-list-methods.xml b/WordPressKitTests/Mock Data/xmlrpc-response-list-methods.xml new file mode 100644 index 00000000..68a88069 --- /dev/null +++ b/WordPressKitTests/Mock Data/xmlrpc-response-list-methods.xml @@ -0,0 +1,91 @@ + + + + + + + system.multicall + system.listMethods + system.getCapabilities + demo.addTwoNumbers + demo.sayHello + pingback.extensions.getPingbacks + pingback.ping + mt.publishPost + mt.getTrackbackPings + mt.supportedTextFilters + mt.supportedMethods + mt.setPostCategories + mt.getPostCategories + mt.getRecentPostTitles + mt.getCategoryList + metaWeblog.getUsersBlogs + metaWeblog.deletePost + metaWeblog.newMediaObject + metaWeblog.getCategories + metaWeblog.getRecentPosts + metaWeblog.getPost + metaWeblog.editPost + metaWeblog.newPost + blogger.deletePost + blogger.editPost + blogger.newPost + blogger.getRecentPosts + blogger.getPost + blogger.getUserInfo + blogger.getUsersBlogs + wp.restoreRevision + wp.getRevisions + wp.getPostTypes + wp.getPostType + wp.getPostFormats + wp.getMediaLibrary + wp.getMediaItem + wp.getCommentStatusList + wp.newComment + wp.editComment + wp.deleteComment + wp.getComments + wp.getComment + wp.setOptions + wp.getOptions + wp.getPageTemplates + wp.getPageStatusList + wp.getPostStatusList + wp.getCommentCount + wp.deleteFile + wp.uploadFile + wp.suggestCategories + wp.deleteCategory + wp.newCategory + wp.getTags + wp.getCategories + wp.getAuthors + wp.getPageList + wp.editPage + wp.deletePage + wp.newPage + wp.getPages + wp.getPage + wp.editProfile + wp.getProfile + wp.getUsers + wp.getUser + wp.getTaxonomies + wp.getTaxonomy + wp.getTerms + wp.getTerm + wp.deleteTerm + wp.editTerm + wp.newTerm + wp.getPosts + wp.getPost + wp.deletePost + wp.editPost + wp.newPost + wp.getUsersBlogs + + + + + diff --git a/WordPressKitTests/Mock Data/xmlrpc-response-mobile-plugin-redirect.html b/WordPressKitTests/Mock Data/xmlrpc-response-mobile-plugin-redirect.html new file mode 100644 index 00000000..fc8f70ea --- /dev/null +++ b/WordPressKitTests/Mock Data/xmlrpc-response-mobile-plugin-redirect.html @@ -0,0 +1,8 @@ + + + + + website + + 👋 + diff --git a/WordPressKitTests/WordPressAPI/WordPressOrgXMLRPCValidatorTests.swift b/WordPressKitTests/WordPressAPI/WordPressOrgXMLRPCValidatorTests.swift index a55abb82..04229fc8 100644 --- a/WordPressKitTests/WordPressAPI/WordPressOrgXMLRPCValidatorTests.swift +++ b/WordPressKitTests/WordPressAPI/WordPressOrgXMLRPCValidatorTests.swift @@ -7,6 +7,16 @@ final class WordPressOrgXMLRPCValidatorTests: XCTestCase { private let exampleURLString = "http://example.com" + override func setUp() { + super.setUp() + + // Report error on all unknown requests + stub(condition: { _ in true }) { + XCTFail("Unexpected request: \($0)") + return HTTPStubsResponse(error: URLError(URLError.Code.timedOut)) + } + } + override func tearDown() { super.tearDown() HTTPStubs.removeAllStubs() @@ -76,6 +86,296 @@ final class WordPressOrgXMLRPCValidatorTests: XCTestCase { // Then XCTAssertEqual(schemes, Set(arrayLiteral: "https", "http")) } + + func testNotWordPressSiteError() { + // Create HTTP stubs to simulate a plain static website + // - Return a plain HTML webpage for all GET requests + // - Return a 405 method not allowed error for all POST requests + + stub(condition: isHost("www.apple.com") && isMethodGET()) { _ in + fixture( + filePath: OHPathForFile("xmlrpc-response-invalid.html", type(of: self))!, + status: 200, + headers: nil + ) + } + stub(condition: isHost("www.apple.com") && isMethodPOST()) { _ in + fixture( + filePath: OHPathForFile("xmlrpc-response-invalid.html", type(of: self))!, + status: 405, + headers: nil + ) + } + + let failure = self.expectation(description: "returns error") + let validator = WordPressOrgXMLRPCValidator() + validator.guessXMLRPCURLForSite("https://www.apple.com/", userAgent: "test/1.0", success: { + XCTFail("Unexpected result: \($0)") + }) { error in + XCTAssertTrue(error is WordPressOrgXMLRPCValidatorError) + let validatorError = error as? WordPressOrgXMLRPCValidatorError + // Since the we simulate a plain static website in this test case, a 'notWordPressError' is the best error + // case to represent the error. But the current implementation returns an 'invalid' error, which is true too. + XCTAssertTrue(validatorError == .invalid || validatorError == .notWordPressError, "Got an error: \(error)") + failure.fulfill() + } + wait(for: [failure], timeout: 0.1) + } + + func testSuccessWithSiteAddress() { + stub(condition: isHost("www.apple.com") && isPath("/blog/xmlrpc.php")) { _ in + fixture( + filePath: OHPathForFile("xmlrpc-response-list-methods.xml", type(of: self))!, + status: 200, + headers: [ + "Content-Type": "application/xml" + ] + ) + } + + let success = self.expectation(description: "success result") + let validator = WordPressOrgXMLRPCValidator() + validator.guessXMLRPCURLForSite("https://www.apple.com/blog", userAgent: "test/1.0", success: { + XCTAssertEqual($0.absoluteString, "https://www.apple.com/blog/xmlrpc.php") + success.fulfill() + }) { + XCTFail("Unexpected result: \($0)") + } + wait(for: [success], timeout: 0.1) + } + + func testSuccessWithIrregularXMLRPCAddress() { + let apiCalls = [ + expectation(description: "Request #1: call xmlrpc.php"), + expectation(description: "Request #2: call the url argument"), + ] + + stub(condition: isHost("www.apple.com") && isPath("/blog/xmlrpc.php")) { _ in + apiCalls[0].fulfill() + return fixture( + filePath: OHPathForFile("xmlrpc-response-invalid.html", type(of: self))!, + status: 403, + headers: nil + ) + } + + stub(condition: isHost("www.apple.com") && isPath("/blog")) { _ in + apiCalls[1].fulfill() + return fixture( + filePath: OHPathForFile("xmlrpc-response-list-methods.xml", type(of: self))!, + status: 200, + headers: [ + "Content-Type": "application/xml" + ] + ) + } + + let success = self.expectation(description: "success result") + let validator = WordPressOrgXMLRPCValidator() + validator.guessXMLRPCURLForSite("https://www.apple.com/blog", userAgent: "test/1.0", success: { + XCTAssertEqual($0.absoluteString, "https://www.apple.com/blog") + success.fulfill() + }) { + XCTFail("Unexpected result: \($0)") + } + wait(for: apiCalls + [success], timeout: 0.3, enforceOrder: true) + } + + func testSuccessWithRSDLink() { + stub(condition: isHost("www.apple.com") && isPath("/blog/xmlrpc.php")) { _ in + return fixture( + filePath: OHPathForFile("xmlrpc-response-invalid.html", type(of: self))!, + status: 403, + headers: nil + ) + } + + stub(condition: isHost("www.apple.com") && isPath("/blog")) { _ in + let html = """ + + + + + test site + + hello world + + """ + return HTTPStubsResponse(data: html.data(using: .utf8)!, statusCode: 200, headers: nil) + } + + stub(condition: isAbsoluteURLString("https://www.apple.com/blog/rsd")) { _ in + // Grabbed from https://developer.wordpress.org/xmlrpc.php?rsd + let xml = """ + + + WordPress + https://wordpress.org/ + https://developer.wordpress.org + + + + + + + + + + """ + return HTTPStubsResponse( + data: xml.data(using: .utf8)!, + statusCode: 200, + headers:[ + "Content-Type": "application/xml" + ] + ) + } + + stub(condition: isHost("www.apple.com") && isPath("/blog-xmlrpc.php")) { _ in + fixture( + filePath: OHPathForFile("xmlrpc-response-list-methods.xml", type(of: self))!, + status: 200, + headers: [ + "Content-Type": "application/xml" + ] + ) + } + + let success = self.expectation(description: "success result") + let validator = WordPressOrgXMLRPCValidator() + validator.guessXMLRPCURLForSite("https://www.apple.com/blog", userAgent: "test/1.0", success: { + XCTAssertEqual($0.absoluteString, "https://www.apple.com/blog-xmlrpc.php") + success.fulfill() + }) { + XCTFail("Unexpected result: \($0)") + } + wait(for: [success], timeout: 0.3) + } + + func testManyRedirectsError() { + // redirect 'POST /redirect/' to '/redirect/'. + for number in 1...30 { + stub(condition: isMethodPOST() && { $0.url!.path.hasPrefix("/redirect/\(number)-req") }) { + HTTPStubsResponse(data: Data(), statusCode: 302, headers: [ + "Location": $0.url!.absoluteString.replacingOccurrences(of: "/redirect/\(number)-req", with: "/redirect/\(number + 1)-req") + ]) + } + } + + // All GET requests get a html webpage. + stub(condition: isMethodGET()) { _ in + fixture( + filePath: OHPathForFile("xmlrpc-response-invalid.html", type(of: self))!, + status: 405, + headers: nil + ) + } + + let failure = self.expectation(description: "returns error") + let validator = WordPressOrgXMLRPCValidator() + validator.guessXMLRPCURLForSite("https://www.apple.com/redirect/1-req", userAgent: "test/1.0", success: { + XCTFail("Unexpected result: \($0)") + }) { error in + // The test site here returns many redirection response, a 'httpTooManyRedirects' is the best error + // case to represent the error. But the current implementation returns an 'invalid' error, which is true too. + XCTAssertTrue( + (error as? WordPressOrgXMLRPCValidatorError == .invalid) + || (error as? URLError) == URLError(URLError.Code.httpTooManyRedirects), + "Got an error: \(error)" + ) + failure.fulfill() + } + wait(for: [failure], timeout: 0.3) + } + + func testMobilePluginRedirectedError() { + // redirect 'POST /redirect/' to '/redirect/'. + stub(condition: isMethodPOST() && isHost("www.apple.com")) { _ in + HTTPStubsResponse(data: Data(), statusCode: 302, headers: [ + "Location": "https://m.apple.com" + ]) + } + stub(condition: isMethodPOST() && isHost("m.apple.com")) { _ in + fixture( + filePath: OHPathForFile("xmlrpc-response-mobile-plugin-redirect.html", type(of: self))!, + status: 200, + headers: nil + ) + } + + let failure = self.expectation(description: "returns error") + let validator = WordPressOrgXMLRPCValidator() + validator.guessXMLRPCURLForSite("https://www.apple.com/xmlrpc.php", userAgent: "test/1.0", success: { + XCTFail("Unexpected result: \($0)") + }) { error in + XCTAssertTrue(error is WordPressOrgXMLRPCValidatorError) + // The test site here returns many redirection response, a 'httpTooManyRedirects' is the best error + // case to represent the error. But the current implementation returns an 'invalid' error, which is true too. + XCTAssertEqual(error as? WordPressOrgXMLRPCValidatorError, .mobilePluginRedirectedError) + failure.fulfill() + } + wait(for: [failure], timeout: 0.3) + } + + func testForbiddenError() { + // All requests get a '403 Forbidden' error. + stub(condition: isHost("www.apple.com")) { _ in + HTTPStubsResponse(data: Data(), statusCode: 403, headers: nil) + } + + let failure = self.expectation(description: "returns error") + let validator = WordPressOrgXMLRPCValidator() + validator.guessXMLRPCURLForSite("https://www.apple.com/xmlrpc.php", userAgent: "test/1.0", success: { + XCTFail("Unexpected result: \($0)") + }) { error in + XCTAssertTrue(error is WordPressOrgXMLRPCValidatorError) + let validatorError = error as? WordPressOrgXMLRPCValidatorError + // The site returns 403 for all requests, a 'forbidden' error is the best error case to represent the error. + // But the current implementation returns an 'invalid' error, which is true too. + XCTAssertTrue(validatorError == .invalid || validatorError == .forbidden, "Got an error: \(error)") + failure.fulfill() + } + wait(for: [failure], timeout: 0.3) + } + + func testXMLRPCMissingError() { + stub(condition: isAbsoluteURLString("https://www.apple.com/xmlrpc.php") || isAbsoluteURLString("http://www.apple.com/xmlrpc.php")) { _ in + HTTPStubsResponse(data: Data(), statusCode: 403, headers: nil) + } + + stub(condition: isHost("www.apple.com") && isMethodGET()) { _ in + let html = """ + + + + + test site + + hello world + + """ + return HTTPStubsResponse(data: html.data(using: .utf8)!, statusCode: 200, headers: nil) + } + + stub(condition: isAbsoluteURLString("https://www.apple.com/rsd")) { _ in + HTTPStubsResponse(data: Data(), statusCode: 404, headers: nil) + } + + let failure = self.expectation(description: "returns error") + let validator = WordPressOrgXMLRPCValidator() + validator.guessXMLRPCURLForSite("https://www.apple.com/xmlrpc.php", userAgent: "test/1.0", success: { + XCTFail("Unexpected result: \($0)") + }) { error in + XCTAssertTrue(error is WordPressOrgXMLRPCValidatorError) + let validatorError = error as? WordPressOrgXMLRPCValidatorError + // The site returns provides a RSD link that returns 404. A 'xmlrpc_missing' error is the best error case + // to represent the error. But the current implementation returns an 'invalid' error, which is true too. + XCTAssertTrue(validatorError == .xmlrpc_missing || validatorError == .invalid, "Got an error: \(error)") + failure.fulfill() + } + wait(for: [failure], timeout: 0.3) + } + } private extension WordPressOrgXMLRPCValidatorTests { From 616150b2598d8143fd7da521754aa5c112fd78c9 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 22 Dec 2023 13:14:58 +1300 Subject: [PATCH 2/2] Fix a swiftlint issue --- .../WordPressAPI/WordPressOrgXMLRPCValidatorTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPressKitTests/WordPressAPI/WordPressOrgXMLRPCValidatorTests.swift b/WordPressKitTests/WordPressAPI/WordPressOrgXMLRPCValidatorTests.swift index 04229fc8..52073faf 100644 --- a/WordPressKitTests/WordPressAPI/WordPressOrgXMLRPCValidatorTests.swift +++ b/WordPressKitTests/WordPressAPI/WordPressOrgXMLRPCValidatorTests.swift @@ -225,7 +225,7 @@ final class WordPressOrgXMLRPCValidatorTests: XCTestCase { return HTTPStubsResponse( data: xml.data(using: .utf8)!, statusCode: 200, - headers:[ + headers: [ "Content-Type": "application/xml" ] )