Skip to content

Commit a499f6b

Browse files
committed
Fixed import emails not sending after members/db import with integration auth
no issue - the syntax used for fetching owner email as a fallback when `frame.user` is missing (as is the case with integration auth) was incorrectly calling `.get('email')` immediately on the Promise returned by `.getOwnerUser()` rather than waiting for that promise to resolve, this meant the email send was erroring due to a missing `to` parameter - fixed all instances of incorrect await syntax when using `User.getOwnerUser()` and added tests to confirm expected behaviour
1 parent 1f28fc1 commit a499f6b

File tree

7 files changed

+411
-3
lines changed

7 files changed

+411
-3
lines changed

ghost/core/core/server/api/endpoints/db.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ const controller = {
9696
if (frame.user) {
9797
email = frame.user.get('email');
9898
} else {
99-
email = await models.User.getOwnerUser().get('email');
99+
email = (await models.User.getOwnerUser()).get('email');
100100
}
101101

102102
return importer.importFromFile(frame.file, {

ghost/core/core/server/api/endpoints/members.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,7 @@ const controller = {
428428
if (frame.user) {
429429
email = frame.user.get('email');
430430
} else {
431-
email = await models.User.getOwnerUser().get('email');
431+
email = (await models.User.getOwnerUser()).get('email');
432432
}
433433

434434
return membersService.processImport({

ghost/core/core/server/models/post.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -914,7 +914,7 @@ Post = ghostBookshelf.Model.extend({
914914
let authorId = await this.contextUser(options);
915915
const authorExists = await ghostBookshelf.model('User').findOne({id: authorId}, {transacting: options.transacting});
916916
if (!authorExists) {
917-
authorId = await ghostBookshelf.model('User').getOwnerUser().get('id');
917+
authorId = (await ghostBookshelf.model('User').getOwnerUser()).get('id');
918918
}
919919
ops.push(async function updateRevisions() {
920920
const revisionModels = await ghostBookshelf.model('PostRevision')

ghost/core/test/e2e-api/admin/__snapshots__/posts.test.js.snap

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2545,3 +2545,189 @@ Object {
25452545
"x-powered-by": "Express",
25462546
}
25472547
`;
2548+
2549+
exports[`Posts API With integration auth can create and update a post with revisions 1: [body] 1`] = `
2550+
Object {
2551+
"posts": Array [
2552+
Object {
2553+
"authors": Any<Array>,
2554+
"canonical_url": null,
2555+
"codeinjection_foot": null,
2556+
"codeinjection_head": null,
2557+
"comment_id": Any<String>,
2558+
"count": Object {
2559+
"clicks": 0,
2560+
"negative_feedback": 0,
2561+
"positive_feedback": 0,
2562+
},
2563+
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
2564+
"custom_excerpt": null,
2565+
"custom_template": null,
2566+
"email": null,
2567+
"email_only": false,
2568+
"email_segment": "all",
2569+
"email_subject": null,
2570+
"excerpt": "Updated content for revision testing.",
2571+
"feature_image": null,
2572+
"feature_image_alt": null,
2573+
"feature_image_caption": null,
2574+
"featured": false,
2575+
"frontmatter": null,
2576+
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
2577+
"lexical": "{\\"root\\":{\\"children\\":[{\\"children\\":[{\\"detail\\":0,\\"format\\":0,\\"mode\\":\\"normal\\",\\"style\\":\\"\\",\\"text\\":\\"Updated content for revision testing.\\",\\"type\\":\\"text\\",\\"version\\":1}],\\"direction\\":\\"ltr\\",\\"format\\":\\"\\",\\"indent\\":0,\\"type\\":\\"paragraph\\",\\"version\\":1}],\\"direction\\":\\"ltr\\",\\"format\\":\\"\\",\\"indent\\":0,\\"type\\":\\"root\\",\\"version\\":1}}",
2578+
"meta_description": null,
2579+
"meta_title": null,
2580+
"newsletter": null,
2581+
"og_description": null,
2582+
"og_image": null,
2583+
"og_title": null,
2584+
"primary_author": Any<Object>,
2585+
"primary_tag": Any<Object>,
2586+
"published_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
2587+
"reading_time": 0,
2588+
"slug": "integration-auth-test-post",
2589+
"status": "published",
2590+
"tags": Any<Array>,
2591+
"tiers": Array [
2592+
Object {
2593+
"active": true,
2594+
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
2595+
"currency": null,
2596+
"description": null,
2597+
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
2598+
"monthly_price": null,
2599+
"monthly_price_id": null,
2600+
"name": "Free",
2601+
"slug": "free",
2602+
"trial_days": 0,
2603+
"type": "free",
2604+
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
2605+
"visibility": "public",
2606+
"welcome_page_url": null,
2607+
"yearly_price": null,
2608+
"yearly_price_id": null,
2609+
},
2610+
Object {
2611+
"active": true,
2612+
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
2613+
"currency": "usd",
2614+
"description": null,
2615+
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
2616+
"monthly_price": 500,
2617+
"monthly_price_id": null,
2618+
"name": "Default Product",
2619+
"slug": "default-product",
2620+
"trial_days": 0,
2621+
"type": "paid",
2622+
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
2623+
"visibility": "public",
2624+
"welcome_page_url": null,
2625+
"yearly_price": 5000,
2626+
"yearly_price_id": null,
2627+
},
2628+
],
2629+
"title": "Integration Auth Test Post",
2630+
"twitter_description": null,
2631+
"twitter_image": null,
2632+
"twitter_title": null,
2633+
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
2634+
"url": Any<String>,
2635+
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
2636+
"visibility": "public",
2637+
},
2638+
],
2639+
}
2640+
`;
2641+
2642+
exports[`Posts API can create and update a post with revisions using integration auth 1: [body] 1`] = `
2643+
Object {
2644+
"posts": Array [
2645+
Object {
2646+
"authors": Any<Array>,
2647+
"canonical_url": null,
2648+
"codeinjection_foot": null,
2649+
"codeinjection_head": null,
2650+
"comment_id": Any<String>,
2651+
"count": Object {
2652+
"clicks": 0,
2653+
"negative_feedback": 0,
2654+
"positive_feedback": 0,
2655+
},
2656+
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
2657+
"custom_excerpt": null,
2658+
"custom_template": null,
2659+
"email": null,
2660+
"email_only": false,
2661+
"email_segment": "all",
2662+
"email_subject": null,
2663+
"excerpt": "Updated content for revision testing.",
2664+
"feature_image": null,
2665+
"feature_image_alt": null,
2666+
"feature_image_caption": null,
2667+
"featured": false,
2668+
"frontmatter": null,
2669+
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
2670+
"lexical": "{\\"root\\":{\\"children\\":[{\\"children\\":[{\\"detail\\":0,\\"format\\":0,\\"mode\\":\\"normal\\",\\"style\\":\\"\\",\\"text\\":\\"Updated content for revision testing.\\",\\"type\\":\\"text\\",\\"version\\":1}],\\"direction\\":\\"ltr\\",\\"format\\":\\"\\",\\"indent\\":0,\\"type\\":\\"paragraph\\",\\"version\\":1}],\\"direction\\":\\"ltr\\",\\"format\\":\\"\\",\\"indent\\":0,\\"type\\":\\"root\\",\\"version\\":1}}",
2671+
"meta_description": null,
2672+
"meta_title": null,
2673+
"newsletter": null,
2674+
"og_description": null,
2675+
"og_image": null,
2676+
"og_title": null,
2677+
"primary_author": Any<Object>,
2678+
"primary_tag": Any<Object>,
2679+
"published_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
2680+
"reading_time": 0,
2681+
"slug": "integration-auth-test-post",
2682+
"status": "published",
2683+
"tags": Any<Array>,
2684+
"tiers": Array [
2685+
Object {
2686+
"active": true,
2687+
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
2688+
"currency": null,
2689+
"description": null,
2690+
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
2691+
"monthly_price": null,
2692+
"monthly_price_id": null,
2693+
"name": "Free",
2694+
"slug": "free",
2695+
"trial_days": 0,
2696+
"type": "free",
2697+
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
2698+
"visibility": "public",
2699+
"welcome_page_url": null,
2700+
"yearly_price": null,
2701+
"yearly_price_id": null,
2702+
},
2703+
Object {
2704+
"active": true,
2705+
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
2706+
"currency": "usd",
2707+
"description": null,
2708+
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
2709+
"monthly_price": 500,
2710+
"monthly_price_id": null,
2711+
"name": "Default Product",
2712+
"slug": "default-product",
2713+
"trial_days": 0,
2714+
"type": "paid",
2715+
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
2716+
"visibility": "public",
2717+
"welcome_page_url": null,
2718+
"yearly_price": 5000,
2719+
"yearly_price_id": null,
2720+
},
2721+
],
2722+
"title": "Integration Auth Test Post",
2723+
"twitter_description": null,
2724+
"twitter_image": null,
2725+
"twitter_title": null,
2726+
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
2727+
"url": Any<String>,
2728+
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
2729+
"visibility": "public",
2730+
},
2731+
],
2732+
}
2733+
`;

ghost/core/test/e2e-api/admin/posts.test.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -664,4 +664,73 @@ describe('Posts API', function () {
664664
});
665665
});
666666
});
667+
668+
describe('With integration auth', function () {
669+
it('can create and update a post with revisions', async function () {
670+
// Use Zapier integration to test integration auth scenario
671+
await agent.useZapierAdminAPIKey();
672+
673+
const lexical = createLexical('This is content for revision testing.');
674+
const postData = {
675+
title: 'Integration Auth Test Post',
676+
status: 'published',
677+
lexical: lexical,
678+
mobiledoc: null
679+
};
680+
681+
// Create post using integration auth - this should trigger the revision creation
682+
// with author fallback to owner user when contextUser returns integration context
683+
const {body} = await agent
684+
.post('/posts/?formats=lexical')
685+
.body({posts: [postData]})
686+
.expectStatus(201);
687+
688+
const [postResponse] = body.posts;
689+
postResponse.title.should.equal('Integration Auth Test Post');
690+
postResponse.status.should.equal('published');
691+
postResponse.lexical.should.equal(lexical);
692+
693+
// Verify the post revision was created with owner user as author
694+
const ownerUser = await models.User.getOwnerUser();
695+
const postRevisions = await models.PostRevision
696+
.where('post_id', postResponse.id)
697+
.fetchAll();
698+
699+
postRevisions.length.should.equal(1);
700+
const revision = postRevisions.at(0);
701+
revision.get('lexical').should.equal(lexical);
702+
revision.get('author_id').should.equal(ownerUser.get('id'));
703+
704+
// Update the post to ensure revision creation works properly
705+
const updatedLexical = createLexical('Updated content for revision testing.');
706+
await agent
707+
.put(`/posts/${postResponse.id}/?formats=lexical&save_revision=true`)
708+
.body({posts: [{
709+
...postResponse,
710+
lexical: updatedLexical
711+
}]})
712+
.expectStatus(200);
713+
714+
// Verify updated revision also has owner user as author
715+
const updatedRevisions = await models.PostRevision
716+
.where('post_id', postResponse.id)
717+
.orderBy('created_at_ts', 'desc')
718+
.fetchAll();
719+
720+
updatedRevisions.length.should.equal(2);
721+
const latestRevision = updatedRevisions.at(0);
722+
latestRevision.get('lexical').should.equal(updatedLexical);
723+
latestRevision.get('author_id').should.equal(ownerUser.get('id'));
724+
725+
// Verify the post was updated successfully
726+
await agent
727+
.get(`/posts/${postResponse.id}/?formats=lexical`)
728+
.expectStatus(200)
729+
.matchBodySnapshot({
730+
posts: [Object.assign({}, matchPostShallowIncludes, {
731+
lexical: updatedLexical
732+
})]
733+
});
734+
});
735+
});
667736
});
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
const assert = require('assert/strict');
2+
const sinon = require('sinon');
3+
const models = require('../../../../core/server/models');
4+
const dbController = require('../../../../core/server/api/endpoints/db');
5+
6+
describe('DB controller', function () {
7+
let settingsCache, importer;
8+
9+
before(function () {
10+
models.init();
11+
});
12+
13+
beforeEach(function () {
14+
settingsCache = require('../../../../core/shared/settings-cache');
15+
importer = require('../../../../core/server/data/importer');
16+
17+
sinon.stub(settingsCache, 'get').withArgs('timezone').returns('UTC');
18+
sinon.stub(importer, 'importFromFile').resolves({
19+
db: [{data: {}}],
20+
problems: []
21+
});
22+
});
23+
24+
afterEach(function () {
25+
sinon.restore();
26+
});
27+
28+
describe('importContent', function () {
29+
it('uses frame.user.email when frame.user is present', async function () {
30+
const mockUser = {
31+
get: sinon.stub().returns('[email protected]')
32+
};
33+
const frame = {
34+
user: mockUser,
35+
file: {path: 'test.json'}
36+
};
37+
38+
await dbController.importContent.query(frame);
39+
40+
// Verify the user's email was used
41+
assert(mockUser.get.calledWith('email'));
42+
assert(importer.importFromFile.calledWith(frame.file, sinon.match({
43+
user: {email: '[email protected]'}
44+
})));
45+
});
46+
47+
it('uses owner email fallback when frame.user is missing', async function () {
48+
const mockOwnerUser = {
49+
get: sinon.stub().returns('[email protected]')
50+
};
51+
sinon.stub(models.User, 'getOwnerUser').resolves(mockOwnerUser);
52+
53+
const frame = {
54+
user: null, // No user in frame (integration auth scenario)
55+
file: {path: 'test.json'}
56+
};
57+
58+
await dbController.importContent.query(frame);
59+
60+
// Verify the owner fallback path was used
61+
assert(models.User.getOwnerUser.calledOnce);
62+
assert(mockOwnerUser.get.calledWith('email'));
63+
assert(importer.importFromFile.calledWith(frame.file, sinon.match({
64+
user: {email: '[email protected]'}
65+
})));
66+
});
67+
});
68+
});

0 commit comments

Comments
 (0)