Skip to content

Commit e4c2521

Browse files
committed
Merge branch 'trunk' into woomob-1252-sync-coordinator-store-sync-date-on-site
2 parents 3e41b4b + d17e8a4 commit e4c2521

File tree

16 files changed

+541
-3
lines changed

16 files changed

+541
-3
lines changed

Modules/Sources/Networking/Model/SiteAPI.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,17 @@ public struct SiteAPI: Decodable, Equatable, GeneratedFakeable {
4545
}
4646

4747
let siteAPIContainer = try decoder.container(keyedBy: SiteAPIKeys.self)
48-
let namespaces = siteAPIContainer.failsafeDecodeIfPresent([String].self, forKey: .namespaces) ?? []
48+
49+
/// Some third-party plugins (like CoCart API) alter the response of `namespaces` field into a dictionary instead of array.
50+
/// This workaround transforms the unexpected dictionary to extract the values in the dictionary.
51+
let namespaces = siteAPIContainer.failsafeDecodeIfPresent(
52+
targetType: [String].self,
53+
forKey: .namespaces,
54+
alternativeTypes: [
55+
.dictionary(transform: { Array($0.values) })
56+
]
57+
) ?? []
58+
4959
let authentication = try? siteAPIContainer.decode(Authentication.self, forKey: .authentication)
5060
let applicationPasswordAvailable = authentication?.applicationPasswords?.endpoints?.authorization != nil
5161

Modules/Sources/NetworkingCore/Extensions/KeyedDecodingContainer+Woo.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ public enum AlternativeDecodingType<T> {
66
case string(transform: (String) -> T)
77
case bool(transform: (Bool) -> T)
88
case integer(transform: (Int) -> T)
9+
case dictionary(transform: ([String: String]) -> T)
910
}
1011

1112
// MARK: - KeyedDecodingContainer: Bulletproof JSON Decoding.
@@ -60,6 +61,10 @@ public extension KeyedDecodingContainer {
6061
if let result = failsafeDecodeIfPresent(integerForKey: key) {
6162
return transform(result)
6263
}
64+
case .dictionary(let transform):
65+
if let result = failsafeDecodeIfPresent([String: String].self, forKey: key) {
66+
return transform(result)
67+
}
6368
}
6469
}
6570
return nil

Modules/Sources/NetworkingCore/Network/AlamofireNetwork.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ public struct JetpackSite: Equatable {
1515
}
1616
}
1717

18+
public enum RequestAuthenticationMode: String {
19+
case appPasswords = "app_passwords"
20+
case appPasswordsWithJetpack = "app_passwords_with_jetpack" // switching to app password for Jetpack sites
21+
case jetpackTunnel = "jetpack_tunnel"
22+
}
23+
1824
extension Alamofire.MultipartFormData: MultipartFormData {
1925
public func append(_ data: Data, withName name: String) {
2026
self.append(data, withName: name, fileName: nil, mimeType: nil)
@@ -24,6 +30,10 @@ extension Alamofire.MultipartFormData: MultipartFormData {
2430
/// AlamofireWrapper: Encapsulates all of the Alamofire OP's
2531
///
2632
public class AlamofireNetwork: Network {
33+
34+
/// authentication mode for requests
35+
public private(set) var authenticationMode: RequestAuthenticationMode?
36+
2737
/// Lazy-initialized session manager. Use ensuresSessionManagerIsInitialized=true to avoid race conditions with concurrent requests.
2838
private lazy var alamofireSession: Alamofire.Session = {
2939
let sessionConfiguration = URLSessionConfiguration.default
@@ -88,6 +98,18 @@ public class AlamofireNetwork: Network {
8898
} else if ensuresSessionManagerIsInitialized {
8999
self.alamofireSession = makeSession(configuration: URLSessionConfiguration.default)
90100
}
101+
102+
let authenticationMode: RequestAuthenticationMode? = {
103+
switch credentials {
104+
case .wporg, .applicationPassword:
105+
return .appPasswords
106+
case .wpcom:
107+
return .jetpackTunnel
108+
case .none:
109+
return nil
110+
}
111+
}()
112+
updateAuthenticationMode(authenticationMode)
91113
}
92114

93115
public func updateAppPasswordSwitching(enabled: Bool) {
@@ -98,6 +120,7 @@ public class AlamofireNetwork: Network {
98120
requestConverter = RequestConverter(siteAddress: nil)
99121
requestAuthenticator.updateAuthenticator(DefaultRequestAuthenticator(credentials: credentials))
100122
requestAuthenticator.delegate = nil
123+
updateAuthenticationMode(.jetpackTunnel)
101124
}
102125
}
103126

@@ -276,6 +299,7 @@ private extension AlamofireNetwork {
276299
requestConverter = RequestConverter(siteAddress: nil)
277300
requestAuthenticator.updateAuthenticator(DefaultRequestAuthenticator(credentials: credentials))
278301
requestAuthenticator.delegate = nil
302+
updateAuthenticationMode(.jetpackTunnel)
279303
return
280304
}
281305
requestConverter = RequestConverter(siteAddress: site.siteAddress)
@@ -286,8 +310,15 @@ private extension AlamofireNetwork {
286310
))
287311
requestAuthenticator.delegate = self
288312
errorHandler.resetFailureCount(for: site.siteID) // reset failure count
313+
updateAuthenticationMode(.appPasswordsWithJetpack)
289314
}
290315
}
316+
317+
func updateAuthenticationMode(_ mode: RequestAuthenticationMode?) {
318+
DispatchQueue.main.async { [weak self] in
319+
self?.authenticationMode = mode
320+
}
321+
}
291322
}
292323

293324
// MARK: Helper methods for error handling

Modules/Sources/Storage/GRDB/GRDBManager.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import GRDB
33

44
public protocol GRDBManagerProtocol {
55
var databaseConnection: GRDBDatabaseConnection { get }
6+
func reset() throws
67
}
78

89
public protocol GRDBDatabaseConnection: DatabaseReader & DatabaseWriter {}
@@ -27,6 +28,31 @@ public final class GRDBManager: GRDBManagerProtocol {
2728
self.databaseConnection = try DatabaseQueue()
2829
try migrateIfNeeded()
2930
}
31+
32+
/// Resets the database by deleting all data from all tables
33+
/// Used when user logs out to ensure no data leaks between sessions
34+
public func reset() throws {
35+
try databaseConnection.write { db in
36+
// Disable foreign key constraints temporarily to avoid dependency issues
37+
try db.execute(sql: "PRAGMA foreign_keys = OFF")
38+
39+
// Get all user tables (excluding sqlite internal tables)
40+
let tableNames = try String.fetchAll(db, sql: """
41+
SELECT name FROM sqlite_master
42+
WHERE type = 'table'
43+
AND name NOT LIKE 'sqlite_%'
44+
AND name NOT LIKE 'grdb_%'
45+
""")
46+
47+
// Delete all data from each table
48+
for tableName in tableNames {
49+
try db.execute(sql: "DELETE FROM \(tableName)")
50+
}
51+
52+
// Re-enable foreign key constraints
53+
try db.execute(sql: "PRAGMA foreign_keys = ON")
54+
}
55+
}
3056
}
3157

3258
private extension GRDBManager {

Modules/Sources/Yosemite/Base/StoresManager.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Combine
22
import Foundation
3+
import enum NetworkingCore.RequestAuthenticationMode
34

45
/// Abstracts the Stores coordination
56
///
@@ -53,6 +54,9 @@ public protocol StoresManager {
5354
///
5455
var isAuthenticatedWithoutWPCom: Bool { get }
5556

57+
/// Authentication mode for network requests
58+
var requestAuthenticationMode: RequestAuthenticationMode? { get }
59+
5660
/// Publishes signal that indicates if the user is currently logged in with credentials.
5761
///
5862
var isLoggedInPublisher: AnyPublisher<Bool, Never> { get }

Modules/Sources/Yosemite/Model/Mocks/MockStoresManager.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Combine
22
import Storage
3+
import enum NetworkingCore.RequestAuthenticationMode
34

45
public class MockStoresManager: StoresManager {
56

@@ -223,6 +224,10 @@ public class MockStoresManager: StoresManager {
223224
false
224225
}
225226

227+
public var requestAuthenticationMode: RequestAuthenticationMode? {
228+
.jetpackTunnel
229+
}
230+
226231
public var needsDefaultStore: Bool {
227232
sessionManager.defaultStoreID == nil
228233
}

Modules/Tests/NetworkingTests/Mapper/SiteAPIMapperTests.swift

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import XCTest
55

66
/// SiteAPIMapperTests Unit Tests
77
///
8-
class SiteAPIMapperTests: XCTestCase {
8+
final class SiteAPIMapperTests: XCTestCase {
99

1010
/// Dummy Site ID.
1111
///
@@ -54,6 +54,18 @@ class SiteAPIMapperTests: XCTestCase {
5454
XCTAssertEqual(apiSettings?.namespaces, dummyBrokenNamespaces)
5555
XCTAssertEqual(apiSettings?.highestWooVersion, WooAPIVersion.none)
5656
}
57+
58+
/// Verifies the SiteSetting fields are parsed correctly.
59+
///
60+
func test_SiteSetting_with_malformed_namespaces_fields_are_properly_parsed() {
61+
let apiSettings = mapLoadSiteAPIResponseWithMalformedNamespaces()
62+
63+
XCTAssertNotNil(apiSettings)
64+
XCTAssertEqual(apiSettings?.siteID, dummySiteID)
65+
XCTAssertNotNil(apiSettings?.namespaces)
66+
XCTAssertEqual(apiSettings?.namespaces.sorted(), dummyNamespaces.sorted())
67+
XCTAssertEqual(apiSettings?.highestWooVersion, WooAPIVersion.mark3)
68+
}
5769
}
5870

5971

@@ -88,4 +100,10 @@ private extension SiteAPIMapperTests {
88100
func mapLoadBrokenSiteAPIResponse() -> SiteAPI? {
89101
return mapSiteAPIData(from: "site-api-no-woo")
90102
}
103+
104+
/// Returns the SiteAPIMapper output with malformed namespaces
105+
///
106+
func mapLoadSiteAPIResponseWithMalformedNamespaces() -> SiteAPI? {
107+
return mapSiteAPIData(from: "site-api-malformed")
108+
}
91109
}

Modules/Tests/NetworkingTests/Network/AlamofireNetworkTests.swift

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,142 @@ final class AlamofireNetworkTests: XCTestCase {
668668
let networkError = result.1 as? NetworkError
669669
XCTAssertEqual(networkError?.errorCode, "failed")
670670
}
671+
672+
// MARK: - Authentication Mode Tests
673+
674+
func test_authenticationMode_is_appPasswords_for_wporg_credentials() {
675+
// Given
676+
let wporgCredentials = Credentials.wporg(username: "user", password: "pass", siteAddress: "https://example.com")
677+
678+
// When
679+
let network = AlamofireNetwork(credentials: wporgCredentials, sessionManager: createSessionWithMockURLProtocol())
680+
681+
// Then
682+
let expectation = XCTestExpectation(description: "Authentication mode should be set")
683+
DispatchQueue.main.async {
684+
XCTAssertEqual(network.authenticationMode, .appPasswords)
685+
expectation.fulfill()
686+
}
687+
wait(for: [expectation], timeout: 1.0)
688+
}
689+
690+
func test_authenticationMode_is_appPasswords_for_applicationPassword_credentials() {
691+
// Given
692+
let appPasswordCredentials = Credentials.applicationPassword(username: "user", password: "pass", siteAddress: "https://example.com")
693+
694+
// When
695+
let network = AlamofireNetwork(credentials: appPasswordCredentials, sessionManager: createSessionWithMockURLProtocol())
696+
697+
// Then
698+
let expectation = XCTestExpectation(description: "Authentication mode should be set")
699+
DispatchQueue.main.async {
700+
XCTAssertEqual(network.authenticationMode, .appPasswords)
701+
expectation.fulfill()
702+
}
703+
wait(for: [expectation], timeout: 1.0)
704+
}
705+
706+
func test_authenticationMode_is_jetpackTunnel_for_wpcom_credentials() {
707+
// Given
708+
let wpcomCredentials = createWPComCredentials()
709+
710+
// When
711+
let network = AlamofireNetwork(credentials: wpcomCredentials, sessionManager: createSessionWithMockURLProtocol())
712+
713+
// Then
714+
let expectation = XCTestExpectation(description: "Authentication mode should be set")
715+
DispatchQueue.main.async {
716+
XCTAssertEqual(network.authenticationMode, .jetpackTunnel)
717+
expectation.fulfill()
718+
}
719+
wait(for: [expectation], timeout: 1.0)
720+
}
721+
722+
func test_authenticationMode_is_nil_for_no_credentials() {
723+
// When
724+
let network = AlamofireNetwork(credentials: nil, sessionManager: createSessionWithMockURLProtocol())
725+
726+
// Then
727+
let expectation = XCTestExpectation(description: "Authentication mode should be set")
728+
DispatchQueue.main.async {
729+
XCTAssertNil(network.authenticationMode)
730+
expectation.fulfill()
731+
}
732+
wait(for: [expectation], timeout: 1.0)
733+
}
734+
735+
func test_authenticationMode_changes_to_appPasswordsWithJetpack_when_app_password_switching_enabled() {
736+
// Given
737+
let siteID: Int64 = 123
738+
let wpcomCredentials = createWPComCredentials()
739+
let network = createNetworkWithSelectedSite(siteID: siteID, credentials: wpcomCredentials, userDefaults: userDefaults)
740+
741+
// When - Enable app password switching
742+
network.updateAppPasswordSwitching(enabled: true)
743+
744+
// Then
745+
let expectation = XCTestExpectation(description: "Authentication mode should change")
746+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
747+
XCTAssertEqual(network.authenticationMode, .appPasswordsWithJetpack)
748+
expectation.fulfill()
749+
}
750+
wait(for: [expectation], timeout: 1.0)
751+
}
752+
753+
func test_authenticationMode_reverts_to_jetpackTunnel_when_app_password_switching_disabled() {
754+
// Given
755+
let siteID: Int64 = 456
756+
let wpcomCredentials = createWPComCredentials()
757+
let network = createNetworkWithSelectedSite(siteID: siteID, credentials: wpcomCredentials, userDefaults: userDefaults)
758+
759+
// When - Enable then disable app password switching
760+
network.updateAppPasswordSwitching(enabled: true)
761+
network.updateAppPasswordSwitching(enabled: false)
762+
763+
// Then
764+
let expectation = XCTestExpectation(description: "Authentication mode should revert")
765+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
766+
XCTAssertEqual(network.authenticationMode, .jetpackTunnel)
767+
expectation.fulfill()
768+
}
769+
wait(for: [expectation], timeout: 1.0)
770+
}
771+
772+
func test_authenticationMode_remains_jetpackTunnel_when_site_flagged_as_unsupported() {
773+
// Given
774+
let siteID: Int64 = 789
775+
let wpcomCredentials = createWPComCredentials()
776+
userDefaults.applicationPasswordUnsupportedList = [String(siteID): Date()]
777+
let network = createNetworkWithSelectedSite(siteID: siteID, credentials: wpcomCredentials, userDefaults: userDefaults)
778+
779+
// When - Enable app password switching for an unsupported site
780+
network.updateAppPasswordSwitching(enabled: true)
781+
782+
// Then
783+
let expectation = XCTestExpectation(description: "Authentication mode should remain jetpackTunnel")
784+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
785+
XCTAssertEqual(network.authenticationMode, .jetpackTunnel)
786+
expectation.fulfill()
787+
}
788+
wait(for: [expectation], timeout: 1.0)
789+
}
790+
791+
func test_authenticationMode_does_not_change_for_non_wpcom_credentials() {
792+
// Given
793+
let wporgCredentials = Credentials.wporg(username: "user", password: "pass", siteAddress: "https://example.com")
794+
let network = AlamofireNetwork(credentials: wporgCredentials, sessionManager: createSessionWithMockURLProtocol())
795+
796+
// When - Try to enable app password switching (should have no effect)
797+
network.updateAppPasswordSwitching(enabled: true)
798+
799+
// Then
800+
let expectation = XCTestExpectation(description: "Authentication mode should remain unchanged")
801+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
802+
XCTAssertEqual(network.authenticationMode, .appPasswords)
803+
expectation.fulfill()
804+
}
805+
wait(for: [expectation], timeout: 1.0)
806+
}
671807
}
672808

673809
private extension AlamofireNetworkTests {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"data": {
3+
"namespaces": {
4+
"0": "oembed\/1.0",
5+
"1": "akismet\/v1",
6+
"2": "jetpack\/v4",
7+
"3": "wpcom\/v2",
8+
"4": "wc\/v1",
9+
"5": "wc\/v2",
10+
"6": "wc\/v3",
11+
"7": "wc-pb\/v3",
12+
"8": "wp\/v2"
13+
},
14+
"authentication": [],
15+
"_links": {
16+
"help": [
17+
{
18+
"href": "http:\/\/v2.wp-api.org\/"
19+
}
20+
]
21+
}
22+
}
23+
}

0 commit comments

Comments
 (0)