@saltcorn/meta-marketing-api 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +144 -2
- package/api.js +534 -43
- package/index.js +43 -0
- package/package.json +1 -1
- package/tests/api.test.js +114 -0
- package/tests/media.test.js +291 -0
package/index.js
CHANGED
|
@@ -27,6 +27,10 @@ const {
|
|
|
27
27
|
getAdText,
|
|
28
28
|
getAdHeadline,
|
|
29
29
|
getAdBody,
|
|
30
|
+
getAdMedia,
|
|
31
|
+
getAdMediaType,
|
|
32
|
+
getAdMediaUrl,
|
|
33
|
+
getPageAccessToken,
|
|
30
34
|
getInsights,
|
|
31
35
|
getInsightsAsync,
|
|
32
36
|
exchangeLongLivedToken,
|
|
@@ -413,6 +417,45 @@ module.exports = {
|
|
|
413
417
|
"Get the headline and the primary text of an ad together, as { headline, body }, in one read",
|
|
414
418
|
arguments: [{ name: "ad", type: "String" }],
|
|
415
419
|
},
|
|
420
|
+
get_meta_ad_media: {
|
|
421
|
+
async run(ad, opts, cfgOverRide) {
|
|
422
|
+
return await getAdMedia(ad, { ...cfg, ...cfgOverRide }, opts || {});
|
|
423
|
+
},
|
|
424
|
+
isAsync: true,
|
|
425
|
+
description:
|
|
426
|
+
"Get what an ad is made of and where to download it: { type, carousel, media }, where type is image, video, mixed or unknown and each entry in media has a url. Takes an ad id or an ad row that already has its creative. Set resolve_urls false in the options to only work out the type, without reading the address of every file.",
|
|
427
|
+
arguments: [
|
|
428
|
+
{ name: "ad", type: "String" },
|
|
429
|
+
{ name: "opts", type: "JSON" },
|
|
430
|
+
],
|
|
431
|
+
},
|
|
432
|
+
get_meta_ad_media_type: {
|
|
433
|
+
async run(ad, cfgOverRide) {
|
|
434
|
+
return await getAdMediaType(ad, { ...cfg, ...cfgOverRide });
|
|
435
|
+
},
|
|
436
|
+
isAsync: true,
|
|
437
|
+
description:
|
|
438
|
+
"Whether an ad is an image, a video, mixed or unknown. Takes an ad id or an ad row that already has its creative.",
|
|
439
|
+
arguments: [{ name: "ad", type: "String" }],
|
|
440
|
+
},
|
|
441
|
+
get_meta_ad_media_url: {
|
|
442
|
+
async run(ad, cfgOverRide) {
|
|
443
|
+
return await getAdMediaUrl(ad, { ...cfg, ...cfgOverRide });
|
|
444
|
+
},
|
|
445
|
+
isAsync: true,
|
|
446
|
+
description:
|
|
447
|
+
"The address of the picture or the film in an ad, ready to download. Video addresses are signed and short lived, so download the file straight away.",
|
|
448
|
+
arguments: [{ name: "ad", type: "String" }],
|
|
449
|
+
},
|
|
450
|
+
get_meta_page_access_token: {
|
|
451
|
+
async run(pageId, cfgOverRide) {
|
|
452
|
+
return await getPageAccessToken(pageId, { ...cfg, ...cfgOverRide });
|
|
453
|
+
},
|
|
454
|
+
isAsync: true,
|
|
455
|
+
description:
|
|
456
|
+
"A token for one of your pages, or nothing when your access token has no say over that page. Use it to check why an ad that boosts a page post shows nothing.",
|
|
457
|
+
arguments: [{ name: "pageId", type: "String" }],
|
|
458
|
+
},
|
|
416
459
|
get_meta_ad_preview: {
|
|
417
460
|
async run(adId, adFormat, cfgOverRide) {
|
|
418
461
|
return await getAdPreview(adId, adFormat, {
|
package/package.json
CHANGED
package/tests/api.test.js
CHANGED
|
@@ -8,6 +8,8 @@ const {
|
|
|
8
8
|
DEFAULT_FIELDS,
|
|
9
9
|
creativeHeadlines,
|
|
10
10
|
creativeBodies,
|
|
11
|
+
creativeMedia,
|
|
12
|
+
mediaType,
|
|
11
13
|
} = require("../api");
|
|
12
14
|
|
|
13
15
|
describe("query string encoding", () => {
|
|
@@ -201,3 +203,115 @@ describe("default fields", () => {
|
|
|
201
203
|
expect(DEFAULT_FIELDS.adcreative).toContain("body");
|
|
202
204
|
});
|
|
203
205
|
});
|
|
206
|
+
|
|
207
|
+
describe("finding the media on a creative", () => {
|
|
208
|
+
it("reads the film of a video ad, with its still as the thumbnail", () => {
|
|
209
|
+
const media = creativeMedia({
|
|
210
|
+
video_id: "77",
|
|
211
|
+
object_story_spec: {
|
|
212
|
+
video_data: {
|
|
213
|
+
video_id: "77",
|
|
214
|
+
title: "Watch",
|
|
215
|
+
image_url: "https://example.com/still.jpg",
|
|
216
|
+
image_hash: "abc",
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
expect(media).toEqual([
|
|
221
|
+
{
|
|
222
|
+
kind: "video",
|
|
223
|
+
video_id: "77",
|
|
224
|
+
thumbnail_url: "https://example.com/still.jpg",
|
|
225
|
+
thumbnail_hash: "abc",
|
|
226
|
+
name: "Watch",
|
|
227
|
+
},
|
|
228
|
+
]);
|
|
229
|
+
// the still of a film is not a picture of its own
|
|
230
|
+
expect(mediaType(media)).toBe("video");
|
|
231
|
+
});
|
|
232
|
+
it("reads the picture of a link ad", () => {
|
|
233
|
+
const media = creativeMedia({
|
|
234
|
+
object_story_spec: {
|
|
235
|
+
link_data: {
|
|
236
|
+
name: "Half price",
|
|
237
|
+
image_hash: "hash1",
|
|
238
|
+
picture: "https://example.com/pic.jpg",
|
|
239
|
+
link: "https://example.com",
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
expect(media).toEqual([
|
|
244
|
+
{
|
|
245
|
+
kind: "image",
|
|
246
|
+
url: "https://example.com/pic.jpg",
|
|
247
|
+
image_hash: "hash1",
|
|
248
|
+
name: "Half price",
|
|
249
|
+
link: "https://example.com",
|
|
250
|
+
},
|
|
251
|
+
]);
|
|
252
|
+
expect(mediaType(media)).toBe("image");
|
|
253
|
+
});
|
|
254
|
+
it("reads a photo ad", () => {
|
|
255
|
+
expect(
|
|
256
|
+
creativeMedia({
|
|
257
|
+
object_story_spec: {
|
|
258
|
+
photo_data: { url: "https://example.com/photo.jpg", image_hash: "h" },
|
|
259
|
+
},
|
|
260
|
+
})
|
|
261
|
+
).toEqual([
|
|
262
|
+
{ kind: "image", url: "https://example.com/photo.jpg", image_hash: "h" },
|
|
263
|
+
]);
|
|
264
|
+
});
|
|
265
|
+
it("returns one entry per carousel card, in order", () => {
|
|
266
|
+
const media = creativeMedia({
|
|
267
|
+
object_story_spec: {
|
|
268
|
+
link_data: {
|
|
269
|
+
image_hash: "cover",
|
|
270
|
+
child_attachments: [
|
|
271
|
+
{ image_hash: "h1", name: "Shoes" },
|
|
272
|
+
{ video_id: "v2", name: "Hats", picture: "https://x/2.jpg" },
|
|
273
|
+
],
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
expect(media.map((m) => m.kind)).toEqual(["image", "video"]);
|
|
278
|
+
expect(media.map((m) => m.name)).toEqual(["Shoes", "Hats"]);
|
|
279
|
+
// a carousel with a film in it counts as a mixed ad
|
|
280
|
+
expect(mediaType(media)).toBe("mixed");
|
|
281
|
+
});
|
|
282
|
+
it("reads the assets of a dynamic creative, without repeats", () => {
|
|
283
|
+
const media = creativeMedia({
|
|
284
|
+
image_url: "https://example.com/a.jpg",
|
|
285
|
+
asset_feed_spec: {
|
|
286
|
+
images: [{ hash: "h1", url: "https://example.com/a.jpg" }],
|
|
287
|
+
videos: [{ video_id: "v1", thumbnail_url: "https://example.com/t.jpg" }],
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
expect(media).toHaveLength(2);
|
|
291
|
+
expect(mediaType(media)).toBe("mixed");
|
|
292
|
+
});
|
|
293
|
+
it("does not repeat the same picture reached by two routes", () => {
|
|
294
|
+
const media = creativeMedia({
|
|
295
|
+
image_hash: "h1",
|
|
296
|
+
image_url: "https://example.com/a.jpg",
|
|
297
|
+
object_story_spec: {
|
|
298
|
+
link_data: { image_hash: "h1", picture: "https://example.com/a.jpg" },
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
expect(media).toHaveLength(1);
|
|
302
|
+
});
|
|
303
|
+
it("comes back empty for an ad that promotes an existing post", () => {
|
|
304
|
+
expect(creativeMedia({ effective_object_story_id: "1_2" })).toEqual([]);
|
|
305
|
+
expect(creativeMedia(null)).toEqual([]);
|
|
306
|
+
expect(mediaType([])).toBe("unknown");
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
describe("default fields", () => {
|
|
311
|
+
it("asks what kind of ad each ad is", () => {
|
|
312
|
+
const creative = DEFAULT_FIELDS.ad.find((f) => f.startsWith("creative{"));
|
|
313
|
+
expect(creative).toContain("object_type");
|
|
314
|
+
expect(creative).toContain("video_id");
|
|
315
|
+
expect(creative).toContain("image_url");
|
|
316
|
+
});
|
|
317
|
+
});
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
jest.mock("node-fetch");
|
|
2
|
+
const fetch = require("node-fetch");
|
|
3
|
+
const { getAdMedia, getAdMediaType, getAdText } = require("../api");
|
|
4
|
+
|
|
5
|
+
const cfg = { access_token: "USERTOKEN", max_retries: 0 };
|
|
6
|
+
|
|
7
|
+
// Answer each call from a list of [what the address must contain, the reply],
|
|
8
|
+
// and keep what was asked so a test can check which token was used
|
|
9
|
+
let calls;
|
|
10
|
+
const serve = (routes) => {
|
|
11
|
+
calls = [];
|
|
12
|
+
fetch.mockImplementation(async (url, opts) => {
|
|
13
|
+
calls.push({ url, auth: opts?.headers?.Authorization });
|
|
14
|
+
const hit = routes.find(([match]) => url.includes(match));
|
|
15
|
+
return {
|
|
16
|
+
status: 200,
|
|
17
|
+
headers: { get: () => null },
|
|
18
|
+
text: async () => JSON.stringify(hit ? hit[1] : { error: { code: 100, message: `no route for ${url}` } }),
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// An ad that boosts a post already on the page: the creative names the page
|
|
24
|
+
// and the post, and carries no media of its own
|
|
25
|
+
const boostedAd = {
|
|
26
|
+
id: "120242825926110547",
|
|
27
|
+
name: "Version AI Remake",
|
|
28
|
+
creative: {
|
|
29
|
+
id: "782942930901202",
|
|
30
|
+
object_type: "SHARE",
|
|
31
|
+
object_story_spec: { page_id: "106755536029753", instagram_user_id: "17841401937168196" },
|
|
32
|
+
effective_object_story_id: "106755536029753_1333493868811467",
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const videoPost = {
|
|
37
|
+
permalink_url: "https://facebook.com/post",
|
|
38
|
+
full_picture: "https://example.com/full.jpg",
|
|
39
|
+
attachments: {
|
|
40
|
+
data: [
|
|
41
|
+
{
|
|
42
|
+
media_type: "video",
|
|
43
|
+
media: { image: { src: "https://example.com/still.jpg" } },
|
|
44
|
+
target: { id: "999" },
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
describe("an ad that boosts a page post", () => {
|
|
51
|
+
it("reads the post with a token for the page", async () => {
|
|
52
|
+
serve([
|
|
53
|
+
["/120242825926110547", { account_id: "1", creative: { ...boostedAd.creative, thumbnail_url: "https://example.com/thumb.jpg" } }],
|
|
54
|
+
["/106755536029753?", { access_token: "PAGETOKEN" }],
|
|
55
|
+
["/106755536029753_1333493868811467", videoPost],
|
|
56
|
+
["/999", { id: "999", source: "https://video.fb/film.mp4", picture: "https://example.com/still.jpg", length: 15 }],
|
|
57
|
+
]);
|
|
58
|
+
const res = await getAdMedia(boostedAd, cfg);
|
|
59
|
+
expect(res.type).toBe("video");
|
|
60
|
+
expect(res.from_post).toBe(true);
|
|
61
|
+
expect(res.story_id).toBe("106755536029753_1333493868811467");
|
|
62
|
+
expect(res.media).toHaveLength(1);
|
|
63
|
+
expect(res.media[0].url).toBe("https://video.fb/film.mp4");
|
|
64
|
+
expect(res.media[0].thumbnail_url).toBe("https://example.com/still.jpg");
|
|
65
|
+
expect(res.error).toBeUndefined();
|
|
66
|
+
// the post is read as the page, not as the user
|
|
67
|
+
const post = calls.find((c) => c.url.includes("106755536029753_1333"));
|
|
68
|
+
expect(post.auth).toBe("Bearer PAGETOKEN");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("says why when the token has no say over the page", async () => {
|
|
72
|
+
const ad = {
|
|
73
|
+
...boostedAd,
|
|
74
|
+
creative: {
|
|
75
|
+
...boostedAd.creative,
|
|
76
|
+
object_story_spec: { page_id: "222" },
|
|
77
|
+
effective_object_story_id: "222_333",
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
serve([
|
|
81
|
+
["/120242825926110547", { account_id: "1", creative: { ...ad.creative, thumbnail_url: "https://example.com/thumb.jpg" } }],
|
|
82
|
+
["/222?", { error: { code: 190, message: "no page access" } }],
|
|
83
|
+
["/222_333", { error: { code: 100, message: "Unsupported get request" } }],
|
|
84
|
+
]);
|
|
85
|
+
const res = await getAdMedia(ad, cfg);
|
|
86
|
+
expect(res.type).toBe("unknown");
|
|
87
|
+
expect(res.media).toEqual([]);
|
|
88
|
+
// not silence: the reason, and something to look at
|
|
89
|
+
expect(res.error).toMatch(/could not read the post 222_333/);
|
|
90
|
+
expect(res.error).toMatch(/pages_read_engagement/);
|
|
91
|
+
expect(res.thumbnail_url).toBe("https://example.com/thumb.jpg");
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("reading the ad again", () => {
|
|
96
|
+
it("asks for every media field when handed a creative with none", async () => {
|
|
97
|
+
serve([
|
|
98
|
+
["/120242825926110547", { account_id: "1", creative: { id: "782942930901202", video_id: "555", thumbnail_url: "https://example.com/t.jpg" } }],
|
|
99
|
+
["/555", { id: "555", source: "https://video.fb/film.mp4" }],
|
|
100
|
+
]);
|
|
101
|
+
// the caller asked Meta for a narrow set of fields, so image_hash,
|
|
102
|
+
// video_id and asset_feed_spec were never in the ad we were given
|
|
103
|
+
const res = await getAdMedia(boostedAd, cfg);
|
|
104
|
+
expect(res.type).toBe("video");
|
|
105
|
+
expect(res.media[0].url).toBe("https://video.fb/film.mp4");
|
|
106
|
+
const asked = calls[0].url;
|
|
107
|
+
expect(asked).toContain("video_id");
|
|
108
|
+
expect(asked).toContain("asset_feed_spec");
|
|
109
|
+
expect(asked).toContain("image_hash");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("does not read the ad again when the creative already has media", async () => {
|
|
113
|
+
serve([["/act_", { data: [] }]]);
|
|
114
|
+
const ad = {
|
|
115
|
+
id: "1",
|
|
116
|
+
account_id: "9",
|
|
117
|
+
creative: {
|
|
118
|
+
id: "2",
|
|
119
|
+
object_story_spec: {
|
|
120
|
+
link_data: { image_hash: "h", picture: "https://example.com/p.jpg" },
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
const res = await getAdMedia(ad, cfg, { resolve_urls: false });
|
|
125
|
+
expect(res.type).toBe("image");
|
|
126
|
+
expect(calls).toHaveLength(0);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("works out the type without looking up every address", async () => {
|
|
130
|
+
serve([
|
|
131
|
+
["/1?", { account_id: "9", creative: { id: "2", video_id: "555" } }],
|
|
132
|
+
]);
|
|
133
|
+
expect(await getAdMediaType({ id: "1" }, cfg)).toBe("video");
|
|
134
|
+
// one read for the ad, and no read of the video
|
|
135
|
+
expect(calls).toHaveLength(1);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("what counts as a carousel", () => {
|
|
140
|
+
it("a post with several pictures in it does", async () => {
|
|
141
|
+
serve([
|
|
142
|
+
["/1?", { account_id: "9", creative: { id: "2", object_story_spec: { page_id: "444" }, effective_object_story_id: "444_555" } }],
|
|
143
|
+
["/444?", { access_token: "PAGETOKEN" }],
|
|
144
|
+
["/444_555", {
|
|
145
|
+
attachments: {
|
|
146
|
+
data: [
|
|
147
|
+
{
|
|
148
|
+
media_type: "album",
|
|
149
|
+
subattachments: {
|
|
150
|
+
data: [
|
|
151
|
+
{ media_type: "photo", media: { image: { src: "https://example.com/1.jpg" } } },
|
|
152
|
+
{ media_type: "photo", media: { image: { src: "https://example.com/2.jpg" } } },
|
|
153
|
+
],
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
},
|
|
158
|
+
}],
|
|
159
|
+
]);
|
|
160
|
+
const res = await getAdMedia({ id: "1" }, cfg);
|
|
161
|
+
expect(res.type).toBe("image");
|
|
162
|
+
expect(res.carousel).toBe(true);
|
|
163
|
+
expect(res.media).toHaveLength(2);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("a dynamic creative offering Meta a choice does not", async () => {
|
|
167
|
+
serve([["/act_", { data: [] }]]);
|
|
168
|
+
const ad = {
|
|
169
|
+
id: "1",
|
|
170
|
+
account_id: "9",
|
|
171
|
+
creative: {
|
|
172
|
+
id: "2",
|
|
173
|
+
asset_feed_spec: {
|
|
174
|
+
images: [
|
|
175
|
+
{ hash: "h1", url: "https://example.com/1.jpg" },
|
|
176
|
+
{ hash: "h2", url: "https://example.com/2.jpg" },
|
|
177
|
+
],
|
|
178
|
+
videos: [{ video_id: "v1" }],
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
const res = await getAdMedia(ad, cfg, { resolve_urls: false });
|
|
183
|
+
expect(res.carousel).toBe(false);
|
|
184
|
+
expect(res.type).toBe("mixed");
|
|
185
|
+
expect(res.media).toHaveLength(3);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe("the wording of an ad that boosts a post", () => {
|
|
190
|
+
it("reads the post with a token for the page", async () => {
|
|
191
|
+
serve([
|
|
192
|
+
["/7?", { creative: { id: "2", object_story_spec: { page_id: "888" }, effective_object_story_id: "888_999" } }],
|
|
193
|
+
["/888?", { access_token: "PAGETOKEN" }],
|
|
194
|
+
["/888_999", { message: "The primary text", attachments: { data: [{ title: "The headline" }] } }],
|
|
195
|
+
]);
|
|
196
|
+
expect(await getAdText("7", cfg)).toEqual({
|
|
197
|
+
headline: "The headline",
|
|
198
|
+
body: "The primary text",
|
|
199
|
+
});
|
|
200
|
+
const post = calls.find((c) => c.url.includes("888_999"));
|
|
201
|
+
expect(post.auth).toBe("Bearer PAGETOKEN");
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// A flexible creative names its pictures by hash only. The address of each
|
|
206
|
+
// one is held by the ad account's picture library.
|
|
207
|
+
const hashAd = {
|
|
208
|
+
id: "120254266274370547",
|
|
209
|
+
name: "3_STJ16890_FB_Dental",
|
|
210
|
+
creative: {
|
|
211
|
+
id: "1700780627695097",
|
|
212
|
+
object_type: "SHARE",
|
|
213
|
+
object_story_spec: { page_id: "106755536029753" },
|
|
214
|
+
effective_object_story_id: "106755536029753_1515321970628655",
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const hashCreative = {
|
|
219
|
+
...hashAd.creative,
|
|
220
|
+
asset_feed_spec: {
|
|
221
|
+
images: [{ hash: "aaa" }, { hash: "bbb" }],
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
describe("pictures named by hash", () => {
|
|
226
|
+
it("asks the picture library as a list, which is how Meta wants hashes", async () => {
|
|
227
|
+
serve([
|
|
228
|
+
["/120254266274370547", { account_id: "998", creative: hashCreative }],
|
|
229
|
+
["/adimages", {
|
|
230
|
+
data: [
|
|
231
|
+
{ hash: "aaa", url: "https://example.com/a.jpg", width: 1080, height: 1080 },
|
|
232
|
+
{ hash: "bbb", url: "https://example.com/b.jpg", width: 1080, height: 1080 },
|
|
233
|
+
],
|
|
234
|
+
}],
|
|
235
|
+
]);
|
|
236
|
+
const res = await getAdMedia(hashAd, cfg);
|
|
237
|
+
expect(res.type).toBe("image");
|
|
238
|
+
expect(res.media.map((m) => m.url)).toEqual([
|
|
239
|
+
"https://example.com/a.jpg",
|
|
240
|
+
"https://example.com/b.jpg",
|
|
241
|
+
]);
|
|
242
|
+
expect(res.media[0].width).toBe(1080);
|
|
243
|
+
expect(res.error).toBeUndefined();
|
|
244
|
+
const lookup = calls.find((c) => c.url.includes("adimages"));
|
|
245
|
+
expect(lookup.url).toContain("act_998");
|
|
246
|
+
// a JSON list, not the comma separated kind Meta takes for fields
|
|
247
|
+
expect(decodeURIComponent(lookup.url)).toContain('hashes=["aaa","bbb"]');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("falls back to the post when the ad account does not hold them", async () => {
|
|
251
|
+
serve([
|
|
252
|
+
["/120254266274370547", { account_id: "998", creative: hashCreative }],
|
|
253
|
+
["/adimages", { data: [] }],
|
|
254
|
+
["/106755536029753?", { access_token: "PAGETOKEN" }],
|
|
255
|
+
["/106755536029753_1515321970628655", {
|
|
256
|
+
attachments: {
|
|
257
|
+
data: [{ media_type: "photo", media: { image: { src: "https://example.com/post.jpg" } } }],
|
|
258
|
+
},
|
|
259
|
+
}],
|
|
260
|
+
]);
|
|
261
|
+
const res = await getAdMedia(hashAd, cfg);
|
|
262
|
+
expect(res.type).toBe("image");
|
|
263
|
+
expect(res.from_post).toBe(true);
|
|
264
|
+
expect(res.media).toEqual([{ kind: "image", url: "https://example.com/post.jpg" }]);
|
|
265
|
+
expect(res.error).toBeUndefined();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("says why when neither the library nor the post has them", async () => {
|
|
269
|
+
serve([
|
|
270
|
+
["/120254266274370547", { account_id: "998", creative: hashCreative }],
|
|
271
|
+
["/adimages", { error: { code: 100, message: "Invalid hashes" } }],
|
|
272
|
+
["/106755536029753?", { error: { code: 190, message: "no page access" } }],
|
|
273
|
+
["/106755536029753_1515321970628655", { error: { code: 100, message: "Unsupported get request" } }],
|
|
274
|
+
]);
|
|
275
|
+
const res = await getAdMedia(hashAd, cfg);
|
|
276
|
+
expect(res.type).toBe("image");
|
|
277
|
+
expect(res.media.every((m) => !m.url)).toBe(true);
|
|
278
|
+
expect(res.error).toMatch(/2 of 2 could not be reached/);
|
|
279
|
+
expect(res.error).toMatch(/could not read the pictures of ad account act_998/);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("says so when there is no ad account to ask", async () => {
|
|
283
|
+
serve([]);
|
|
284
|
+
const ad = {
|
|
285
|
+
id: "1",
|
|
286
|
+
creative: { id: "2", asset_feed_spec: { images: [{ hash: "aaa" }] } },
|
|
287
|
+
};
|
|
288
|
+
const res = await getAdMedia(ad, {});
|
|
289
|
+
expect(res.error).toMatch(/held by the ad account/);
|
|
290
|
+
});
|
|
291
|
+
});
|