Skip to content
This repository was archived by the owner on Sep 15, 2025. It is now read-only.

Commit 60e1403

Browse files
authored
Merge pull request #605 from wordpress-mobile/feature/add-blaze-campaigns-endpoints
Add Blaze campaigns search endpoint
2 parents 58e322b + 762d8ea commit 60e1403

File tree

8 files changed

+319
-9
lines changed

8 files changed

+319
-9
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ _None._
3838

3939
### New Features
4040

41-
_None._
41+
- Add Blaze campaigns search endpoint [#605]
4242

4343
### Bug Fixes
4444

WordPressKit.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Pod::Spec.new do |s|
44
s.name = 'WordPressKit'
5-
s.version = '8.2.0'
5+
s.version = '8.3.0-beta.1'
66

77
s.summary = 'WordPressKit offers a clean and simple WordPress.com and WordPress.org API.'
88
s.description = <<-DESC

WordPressKit.xcodeproj/project.pbxproj

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88

99
/* Begin PBXBuildFile section */
1010
0152100C28EDA9E400DD6783 /* StatsAnnualAndMostPopularTimeInsightDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0152100B28EDA9E400DD6783 /* StatsAnnualAndMostPopularTimeInsightDecodingTests.swift */; };
11+
0CB1905E2A2A5E83004D3E80 /* BlazeCampaign.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB1905D2A2A5E83004D3E80 /* BlazeCampaign.swift */; };
12+
0CB190612A2A6A13004D3E80 /* blaze-campaigns-search.json in Resources */ = {isa = PBXBuildFile; fileRef = 0CB1905F2A2A6943004D3E80 /* blaze-campaigns-search.json */; };
13+
0CB190652A2A7569004D3E80 /* BlazeCampaignsSearchResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB190642A2A7569004D3E80 /* BlazeCampaignsSearchResponse.swift */; };
1114
1769DEAA24729AFF00F42EFC /* HomepageSettingsServiceRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1769DEA924729AFF00F42EFC /* HomepageSettingsServiceRemote.swift */; };
1215
17BF9A6C20C7DC3300BF57D2 /* reader-site-search-success.json in Resources */ = {isa = PBXBuildFile; fileRef = 17BF9A6B20C7DC3300BF57D2 /* reader-site-search-success.json */; };
1316
17BF9A7220C7E18200BF57D2 /* reader-site-search-success-hasmore.json in Resources */ = {isa = PBXBuildFile; fileRef = 17BF9A6D20C7E18100BF57D2 /* reader-site-search-success-hasmore.json */; };
@@ -675,6 +678,10 @@
675678

676679
/* Begin PBXFileReference section */
677680
0152100B28EDA9E400DD6783 /* StatsAnnualAndMostPopularTimeInsightDecodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsAnnualAndMostPopularTimeInsightDecodingTests.swift; sourceTree = "<group>"; };
681+
0C3A2A412A2E7BA500FD91D6 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = "<group>"; };
682+
0CB1905D2A2A5E83004D3E80 /* BlazeCampaign.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaign.swift; sourceTree = "<group>"; };
683+
0CB1905F2A2A6943004D3E80 /* blaze-campaigns-search.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "blaze-campaigns-search.json"; sourceTree = "<group>"; };
684+
0CB190642A2A7569004D3E80 /* BlazeCampaignsSearchResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignsSearchResponse.swift; sourceTree = "<group>"; };
678685
1769DEA924729AFF00F42EFC /* HomepageSettingsServiceRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomepageSettingsServiceRemote.swift; sourceTree = "<group>"; };
679686
17BF9A6B20C7DC3300BF57D2 /* reader-site-search-success.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "reader-site-search-success.json"; sourceTree = "<group>"; };
680687
17BF9A6D20C7E18100BF57D2 /* reader-site-search-success-hasmore.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "reader-site-search-success-hasmore.json"; sourceTree = "<group>"; };
@@ -1364,6 +1371,15 @@
13641371
/* End PBXFrameworksBuildPhase section */
13651372

13661373
/* Begin PBXGroup section */
1374+
0CB1905C2A2A5E7B004D3E80 /* Blaze */ = {
1375+
isa = PBXGroup;
1376+
children = (
1377+
0CB1905D2A2A5E83004D3E80 /* BlazeCampaign.swift */,
1378+
0CB190642A2A7569004D3E80 /* BlazeCampaignsSearchResponse.swift */,
1379+
);
1380+
name = Blaze;
1381+
sourceTree = "<group>";
1382+
};
13671383
3297E1DC2564649D00287D21 /* Scan */ = {
13681384
isa = PBXGroup;
13691385
children = (
@@ -1767,6 +1783,7 @@
17671783
children = (
17681784
FFE247CD20CB1245002DF3A2 /* LICENSE */,
17691785
FFE247CC20CB118A002DF3A2 /* README.md */,
1786+
0C3A2A412A2E7BA500FD91D6 /* CHANGELOG.md */,
17701787
FF20AD2120B8471A00082398 /* WordPressKit.podspec */,
17711788
9368C77D1EC5EF1B0092CE8E /* WordPressKit */,
17721789
9368C7881EC5EF1B0092CE8E /* WordPressKitTests */,
@@ -1929,6 +1946,7 @@
19291946
9368C79C1EC62EBD0092CE8E /* Models */ = {
19301947
isa = PBXGroup;
19311948
children = (
1949+
0CB1905C2A2A5E7B004D3E80 /* Blaze */,
19321950
7E3E7A4620E443100075D159 /* Extensions */,
19331951
E1EF5D5E1F9F3CA700B6D53E /* Plugins */,
19341952
9AF4F2FD2183345D00570E4B /* Revisions */,
@@ -2315,6 +2333,7 @@
23152333
C738CAF428622953001BE107 /* qrlogin-validate-expired-401.json */,
23162334
C738CAF628622B94001BE107 /* qrlogin-authenticate-200.json */,
23172335
C738CAF828622BB1001BE107 /* qrlogin-authenticate-failed-400.json */,
2336+
0CB1905F2A2A6943004D3E80 /* blaze-campaigns-search.json */,
23182337
);
23192338
path = "Mock Data";
23202339
sourceTree = "<group>";
@@ -2845,6 +2864,7 @@
28452864
7403A2F91EF06FEB00DED7DC /* me-settings-change-email-success.json in Resources */,
28462865
439A44DC2107CE3C00795ED7 /* site-plans-v3-empty-failure.json in Resources */,
28472866
9AB6D64F218731AB0008F274 /* post-revisions-failure.json in Resources */,
2867+
0CB190612A2A6A13004D3E80 /* blaze-campaigns-search.json in Resources */,
28482868
FA42615E2570C713003A01E2 /* activity-groups-success.json in Resources */,
28492869
984E34F422EF9465005C3F92 /* stats-file-downloads.json in Resources */,
28502870
93F50A3C1F226C0100B5BEBA /* WordPressComRestApiFailThrottled.json in Resources */,
@@ -3159,6 +3179,7 @@
31593179
74DA56351F06EAF000FE9BF4 /* MediaServiceRemoteXMLRPC.m in Sources */,
31603180
C797196C2679007B0072F984 /* PluginManagementClient.swift in Sources */,
31613181
C785325625B5F46C006CEAFB /* JetpackThreatFixStatus.swift in Sources */,
3182+
0CB190652A2A7569004D3E80 /* BlazeCampaignsSearchResponse.swift in Sources */,
31623183
93F50A381F226B9300B5BEBA /* WordPressComServiceRemote.m in Sources */,
31633184
9F4E52002088E38200424676 /* ObjectValidation.swift in Sources */,
31643185
7EC60EC022DC5D7C00FB0336 /* EditorSettings.swift in Sources */,
@@ -3245,6 +3266,7 @@
32453266
FEE4EF57272FDD4B003CDA3C /* RemoteCommentV2.swift in Sources */,
32463267
7430C9B21F1927C50051B8E6 /* RemoteReaderPost.m in Sources */,
32473268
74A44DCD1F13C533006CD8F4 /* PushAuthenticationServiceRemote.swift in Sources */,
3269+
0CB1905E2A2A5E83004D3E80 /* BlazeCampaign.swift in Sources */,
32483270
74B5F0D81EF8299B00B411E7 /* BlogServiceRemoteREST.m in Sources */,
32493271
9FCDD09720A5EF75004F0BF7 /* ReaderTopicServiceError.swift in Sources */,
32503272
74A44DD11F13C64B006CD8F4 /* RemoteNotificationSettings.swift in Sources */,

WordPressKit/BlazeCampaign.swift

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import Foundation
2+
3+
public final class BlazeCampaign: Decodable {
4+
public let campaignID: Int
5+
public let name: String?
6+
public let startDate: Date?
7+
public let endDate: Date?
8+
public let status: Status
9+
public let budgetCents: Int?
10+
public let targetURL: String?
11+
public let stats: Stats?
12+
public let contentConfig: ContentConfig?
13+
public let creativeHTML: String?
14+
15+
public init(campaignID: Int, name: String?, startDate: Date?, endDate: Date?, status: Status, budgetCents: Int?, targetURL: String?, stats: Stats?, contentConfig: ContentConfig?, creativeHTML: String?) {
16+
self.campaignID = campaignID
17+
self.name = name
18+
self.startDate = startDate
19+
self.endDate = endDate
20+
self.status = status
21+
self.budgetCents = budgetCents
22+
self.targetURL = targetURL
23+
self.stats = stats
24+
self.contentConfig = contentConfig
25+
self.creativeHTML = creativeHTML
26+
}
27+
28+
enum CodingKeys: String, CodingKey {
29+
case campaignID = "campaignId"
30+
case name
31+
case startDate
32+
case endDate
33+
case status
34+
case budgetCents
35+
case targetURL = "targetUrl"
36+
case contentConfig
37+
case stats = "campaignStats"
38+
case creativeHTML = "creativeHtml"
39+
}
40+
41+
public enum Status: String, Decodable {
42+
case scheduled
43+
case created
44+
case rejected
45+
case approved
46+
case active
47+
case canceled
48+
case finished
49+
case processing
50+
case unknown
51+
52+
public init(from decoder: Decoder) throws {
53+
let status = try? String(from: decoder)
54+
self = status.flatMap(Status.init) ?? .unknown
55+
}
56+
}
57+
58+
public struct Stats: Decodable {
59+
public let impressionsTotal: Int?
60+
public let clicksTotal: Int?
61+
62+
public init(impressionsTotal: Int?, clicksTotal: Int?) {
63+
self.impressionsTotal = impressionsTotal
64+
self.clicksTotal = clicksTotal
65+
}
66+
}
67+
68+
public struct ContentConfig: Decodable {
69+
public let title: String?
70+
public let snippet: String?
71+
public let clickURL: String?
72+
public let imageURL: String?
73+
74+
public init(title: String?, snippet: String?, clickURL: String?, imageURL: String?) {
75+
self.title = title
76+
self.snippet = snippet
77+
self.clickURL = clickURL
78+
self.imageURL = imageURL
79+
}
80+
81+
enum CodingKeys: String, CodingKey {
82+
case title
83+
case snippet
84+
case clickURL = "clickUrl"
85+
case imageURL = "imageUrl"
86+
}
87+
}
88+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import Foundation
2+
3+
public final class BlazeCampaignsSearchResponse: Decodable {
4+
public let campaigns: [BlazeCampaign]?
5+
public let totalItems: Int?
6+
public let totalPages: Int?
7+
public let page: Int?
8+
9+
public init(totalItems: Int?, campaigns: [BlazeCampaign]?, totalPages: Int?, page: Int?) {
10+
self.totalItems = totalItems
11+
self.campaigns = campaigns
12+
self.totalPages = totalPages
13+
self.page = page
14+
}
15+
}

WordPressKit/BlazeServiceRemote.swift

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,12 @@ open class BlazeServiceRemote: ServiceRemoteWordPressComREST {
1616
let path = self.path(forEndpoint: endpoint, withVersion: ._2_0)
1717

1818
wordPressComRestApi.GET(path, parameters: nil, success: { response, _ in
19-
2019
if let json = response as? [String: Any],
2120
let approved = json["approved"] as? Bool {
2221
callback(.success(approved))
2322
} else {
2423
callback(.failure(BlazeServiceRemoteError.InvalidDataError))
2524
}
26-
2725
}, failure: { error, response in
2826
WPKitLogError("Error retrieving blaze status")
2927
WPKitLogError("\(error)")
@@ -34,6 +32,32 @@ open class BlazeServiceRemote: ServiceRemoteWordPressComREST {
3432

3533
callback(.failure(error))
3634
})
35+
}
36+
37+
// MARK: - Campaigns
3738

39+
/// Searches the campaigns for the site with the given ID. The campaigns are returned ordered by the post date.
40+
///
41+
/// - parameters:
42+
/// - siteId: The site ID.
43+
/// - page: The response page. By default, returns the first page.
44+
open func searchCampaigns(forSiteId siteId: Int, page: Int = 1, callback: @escaping (Result<BlazeCampaignsSearchResponse, Error>) -> Void) {
45+
let endpoint = "sites/\(siteId)/wordads/dsp/api/v1/search/campaigns/site/\(siteId)"
46+
let path = self.path(forEndpoint: endpoint, withVersion: ._2_0)
47+
wordPressComRestApi.GET(path, parameters: [
48+
"page": "\(page)" as AnyObject
49+
], success: { response, _ in
50+
do {
51+
let data = try JSONSerialization.data(withJSONObject: response)
52+
let response = try JSONDecoder.apiDecoder.decode(BlazeCampaignsSearchResponse.self, from: data)
53+
callback(.success((response)))
54+
} catch {
55+
WPKitLogError("Error parsing campaigns response: \(error), \(response)")
56+
callback(.failure(error))
57+
}
58+
}, failure: { error, _ in
59+
WPKitLogError("Error retrieving blaze campaigns: ", error)
60+
callback(.failure(error))
61+
})
3862
}
3963
}

WordPressKitTests/BlazeServiceRemoteTests.swift

Lines changed: 86 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ final class BlazeServiceRemoteTests: RemoteTestCase, RESTTestable {
99

1010
// MARK: - Properties
1111

12-
var statusEndpoint: String { return "sites/\(siteId)/blaze/status" }
12+
var statusEndpoint: String { "sites/\(siteId)/blaze/status" }
13+
var searchEndpoint: String { "sites/\(siteId)/wordads/dsp/api/v1/search/campaigns/site/\(siteId)" }
1314
var service: BlazeServiceRemote!
1415

15-
// MARK: - Tests
16+
// MARK: - Get Status
1617

1718
func testGetStatusReturnsSuccess() throws {
1819
// Given
@@ -28,7 +29,6 @@ final class BlazeServiceRemoteTests: RemoteTestCase, RESTTestable {
2829
let approved = try! result.get()
2930
XCTAssertEqual(approved, true)
3031
expectation.fulfill()
31-
3232
}
3333

3434
wait(for: [expectation], timeout: 1)
@@ -45,8 +45,8 @@ final class BlazeServiceRemoteTests: RemoteTestCase, RESTTestable {
4545

4646
// Then
4747
switch result {
48-
case .success: XCTFail()
49-
case .failure: expectation.fulfill()
48+
case .success: XCTFail()
49+
case .failure: expectation.fulfill()
5050
}
5151

5252
}
@@ -77,4 +77,85 @@ final class BlazeServiceRemoteTests: RemoteTestCase, RESTTestable {
7777
encoder.outputFormatting = [.sortedKeys, .prettyPrinted]
7878
return try encoder.encode(object)
7979
}
80+
81+
// MARK: - Campaigns
82+
83+
func testGetCampaignsSuccess() throws {
84+
// Given
85+
let bundle = Bundle(for: BlazeServiceRemoteTests.self)
86+
let url = try XCTUnwrap(bundle.url(forResource: "blaze-campaigns-search", withExtension: "json"))
87+
stubRemoteResponse(searchEndpoint, data: try Data(contentsOf: url), contentType: .ApplicationJSON)
88+
89+
// When
90+
let result = try getSearchCampaignsResult()
91+
92+
// Then
93+
let response = try result.get()
94+
95+
XCTAssertEqual(response.totalItems, 1)
96+
XCTAssertEqual(response.totalPages, 1)
97+
XCTAssertEqual(response.page, 1)
98+
XCTAssertEqual(response.campaigns?.count, 1)
99+
100+
let campaign = try XCTUnwrap(response.campaigns?.first)
101+
XCTAssertEqual(campaign.campaignID, 26916)
102+
XCTAssertEqual(campaign.name, "Test Post - don't approve")
103+
XCTAssertEqual(campaign.startDate, ISO8601DateFormatter().date(from: "2023-06-13T00:00:00Z"))
104+
XCTAssertEqual(campaign.endDate, ISO8601DateFormatter().date(from: "2023-06-01T19:15:45Z"))
105+
XCTAssertEqual(campaign.status, .canceled)
106+
XCTAssertEqual(campaign.budgetCents, 500)
107+
XCTAssertEqual(campaign.targetURL, "https://alextest9123.wordpress.com/2023/06/01/test-post/")
108+
XCTAssertEqual(campaign.contentConfig?.title, "Test Post - don't approve")
109+
XCTAssertEqual(campaign.contentConfig?.snippet, "Test Post Empty Empty")
110+
XCTAssertEqual(campaign.contentConfig?.clickURL, "https://alextest9123.wordpress.com/2023/06/01/test-post/")
111+
XCTAssertEqual(campaign.contentConfig?.imageURL, "https://i0.wp.com/public-api.wordpress.com/wpcom/v2/wordads/dsp/api/v1/dsp/creatives/56259/image?w=600&zoom=2")
112+
113+
let stats = try XCTUnwrap(campaign.stats)
114+
XCTAssertEqual(stats.impressionsTotal, 1000)
115+
XCTAssertEqual(stats.clicksTotal, 235)
116+
}
117+
118+
func testGetCampaignsSuccessFailureInvalidJSON() throws {
119+
// Given
120+
let data = #"{ "campaigns": "XXXX" }"#.data(using: .utf8)!
121+
stubRemoteResponse(searchEndpoint, data: data, contentType: .ApplicationJSON)
122+
123+
// When
124+
let result = try getSearchCampaignsResult()
125+
126+
// Then
127+
switch result {
128+
case .success:
129+
XCTFail("Expected failure")
130+
case .failure:
131+
break // OK
132+
}
133+
}
134+
135+
func testGetCampaignsSuccessFailureUnauthorized() throws {
136+
// Given
137+
stubRemoteResponse(searchEndpoint, data: Data(), contentType: .NoContentType, status: 403)
138+
139+
// When
140+
let result = try getSearchCampaignsResult()
141+
142+
// Then
143+
switch result {
144+
case .success:
145+
XCTFail("Expected failure")
146+
case .failure:
147+
break // OK
148+
}
149+
}
150+
151+
private func getSearchCampaignsResult() throws -> Result<BlazeCampaignsSearchResponse, Error> {
152+
var result: Result<BlazeCampaignsSearchResponse, Error>?
153+
let expectation = self.expectation(description: "requestCompleted")
154+
BlazeServiceRemote(wordPressComRestApi: getRestApi()).searchCampaigns(forSiteId: siteId) {
155+
result = $0
156+
expectation.fulfill()
157+
}
158+
wait(for: [expectation], timeout: 1)
159+
return try XCTUnwrap(result)
160+
}
80161
}

0 commit comments

Comments
 (0)