Skip to content

Commit e4bdf63

Browse files
authored
Merge pull request #4095 from sfdctaka/feature/token-exchange-error-tests
Add tests for OAuth2 token endpoint error classification
2 parents a2a271c + 2390f7c commit e4bdf63

2 files changed

Lines changed: 170 additions & 0 deletions

File tree

libs/SalesforceSDKCore/SalesforceSDKCore.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
010A9B591CC1A147002AF4D3 /* SFCryptoStreamTestUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 010A9B521CC1A131002AF4D3 /* SFCryptoStreamTestUtils.m */; };
2828
1A31073F5F374B9EB1162F2E /* SFOAuthCoordinatorLightningURLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399A11508BCB47F490DFB724 /* SFOAuthCoordinatorLightningURLTests.swift */; };
2929
230834842DF7838200C7CBF7 /* URLSessionTask+RetryPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 230834832DF7837400C7CBF7 /* URLSessionTask+RetryPolicy.swift */; };
30+
3D012B381C4D4D97BCDA30E6 /* SFSDKOAuth2TokenExchangeErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4746050277064A9790E3470A /* SFSDKOAuth2TokenExchangeErrorTests.swift */; };
3031
230834862DF8938D00C7CBF7 /* URLSessionTask+RetryPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 230834852DF8938D00C7CBF7 /* URLSessionTask+RetryPolicyTests.swift */; };
3132
230834882DF8A8F300C7CBF7 /* WebSocketClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 230834872DF8A8F300C7CBF7 /* WebSocketClientTests.swift */; };
3233
23350C562DCAB19D009A10DE /* RestClient+Blocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23350C552DCAB190009A10DE /* RestClient+Blocks.swift */; };
@@ -578,6 +579,7 @@
578579
23F200AD2E551C890091C5F5 /* BootconfigTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BootconfigTests.swift; path = SalesforceSDKCoreTests/BootconfigTests.swift; sourceTree = SOURCE_ROOT; };
579580
399A11508BCB47F490DFB724 /* SFOAuthCoordinatorLightningURLTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SFOAuthCoordinatorLightningURLTests.swift; path = ../SalesforceSDKCoreTests/SFOAuthCoordinatorLightningURLTests.swift; sourceTree = "<group>"; };
580581
444B95CF1E83251900908C61 /* UIColor+SFColorsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "UIColor+SFColorsTests.m"; path = "SalesforceSDKCoreTests/UIColor+SFColorsTests.m"; sourceTree = SOURCE_ROOT; };
582+
4746050277064A9790E3470A /* SFSDKOAuth2TokenExchangeErrorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SFSDKOAuth2TokenExchangeErrorTests.swift; path = ../SalesforceSDKCoreTests/SFSDKOAuth2TokenExchangeErrorTests.swift; sourceTree = "<group>"; };
581583
4F06AF5D1C49A16A00F70798 /* NSURL+SFStringUtilsTests.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSURL+SFStringUtilsTests.h"; path = "SalesforceSDKCoreTests/NSURL+SFStringUtilsTests.h"; sourceTree = SOURCE_ROOT; };
582584
4F06AF5E1C49A16A00F70798 /* NSURL+SFStringUtilsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSURL+SFStringUtilsTests.m"; path = "SalesforceSDKCoreTests/NSURL+SFStringUtilsTests.m"; sourceTree = SOURCE_ROOT; };
583585
4F06AF5F1C49A16A00F70798 /* SalesforceOAuthUnitTests.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SalesforceOAuthUnitTests.h; path = SalesforceSDKCoreTests/SalesforceOAuthUnitTests.h; sourceTree = SOURCE_ROOT; };
@@ -1113,6 +1115,7 @@
11131115
B7A901BD228E4DFA0036D749 /* SFSDKLogoutBlocker.m */,
11141116
399A11508BCB47F490DFB724 /* SFOAuthCoordinatorLightningURLTests.swift */,
11151117
4FOAUTHECT002E98711600C89DDD /* SFOAuthErrorCodeTests.swift */,
1118+
4746050277064A9790E3470A /* SFSDKOAuth2TokenExchangeErrorTests.swift */,
11161119
4F9E052C2DD6A06F00548985 /* SFSDKOAuthTokenEndpointResponseTests.m */,
11171120
69848CBB2364063E00893E57 /* SFSDKPushNotificationDataProvider.h */,
11181121
69848CBC2364063E00893E57 /* SFSDKPushNotificationDataProvider.m */,
@@ -2261,6 +2264,7 @@
22612264
4FA1B2C32F0E000000000001 /* LoginForAdminTests.swift in Sources */,
22622265
1A31073F5F374B9EB1162F2E /* SFOAuthCoordinatorLightningURLTests.swift in Sources */,
22632266
4FOAUTHECT012E98711600C89DDD /* SFOAuthErrorCodeTests.swift in Sources */,
2267+
3D012B381C4D4D97BCDA30E6 /* SFSDKOAuth2TokenExchangeErrorTests.swift in Sources */,
22642268
4F9E05322DD6A08000548985 /* SFSDKOAuthTokenEndpointResponseTests.m in Sources */,
22652269
4F06AF8D1C49A18E00F70798 /* SalesforceSDKManagerTests.m in Sources */,
22662270
237C186C2E44FCAE0008015C /* EncryptStreamTests.swift in Sources */,
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
/*
2+
SFSDKOAuth2TokenExchangeErrorTests.swift
3+
SalesforceSDKCoreTests
4+
5+
Copyright (c) 2026-present, salesforce.com, inc. All rights reserved.
6+
7+
Redistribution and use of this software in source and binary forms, with or without modification,
8+
are permitted provided that the following conditions are met:
9+
* Redistributions of source code must retain the above copyright notice, this list of conditions
10+
and the following disclaimer.
11+
* Redistributions in binary form must reproduce the above copyright notice, this list of
12+
conditions and the following disclaimer in the documentation and/or other materials provided
13+
with the distribution.
14+
* Neither the name of salesforce.com, inc. nor the names of its contributors may be used to
15+
endorse or promote products derived from this software without specific prior written
16+
permission of salesforce.com, inc.
17+
18+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
19+
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
20+
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
21+
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
24+
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
25+
WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26+
*/
27+
28+
import XCTest
29+
@testable import SalesforceSDKCore
30+
31+
final class SFSDKOAuth2TokenExchangeErrorTests: XCTestCase {
32+
33+
// MARK: - Helper
34+
35+
/// Builds a response from a wire-format error dict and asserts all four
36+
/// observable error fields at once. Callers pass file/line so failing
37+
/// assertions carry the caller's location.
38+
private func assertError(
39+
wire: String,
40+
description: String,
41+
expectedEnum: SFOAuthErrorCode,
42+
file: StaticString = #file,
43+
line: UInt = #line
44+
) {
45+
let params = ["error": wire, "error_description": description]
46+
guard let response = SFSDKOAuthTokenEndpointResponse(dictionary: params, parseAdditionalFields: nil) else {
47+
XCTFail("SFSDKOAuthTokenEndpointResponse initializer returned nil", file: file, line: line)
48+
return
49+
}
50+
51+
XCTAssertTrue(response.hasError, "response.hasError should be true", file: file, line: line)
52+
XCTAssertEqual(response.error?.tokenEndpointErrorCode, wire, file: file, line: line)
53+
XCTAssertEqual(response.error?.errorCode, expectedEnum.rawValue, file: file, line: line)
54+
XCTAssertEqual(response.error?.tokenEndpointErrorDescription, description, file: file, line: line)
55+
56+
// Sanity check: response.error.error is a non-nil NSError in kSFOAuthErrorDomain
57+
XCTAssertNotNil(response.error?.error, "response.error.error should be non-nil", file: file, line: line)
58+
let nsError = response.error?.error as NSError?
59+
XCTAssertEqual(nsError?.domain, kSFOAuthErrorDomain, file: file, line: line)
60+
}
61+
62+
// MARK: - invalid_grant family
63+
// Invalid, expired, and redirect_uri-mismatched authorization codes all
64+
// collapse to the same client-side branch; we exercise the shared branch
65+
// three times with distinct server-provided descriptions.
66+
67+
func test_givenInvalidGrant_whenInitDictionary_thenClassifiedAsInvalidGrant() {
68+
let wire = SFOAuthErrorCode.invalidGrant.wireValue!
69+
70+
// Invalid authorization code (garbage, replayed, or never issued)
71+
assertError(
72+
wire: wire,
73+
description: "authorization code invalid",
74+
expectedEnum: .invalidGrant
75+
)
76+
77+
// Expired authorization code (>~10 min old)
78+
assertError(
79+
wire: wire,
80+
description: "expired authorization code",
81+
expectedEnum: .invalidGrant
82+
)
83+
84+
// Mismatched redirect_uri at exchange
85+
assertError(
86+
wire: wire,
87+
description: "redirect_uri mismatch",
88+
expectedEnum: .invalidGrant
89+
)
90+
}
91+
92+
// MARK: - invalid_client_id and invalid_client
93+
94+
func test_givenInvalidClientId_whenInitDictionary_thenClassifiedAsInvalidClientId() {
95+
let wire = SFOAuthErrorCode.invalidClientId.wireValue!
96+
assertError(
97+
wire: wire,
98+
description: "client identifier invalid",
99+
expectedEnum: .invalidClientId
100+
)
101+
}
102+
103+
func test_givenInvalidClient_whenInitDictionary_thenClassifiedAsInvalidClient() {
104+
let wire = SFOAuthErrorCode.invalidClient.wireValue!
105+
assertError(
106+
wire: wire,
107+
description: "client authentication failed",
108+
expectedEnum: .invalidClient
109+
)
110+
}
111+
112+
// MARK: - unsupported_grant_type
113+
114+
func test_givenUnsupportedGrantType_whenInitDictionary_thenClassifiedAsUnsupportedGrantType() {
115+
let wire = SFOAuthErrorCode.unsupportedGrantType.wireValue!
116+
assertError(
117+
wire: wire,
118+
description: "grant type not supported",
119+
expectedEnum: .unsupportedGrantType
120+
)
121+
}
122+
123+
// MARK: - invalid_request
124+
125+
func test_givenInvalidRequest_whenInitDictionary_thenClassifiedAsInvalidRequest() {
126+
let wire = SFOAuthErrorCode.invalidRequest.wireValue!
127+
assertError(
128+
wire: wire,
129+
description: "missing required parameter",
130+
expectedEnum: .invalidRequest
131+
)
132+
}
133+
134+
// MARK: - Enum mapping lock-in for the five distinct wire values
135+
136+
func test_from_allTokenEndpointWireValues_returnCorrectEnumCase() {
137+
let testCases: [(String, SFOAuthErrorCode)] = [
138+
("invalid_grant", .invalidGrant),
139+
("invalid_client_id", .invalidClientId),
140+
("invalid_client", .invalidClient),
141+
("unsupported_grant_type", .unsupportedGrantType),
142+
("invalid_request", .invalidRequest)
143+
]
144+
145+
for (wire, expected) in testCases {
146+
XCTAssertEqual(SFOAuthErrorCode.from(wire), expected, "Wire '\(wire)' should map to \(expected)")
147+
}
148+
}
149+
150+
// MARK: - Control — success response has no error
151+
152+
func test_givenSuccessResponse_whenInitDictionary_thenHasErrorIsFalse() {
153+
let params = [
154+
"access_token": "00D1234567890abcd!ARMAQGu.test",
155+
"refresh_token": "5Aep1234567890abcd!AREAQItest",
156+
"instance_url": "https://na1.salesforce.com"
157+
]
158+
guard let response = SFSDKOAuthTokenEndpointResponse(dictionary: params, parseAdditionalFields: nil) else {
159+
XCTFail("SFSDKOAuthTokenEndpointResponse initializer returned nil")
160+
return
161+
}
162+
163+
XCTAssertFalse(response.hasError, "response.hasError should be false for success response")
164+
XCTAssertNil(response.error, "response.error should be nil for success response")
165+
}
166+
}

0 commit comments

Comments
 (0)