diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/members.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/members.test.js.snap index e799417f9d..b0bb883873 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/members.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/members.test.js.snap @@ -366,7 +366,7 @@ Object { "referrer_source": "Created manually", "referrer_url": null, "title": null, - "type": "url", + "type": null, "url": null, }, "avatar_image": null, @@ -402,7 +402,7 @@ exports[`Members API Adding newsletters to member with no subscriptions works ev Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "694", + "content-length": "693", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -422,7 +422,7 @@ Object { "referrer_source": "Created manually", "referrer_url": null, "title": null, - "type": "url", + "type": null, "url": null, }, "avatar_image": null, @@ -493,7 +493,7 @@ exports[`Members API Adding newsletters to member with no subscriptions works ev Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1597", + "content-length": "1596", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -764,7 +764,7 @@ Object { "referrer_source": "Created manually", "referrer_url": null, "title": null, - "type": "url", + "type": null, "url": null, }, "avatar_image": null, @@ -800,7 +800,7 @@ exports[`Members API Can add 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "827", + "content-length": "826", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -820,7 +820,7 @@ Object { "referrer_source": "Created manually", "referrer_url": null, "title": null, - "type": "url", + "type": null, "url": null, }, "avatar_image": null, @@ -856,7 +856,7 @@ exports[`Members API Can add a member and trigger host email verification limits Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "2442", + "content-length": "2441", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -876,7 +876,7 @@ Object { "referrer_source": "Created manually", "referrer_url": null, "title": null, - "type": "url", + "type": null, "url": null, }, "avatar_image": null, @@ -912,7 +912,7 @@ exports[`Members API Can add a member and trigger host email verification limits Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "2442", + "content-length": "2441", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -932,7 +932,7 @@ Object { "referrer_source": "Created manually", "referrer_url": null, "title": null, - "type": "url", + "type": null, "url": null, }, "avatar_image": null, @@ -968,7 +968,7 @@ exports[`Members API Can add a member that is not subscribed (old) 2: [headers] Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "703", + "content-length": "702", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -1019,7 +1019,7 @@ Object { "referrer_source": null, "referrer_url": null, "title": null, - "type": "url", + "type": null, "url": null, }, "cancel_at_period_end": false, @@ -1111,7 +1111,7 @@ exports[`Members API Can add a subscription 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "3545", + "content-length": "3544", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -1161,7 +1161,7 @@ Object { "referrer_source": null, "referrer_url": null, "title": null, - "type": "url", + "type": null, "url": null, }, "cancel_at_period_end": false, @@ -1253,7 +1253,7 @@ exports[`Members API Can add a subscription 4: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "3545", + "content-length": "3544", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -1272,7 +1272,7 @@ Object { "referrer_source": "Created manually", "referrer_url": null, "title": null, - "type": "url", + "type": null, "url": null, }, "avatar_image": null, @@ -1343,7 +1343,7 @@ exports[`Members API Can add and edit with custom newsletters 2: [headers] 1`] = Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1753", + "content-length": "1752", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -1363,7 +1363,7 @@ Object { "referrer_source": "Created manually", "referrer_url": null, "title": null, - "type": "url", + "type": null, "url": null, }, "avatar_image": null, @@ -1434,7 +1434,7 @@ exports[`Members API Can add and edit with custom newsletters 4: [headers] 1`] = Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1752", + "content-length": "1751", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -1453,7 +1453,7 @@ Object { "referrer_source": "Created manually", "referrer_url": null, "title": null, - "type": "url", + "type": null, "url": null, }, "avatar_image": null, @@ -1558,7 +1558,7 @@ exports[`Members API Can add and send a signup confirmation email (old) 2: [head Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "2454", + "content-length": "2453", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -1589,7 +1589,7 @@ Object { "referrer_source": "Created manually", "referrer_url": null, "title": null, - "type": "url", + "type": null, "url": null, }, "avatar_image": null, @@ -1694,7 +1694,7 @@ exports[`Members API Can add and send a signup confirmation email 2: [headers] 1 Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "2449", + "content-length": "2448", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -1929,7 +1929,7 @@ Object { "referrer_source": "Created manually", "referrer_url": null, "title": null, - "type": "url", + "type": null, "url": null, }, "avatar_image": null, @@ -1965,7 +1965,7 @@ exports[`Members API Can add complimentary subscription (out of date) 2: [header Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1511", + "content-length": "1510", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -1985,7 +1985,7 @@ Object { "referrer_source": "Created manually", "referrer_url": null, "title": null, - "type": "url", + "type": null, "url": null, }, "avatar_image": null, @@ -2041,7 +2041,7 @@ exports[`Members API Can add complimentary subscription (out of date) 4: [header Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "3271", + "content-length": "3269", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -2400,7 +2400,7 @@ Object { "referrer_source": "Created manually", "referrer_url": null, "title": null, - "type": "url", + "type": null, "url": null, }, "avatar_image": null, @@ -2491,7 +2491,7 @@ exports[`Members API Can create a member with an existing complimentary subscrip Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "3322", + "content-length": "3320", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -2511,7 +2511,7 @@ Object { "referrer_source": "Created manually", "referrer_url": null, "title": null, - "type": "url", + "type": null, "url": null, }, "avatar_image": null, @@ -2602,7 +2602,7 @@ exports[`Members API Can create a member with an existing paid subscription 2: [ Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "3308", + "content-length": "3306", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -2622,7 +2622,7 @@ Object { "referrer_source": "Created manually", "referrer_url": null, "title": null, - "type": "url", + "type": null, "url": null, }, "avatar_image": null, @@ -2713,7 +2713,7 @@ exports[`Members API Can create a new member with a product (complimentary) 2: [ Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "2969", + "content-length": "2968", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -2755,7 +2755,7 @@ Object { "referrer_source": "Created manually", "referrer_url": null, "title": null, - "type": "url", + "type": null, "url": null, }, "avatar_image": null, @@ -2791,7 +2791,7 @@ exports[`Members API Can destroy 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "2424", + "content-length": "2423", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -2884,7 +2884,7 @@ Object { "referrer_source": null, "referrer_url": null, "title": null, - "type": "url", + "type": null, "url": null, }, "cancel_at_period_end": false, @@ -2976,7 +2976,7 @@ exports[`Members API Can edit a subscription 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "3388", + "content-length": "3387", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -3026,7 +3026,7 @@ Object { "referrer_source": null, "referrer_url": null, "title": null, - "type": "url", + "type": null, "url": null, }, "cancel_at_period_end": false, @@ -3079,7 +3079,7 @@ exports[`Members API Can edit a subscription 4: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "2488", + "content-length": "2487", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -3098,7 +3098,7 @@ Object { "referrer_source": "Created manually", "referrer_url": null, "title": null, - "type": "url", + "type": null, "url": null, }, "avatar_image": null, @@ -3134,7 +3134,7 @@ exports[`Members API Can edit by id 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1529", + "content-length": "1528", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -3154,7 +3154,7 @@ Object { "referrer_source": "Created manually", "referrer_url": null, "title": null, - "type": "url", + "type": null, "url": null, }, "avatar_image": null, @@ -3190,7 +3190,7 @@ exports[`Members API Can edit by id 4: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "678", + "content-length": "677", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -4889,7 +4889,7 @@ Object { "referrer_source": "Created manually", "referrer_url": null, "title": null, - "type": "url", + "type": null, "url": null, }, "avatar_image": null, @@ -4925,7 +4925,7 @@ exports[`Members API Can subscribe by setting (old) subscribed property to true Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "685", + "content-length": "684", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -4945,7 +4945,7 @@ Object { "referrer_source": "Created manually", "referrer_url": null, "title": null, - "type": "url", + "type": null, "url": null, }, "avatar_image": null, @@ -5050,7 +5050,7 @@ exports[`Members API Can subscribe by setting (old) subscribed property to true Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "2438", + "content-length": "2437", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -5069,7 +5069,7 @@ Object { "referrer_source": "Created manually", "referrer_url": null, "title": null, - "type": "url", + "type": null, "url": null, }, "avatar_image": null, @@ -5105,7 +5105,7 @@ exports[`Members API Can subscribe to a newsletter 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1519", + "content-length": "1518", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -5125,7 +5125,7 @@ Object { "referrer_source": "Created manually", "referrer_url": null, "title": null, - "type": "url", + "type": null, "url": null, }, "avatar_image": null, @@ -5161,7 +5161,7 @@ exports[`Members API Can subscribe to a newsletter 4: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1575", + "content-length": "1574", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -5174,7 +5174,7 @@ exports[`Members API Can subscribe to a newsletter 5: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "5687", + "content-length": "5686", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -5193,7 +5193,7 @@ Object { "referrer_source": "Created manually", "referrer_url": null, "title": null, - "type": "url", + "type": null, "url": null, }, "avatar_image": null, @@ -5264,7 +5264,7 @@ exports[`Members API Can unsubscribe by setting (old) subscribed property to fal Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1537", + "content-length": "1536", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -5284,7 +5284,7 @@ Object { "referrer_source": "Created manually", "referrer_url": null, "title": null, - "type": "url", + "type": null, "url": null, }, "avatar_image": null, @@ -5320,7 +5320,7 @@ exports[`Members API Can unsubscribe by setting (old) subscribed property to fal Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "690", + "content-length": "689", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -5657,7 +5657,7 @@ Object { "referrer_source": "Created manually", "referrer_url": null, "title": null, - "type": "url", + "type": null, "url": null, }, "avatar_image": null, @@ -5762,7 +5762,7 @@ exports[`Members API Setting subscribed when editing a member won't reset to def Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "2509", + "content-length": "2508", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -5782,7 +5782,7 @@ Object { "referrer_source": "Created manually", "referrer_url": null, "title": null, - "type": "url", + "type": null, "url": null, }, "avatar_image": null, @@ -5887,7 +5887,7 @@ exports[`Members API Setting subscribed when editing a member won't reset to def Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "2509", + "content-length": "2508", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -5906,7 +5906,7 @@ Object { "referrer_source": "Created manually", "referrer_url": null, "title": null, - "type": "url", + "type": null, "url": null, }, "avatar_image": null, @@ -5942,7 +5942,7 @@ exports[`Members API Subscribes to default newsletters 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "2425", + "content-length": "2424", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, diff --git a/ghost/core/test/e2e-api/members/__snapshots__/webhooks.test.js.snap b/ghost/core/test/e2e-api/members/__snapshots__/webhooks.test.js.snap index 5495846133..41c30d859f 100644 --- a/ghost/core/test/e2e-api/members/__snapshots__/webhooks.test.js.snap +++ b/ghost/core/test/e2e-api/members/__snapshots__/webhooks.test.js.snap @@ -132,7 +132,7 @@ exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "3204", + "content-length": "3202", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -367,7 +367,7 @@ exports[`Members API Member attribution Creates a SubscriptionCreatedEvent witho Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "3204", + "content-length": "3202", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -381,35 +381,35 @@ Object { "events": Array [ Object { "data": Any, - "type": Any, + "type": "subscription_event", }, Object { "data": Any, - "type": Any, + "type": "subscription_event", }, Object { "data": Any, - "type": Any, + "type": "subscription_event", }, Object { "data": Any, - "type": Any, + "type": "subscription_event", }, Object { "data": Any, - "type": Any, + "type": "subscription_event", }, Object { "data": Any, - "type": Any, + "type": "subscription_event", }, Object { "data": Any, - "type": Any, + "type": "subscription_event", }, Object { "data": Any, - "type": Any, + "type": "subscription_event", }, ], "meta": Object { @@ -429,7 +429,7 @@ exports[`Members API Member attribution Returns subscription created attribution Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "7962", + "content-length": "7960", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, diff --git a/ghost/core/test/e2e-api/members/webhooks.test.js b/ghost/core/test/e2e-api/members/webhooks.test.js index 1752e8cefc..7087508337 100644 --- a/ghost/core/test/e2e-api/members/webhooks.test.js +++ b/ghost/core/test/e2e-api/members/webhooks.test.js @@ -1965,7 +1965,7 @@ describe('Members API', function () { await testWithAttribution(attribution, { id: null, url: null, - type: 'url', + type: null, title: null, referrer_source: null, referrer_medium: null, @@ -1979,7 +1979,7 @@ describe('Members API', function () { await testWithAttribution(attribution, { id: null, url: null, - type: 'url', + type: null, title: null, referrer_source: null, referrer_medium: null, @@ -2000,7 +2000,6 @@ describe('Members API', function () { }) .matchBodySnapshot({ events: new Array(subscriptionAttributions.length).fill({ - type: anyString, data: anyObject }) }) diff --git a/ghost/member-attribution/lib/AttributionBuilder.js b/ghost/member-attribution/lib/AttributionBuilder.js index dd32cf0e57..77404f3725 100644 --- a/ghost/member-attribution/lib/AttributionBuilder.js +++ b/ghost/member-attribution/lib/AttributionBuilder.js @@ -2,7 +2,7 @@ * @typedef {object} AttributionResource * @prop {string|null} id * @prop {string|null} url (absolute URL) - * @prop {'page'|'post'|'author'|'tag'|'url'} type + * @prop {'page'|'post'|'author'|'tag'|'url'|null} type * @prop {string|null} title * @prop {string|null} referrerSource * @prop {string|null} referrerMedium @@ -17,7 +17,7 @@ class Attribution { * @param {object} data * @param {string|null} [data.id] * @param {string|null} [data.url] Relative to subdirectory - * @param {'page'|'post'|'author'|'tag'|'url'} [data.type] + * @param {'page'|'post'|'author'|'tag'|'url'|null} [data.type] * @param {string|null} [data.referrerSource] * @param {string|null} [data.referrerMedium] * @param {string|null} [data.referrerUrl] @@ -49,6 +49,17 @@ class Attribution { */ getResource(model) { if (!this.id || this.type === 'url' || !this.type || !model) { + if (!this.url) { + return { + id: null, + type: null, + url: null, + title: null, + referrerSource: this.referrerSource, + referrerMedium: this.referrerMedium, + referrerUrl: this.referrerUrl + }; + } return { id: null, type: 'url', diff --git a/ghost/member-attribution/lib/UrlHistory.js b/ghost/member-attribution/lib/UrlHistory.js index 2ebf99b233..5b01c92b96 100644 --- a/ghost/member-attribution/lib/UrlHistory.js +++ b/ghost/member-attribution/lib/UrlHistory.js @@ -45,14 +45,15 @@ class UrlHistory { /** * @private * @param {any[]} history - * @returns {boolean} + * @returns {history is UrlHistoryArray} */ static isValidHistory(history) { for (const item of history) { const isValidIdEntry = typeof item?.id === 'string' && typeof item?.type === 'string' && ALLOWED_TYPES.includes(item.type); const isValidPathEntry = typeof item?.path === 'string'; + const isValidReferrerSource = typeof item?.referrerSource === 'string'; - const isValidEntry = isValidPathEntry || isValidIdEntry; + const isValidEntry = isValidPathEntry || isValidIdEntry || isValidReferrerSource; if (!isValidEntry || !Number.isSafeInteger(item?.time)) { return false; diff --git a/ghost/member-attribution/test/attribution.test.js b/ghost/member-attribution/test/attribution.test.js index 0cf21de986..3f0e4ae642 100644 --- a/ghost/member-attribution/test/attribution.test.js +++ b/ghost/member-attribution/test/attribution.test.js @@ -158,6 +158,21 @@ describe('AttributionBuilder', function () { }); }); + it('Returns all null if only invalid ids', async function () { + const history = UrlHistory.create([ + {id: 'invalid', type: 'post', time: now + 124}, + {id: 'invalid', type: 'post', time: now + 124} + ]); + should(await attributionBuilder.getAttribution(history)).match({ + type: null, + id: null, + url: null, + referrerSource: 'Ghost Explore', + referrerMedium: 'Ghost Network', + referrerUrl: 'https://ghost.org/explore' + }); + }); + it('Returns null referrer attribution', async function () { attributionBuilder = new AttributionBuilder({ urlTranslator, @@ -179,18 +194,6 @@ describe('AttributionBuilder', function () { }); }); - it('Returns all null if only invalid ids', async function () { - const history = UrlHistory.create([ - {id: 'invalid', type: 'post', time: now + 124}, - {id: 'invalid', type: 'post', time: now + 124} - ]); - should(await attributionBuilder.getAttribution(history)).match({ - type: null, - id: null, - url: null - }); - }); - it('Returns all null for invalid histories', async function () { const history = UrlHistory.create('invalid'); should(await attributionBuilder.getAttribution(history)).match({ diff --git a/ghost/member-attribution/test/history.test.js b/ghost/member-attribution/test/history.test.js index c80752e189..cc04b77beb 100644 --- a/ghost/member-attribution/test/history.test.js +++ b/ghost/member-attribution/test/history.test.js @@ -85,6 +85,12 @@ describe('UrlHistory', function () { referrerSource: 'ghost-explore', referrerMedium: null, referrerUrl: 'https://ghost.org' + }], + [{ + time: Date.now(), + referrerSource: 'ghost-explore', + referrerMedium: null, + referrerUrl: 'https://ghost.org' }] ]; for (const input of inputs) { diff --git a/ghost/member-attribution/test/url-translator.test.js b/ghost/member-attribution/test/url-translator.test.js index 5430792a33..e182c3ab29 100644 --- a/ghost/member-attribution/test/url-translator.test.js +++ b/ghost/member-attribution/test/url-translator.test.js @@ -78,6 +78,10 @@ describe('UrlTranslator', function () { }); }); + it('skips items without path and type', async function () { + should(await translator.getResourceDetails({time: 123})).eql(null); + }); + it('returns posts for explicit items', async function () { should(await translator.getResourceDetails({id: 'my-post', type: 'post', time: 123})).eql({ type: 'post', diff --git a/ghost/signup-form/src/utils/api.tsx b/ghost/signup-form/src/utils/api.tsx index ebf0d05f78..771dce5eef 100644 --- a/ghost/signup-form/src/utils/api.tsx +++ b/ghost/signup-form/src/utils/api.tsx @@ -1,3 +1,5 @@ +import {getUrlHistory} from './helpers'; + export const setupGhostApi = ({siteUrl}: {siteUrl: string}) => { const apiPath = 'members/api'; @@ -16,7 +18,8 @@ export const setupGhostApi = ({siteUrl}: {siteUrl: string}) => { const payload = JSON.stringify({ email, emailType: 'signup', - labels + labels, + urlHistory: getUrlHistory() }); const response = await fetch(url, { diff --git a/ghost/signup-form/src/utils/helpers.tsx b/ghost/signup-form/src/utils/helpers.tsx index d8c4decde6..a5863a6b35 100644 --- a/ghost/signup-form/src/utils/helpers.tsx +++ b/ghost/signup-form/src/utils/helpers.tsx @@ -1,5 +1,31 @@ import {SignupFormOptions} from '../AppContext'; +export type URLHistory = { + type?: 'post', + path?: string, + time: number, + referrerSource: string | null, + referrerMedium: string | null, + referrerUrl: string | null, +}[]; + export function isMinimal(options: SignupFormOptions): boolean { return !options.title; } + +export function getUrlHistory(): URLHistory { + const history: URLHistory = []; + + // Href without query string + const currentPath = window.location.protocol + '//' + window.location.host + window.location.pathname; + const currentTime = new Date().getTime(); + + history.push({ + time: currentTime, + referrerSource: window.location.host, + referrerMedium: 'Embed', + referrerUrl: currentPath + }); + + return history; +} diff --git a/ghost/signup-form/test/e2e/attribution.test.ts b/ghost/signup-form/test/e2e/attribution.test.ts new file mode 100644 index 0000000000..1443b4c21a --- /dev/null +++ b/ghost/signup-form/test/e2e/attribution.test.ts @@ -0,0 +1,61 @@ +import {expect} from '@playwright/test'; +import {initialize} from '../utils/e2e'; +import {test} from '@playwright/test'; + +async function testHistory({page, path, referrer, urlHistory}: {page: any, path: string, urlHistory: any[]}) { + const {frame, lastApiRequest} = await initialize({page, title: 'Sign up', path}); + + // Fill out the form + const emailInput = frame.getByTestId('input'); + await emailInput.fill('jamie@example.com'); + + // Click the submit button + const submitButton = frame.getByTestId('button'); + await submitButton.click(); + + // Check input and button are gone + await expect(frame.getByTestId('input')).toHaveCount(0); + await expect(frame.getByTestId('button')).toHaveCount(0); + + // Showing the success page + await expect(frame.getByTestId('success-page')).toHaveCount(1); + + // Check email address text is visible on the page + await expect(frame.getByText('jamie@example.com')).toBeVisible(); + + // Check the request body + expect(lastApiRequest.body).not.toBeNull(); + expect(lastApiRequest.body).toHaveProperty('email', 'jamie@example.com'); + expect(lastApiRequest.body).toHaveProperty('urlHistory', urlHistory); +} + +test.describe('Attribution', async () => { + test('Sends the current path', async ({page}) => { + await testHistory({page, + path: '/my-custom-path/123', + urlHistory: [ + { + referrerMedium: 'Embed', + referrerSource: 'localhost:1234', + referrerUrl: 'https://localhost:1234/my-custom-path/123', + time: expect.any(Number) + } + ]} + ); + }); + + test('removes query string', async ({page}) => { + await testHistory({page, + path: '/my-custom-path/123?ref=ghost', + urlHistory: [ + { + referrerMedium: 'Embed', + referrerSource: 'localhost:1234', + referrerUrl: 'https://localhost:1234/my-custom-path/123', + time: expect.any(Number) + } + ]} + ); + }); +}); + diff --git a/ghost/signup-form/test/e2e/form.test.ts b/ghost/signup-form/test/e2e/form.test.ts index ddb4215833..5584f6b29a 100644 --- a/ghost/signup-form/test/e2e/form.test.ts +++ b/ghost/signup-form/test/e2e/form.test.ts @@ -190,7 +190,7 @@ test.describe('Form', async () => { }); test('Shows error message on network issues', async ({page}) => { - const {frame} = await initialize({page, title: 'Sign up', site: '127.0.0.1:9999'}); + const {frame} = await initialize({page, title: 'Sign up', site: 'http://localhost:1234/invalid'}); // Fill out the form const emailInput = frame.getByTestId('input'); diff --git a/ghost/signup-form/test/utils/e2e.ts b/ghost/signup-form/test/utils/e2e.ts index 2f06866754..ceccdff05e 100644 --- a/ghost/signup-form/test/utils/e2e.ts +++ b/ghost/signup-form/test/utils/e2e.ts @@ -1,16 +1,25 @@ import {E2E_PORT} from '../../playwright.config'; +import {Page} from '@playwright/test'; -const MOCKED_SITE_URL = 'http://localhost:1234'; +const MOCKED_SITE_URL = 'https://localhost:1234'; type LastApiRequest = { body: null | any }; -export async function initialize({page, ...options}: {page: any; title?: string, description?: string, logo?: string, backgroundColor?: string, buttonColor?: string, site?: string, 'label-1'?: string, 'label-2'?: string}) { - const url = `http://localhost:${E2E_PORT}/signup-form.min.js`; +export async function initialize({page, path, ...options}: {page: Page, path?: string; title?: string, description?: string, logo?: string, backgroundColor?: string, buttonColor?: string, site?: string, 'label-1'?: string, 'label-2'?: string}) { + const sitePath = `${MOCKED_SITE_URL}${path ?? ''}`; + await page.route(sitePath, async (route) => { + await route.fulfill({ + status: 200, + body: '' + }); + }); - await page.goto('about:blank'); + const url = `http://localhost:${E2E_PORT}/signup-form.min.js`; await page.setViewportSize({width: 1000, height: 1000}); + + await page.goto(sitePath); const lastApiRequest = await mockApi({page}); if (!options.site) { @@ -50,5 +59,9 @@ export async function mockApi({page}: {page: any}) { }); }); + await page.route(`${MOCKED_SITE_URL}/invalid/members/api/send-magic-link/`, async (route) => { + await route.abort('addressunreachable'); + }); + return lastApiRequest; }