Remove Patreon functionality (#129)

* Remove Patreon links, supper status checking
* Add warnings to admin panel and README
This commit is contained in:
Eliot Partridge
2019-09-17 17:55:21 -05:00
committed by GitHub
parent 4def56d96c
commit cb5e757fd3
24 changed files with 18 additions and 1127 deletions
-650
View File
@@ -1,650 +0,0 @@
import '../lib';
import { noop } from 'lodash';
import { Types } from 'mongoose';
import { expect } from 'chai';
import { stub, assert, SinonStub, SinonFakeTimers, useFakeTimers, match } from 'sinon';
import {
createRemoveOldSupporters, createUpdateSupporters, fetchPatreonData, createUpdatePatreonInfo,
RemoveOldSupporters, UpdateSupporters, createAddTotalPledged, AddTotalPledged
} from '../../server/patreon';
import { genId, auth, account } from '../mocks';
import { PatreonFlags, PatreonData } from '../../common/adminInterfaces';
import { fromNow, fromDate } from '../../common/utils';
import { DAY, rewardLevel1, rewardLevel2 } from '../../common/constants';
const dbId = () => Types.ObjectId(genId());
const queryBase = 'page%5Bcount%5D=100&sort=created';
const queryAdd = '&include=patron.null,reward.null&fields%5Bpledge%5D=total_historical_amount_cents,declined_since';
const query = `${queryBase}${queryAdd}`;
describe('patreon', () => {
describe('fetchPatreonData()', () => {
let client: SinonStub;
beforeEach(() => {
client = stub();
client.withArgs('/current_user/campaigns')
.resolves({ rawJson: { data: [{ id: 'foo' }], included: [] } });
client.withArgs(`/campaigns/foo/pledges?${query}`)
.resolves({ rawJson: { data: [], links: {} } });
});
it('returns rewards', async () => {
client.withArgs('/current_user/campaigns')
.resolves({
rawJson: {
data: [{ id: 'foo', attributes: {} }],
included: [
{ type: 'reward', id: '123', attributes: { title: 'title', description: 'desc' } },
{ type: 'other', id: '111', attributes: { title: 'ttt', description: 'ddd' } },
],
},
});
const result = await fetchPatreonData(client as any, noop);
expect(result.rewards).eql([
{ id: '123', title: 'title', description: 'desc' },
]);
});
it('fixes incomplete data', async () => {
client.withArgs('/current_user/campaigns')
.resolves({
rawJson: {
data: [{ id: 'foo' }],
included: [
{ type: 'reward', id: '123', attributes: {} },
],
},
});
const result = await fetchPatreonData(client as any, noop);
expect(result.rewards).eql([
{ id: '123', title: '', description: '' },
]);
});
it('returns pledges', async () => {
client.withArgs(`/campaigns/foo/pledges?${query}`)
.resolves({
rawJson: {
data: [
{
attributes: { total_historical_amount_cents: 100 },
relationships: { patron: { data: { id: 'patid1' } }, reward: { data: { id: 'rewid1' } } },
},
{
attributes: { total_historical_amount_cents: 200 },
relationships: { patron: { data: { id: 'patid2' } }, reward: { data: { id: 'rewid2' } } },
},
],
links: {},
},
});
const result = await fetchPatreonData(client as any, noop);
expect(result.pledges).eql([
{ user: 'patid1', reward: 'rewid1', total: 100, declinedSince: undefined },
{ user: 'patid2', reward: 'rewid2', total: 200, declinedSince: undefined },
]);
});
it('skips pledges with missing data', async () => {
client.withArgs(`/campaigns/foo/pledges?${query}`)
.resolves({
rawJson: {
data: [
{
attributes: {},
relationships: { patron: { data: { id: 'patid1' } }, reward: { data: { id: 'rewid1' } } },
},
{
attributes: {},
relationships: { patron: {}, reward: { data: { id: 'rewid2' } } },
},
{
attributes: {},
relationships: { patron: { data: { id: 'patid3' } }, reward: {} },
},
],
links: {},
},
});
const result = await fetchPatreonData(client as any, noop);
expect(result.pledges).eql([
{ user: 'patid1', reward: 'rewid1', total: 0, declinedSince: undefined },
]);
});
it('fetches multiple pages of pledges (with correct parameters for next page)', async () => {
client.withArgs(`/campaigns/foo/pledges?${query}`)
.resolves({
rawJson: {
data: [
{
attributes: {},
relationships: { patron: { data: { id: 'patid1' } }, reward: { data: { id: 'rewid1' } } },
},
],
links: {
next: 'https://www.patreon.com/api/oauth2/api/link-to-next-page?page=5',
},
},
});
client.withArgs('/link-to-next-page?page=5' + queryAdd)
.resolves({
rawJson: {
data: [
{
attributes: {},
relationships: { patron: { data: { id: 'patid2' } }, reward: { data: { id: 'rewid2' } } },
},
],
links: {},
},
});
const result = await fetchPatreonData(client as any, noop);
assert.calledWith(client, '/link-to-next-page?page=5' + queryAdd);
expect(result.pledges).eql([
{ user: 'patid1', reward: 'rewid1', total: 0, declinedSince: undefined },
{ user: 'patid2', reward: 'rewid2', total: 0, declinedSince: undefined },
]);
});
it('throws on too many pages', async () => {
client.withArgs(`/campaigns/foo/pledges?${query}`)
.resolves({
rawJson: {
data: [
{
attributes: {},
relationships: { patron: { data: { id: 'patid1' } }, reward: { data: { id: 'rewid1' } } },
},
],
links: {
next: `/campaigns/foo/pledges?${queryBase}`,
},
},
});
await expect(fetchPatreonData(client as any, noop)).rejectedWith('Exceeded 100 pages of patreon data');
});
});
describe('updatePatreonInfo()', () => {
let queryAuths: SinonStub;
let queryAccounts: SinonStub;
let removeOldSupporters: SinonStub;
let updateSupporters: SinonStub;
let addTotalPledged: SinonStub;
let updatePatreonInfo: (data: PatreonData, now: Date) => any;
beforeEach(() => {
queryAuths = stub();
queryAccounts = stub();
removeOldSupporters = stub();
updateSupporters = stub();
addTotalPledged = stub();
updatePatreonInfo = createUpdatePatreonInfo(
queryAuths, queryAccounts, removeOldSupporters, updateSupporters, addTotalPledged);
});
it('queries auths', async () => {
queryAuths.resolves([]);
queryAccounts.resolves([]);
await updatePatreonInfo({
pledges: [
{ reward: '123', user: 'foo', total: 0 },
{ reward: '123', user: 'bar', total: 0 },
],
rewards: [],
}, new Date());
assert.calledWith(queryAuths, match({
provider: 'patreon',
openId: { $in: ['foo', 'bar'] },
account: { $exists: true },
banned: { $ne: true },
disabled: { $ne: true },
}), '_id account openId pledged');
});
it('queries accounts', async () => {
queryAuths.resolves([]);
queryAccounts.resolves([]);
await updatePatreonInfo({ pledges: [], rewards: [] }, new Date());
assert.calledWith(
queryAccounts, match({ patreon: { $exists: true, $ne: 0 } }), '_id patreon supporterDeclinedSince');
});
it('removes old supporters', async () => {
const auths = [{}] as any;
const accounts = [{}] as any;
queryAuths.resolves(auths);
queryAccounts.resolves(auths);
await updatePatreonInfo({ pledges: [], rewards: [] }, new Date());
assert.calledWith(removeOldSupporters, auths, accounts);
});
it('adds new supporters', async () => {
const auths = [{}] as any;
const accounts = [{}] as any;
const pledges = [{}] as any;
const now = new Date();
queryAuths.resolves(auths);
queryAccounts.resolves(auths);
await updatePatreonInfo({ pledges, rewards: [] }, now);
assert.calledWith(updateSupporters, auths, accounts, pledges, now);
});
it('adds total pledged', async () => {
const auths = [{}] as any;
const pledges = [{}] as any;
queryAuths.resolves(auths);
queryAccounts.resolves(auths);
await updatePatreonInfo({ pledges, rewards: [] }, new Date());
assert.calledWith(addTotalPledged, auths, pledges);
});
});
describe('removeOldSupporters()', () => {
let updateAccounts: SinonStub;
let log: SinonStub;
let removeOldSupporters: RemoveOldSupporters;
let clock: SinonFakeTimers;
const update = {
$unset: { patreon: 1, supporterDeclinedSince: 1 },
$push: {
supporterLog: {
$each: [{ date: new Date(1234), message: 'removed supporter' }],
$slice: -10,
},
},
};
beforeEach(() => {
clock = useFakeTimers();
clock.setSystemTime(1234);
updateAccounts = stub();
log = stub();
removeOldSupporters = createRemoveOldSupporters(updateAccounts, log);
});
afterEach(() => {
clock.restore();
});
it('does nothing if list of auth and accounts are empty', async () => {
await removeOldSupporters([], []);
assert.calledWithMatch(updateAccounts, { _id: { $in: [] } }, update);
});
it('unsets patreon for all accounts without corresponding auths', async () => {
const accountId = dbId();
await removeOldSupporters([], [account({ _id: accountId })]);
assert.calledWithMatch(updateAccounts, { _id: { $in: [accountId] } }, update);
assert.calledWith(log, accountId.toHexString(), `removed supporter`);
});
it('unsets patreon for all accounts without corresponding auths (2)', async () => {
const account1Id = dbId();
const account2Id = dbId();
await removeOldSupporters(
[auth({ account: Types.ObjectId(account2Id.toHexString()) })],
[account({ _id: account1Id }), account({ _id: account2Id })]);
assert.calledWithMatch(updateAccounts, { _id: { $in: [account1Id] } }, update);
assert.calledWith(log, account1Id.toHexString(), `removed supporter`);
});
it('works with unassigned auths', async () => {
const accountId = dbId();
await removeOldSupporters(
[auth({ account: undefined })],
[account({ _id: accountId })]);
assert.calledWithMatch(updateAccounts, { _id: { $in: [accountId] } }, update);
});
});
describe('updateSupporters()', () => {
let updateAccount: SinonStub;
let log: SinonStub;
let updateSupporters: UpdateSupporters;
let clock: SinonFakeTimers;
function push(message: string) {
return {
supporterLog: {
$each: [{ date: new Date(1234), message }],
$slice: -10,
},
};
}
beforeEach(() => {
clock = useFakeTimers();
clock.setSystemTime(1234);
updateAccount = stub();
log = stub();
updateSupporters = createUpdateSupporters(updateAccount, log);
});
afterEach(() => {
clock.restore();
});
it('does nothing if list of auth and accounts are empty', async () => {
await updateSupporters([], [], [], new Date());
assert.notCalled(updateAccount);
});
it('adds patreon info to account', async () => {
const accountId = dbId();
await updateSupporters(
[auth({ account: accountId, openId: '123' })],
[],
[{ reward: rewardLevel1, user: '123', total: 0 }],
new Date());
assert.calledWith(updateAccount, accountId.toString(), {
patreon: PatreonFlags.Supporter1,
supporterDeclinedSince: undefined,
$push: push('added supporter (1)'),
});
assert.calledWith(log, accountId.toString(), 'added supporter (1)');
});
it('handles duplicate auths', async () => {
const accountId = dbId();
await updateSupporters(
[auth({ account: accountId, openId: '123' }), auth({ account: accountId, openId: '321' })],
[],
[{ reward: rewardLevel2, user: '321', total: 0 }, { reward: rewardLevel1, user: '123', total: 0 }],
new Date());
assert.calledWith(updateAccount, accountId.toString(), {
patreon: PatreonFlags.Supporter2,
supporterDeclinedSince: undefined,
$push: push('added supporter (2)'),
});
assert.calledWith(log, accountId.toString(), 'added supporter (2)');
});
it('handles unassigned auths', async () => {
await updateSupporters(
[auth({ account: undefined, openId: '123' })],
[],
[{ reward: rewardLevel2, user: '123', total: 0 }],
new Date());
assert.notCalled(updateAccount);
});
it('sets supporter to none if pledge is missing', async () => {
const accountId = dbId();
await updateSupporters(
[auth({ account: accountId, openId: '123' })],
[account({ _id: accountId, patreon: PatreonFlags.Supporter1 })],
[],
new Date());
assert.calledWith(updateAccount, accountId.toString(), {
patreon: PatreonFlags.None,
supporterDeclinedSince: undefined,
$push: push('removed supporter'),
});
assert.calledWith(log, accountId.toString(), 'removed supporter');
});
it('updates existing info', async () => {
const accountId = dbId();
await updateSupporters(
[auth({ account: accountId, openId: '123' })],
[account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter1 })],
[{ reward: rewardLevel2, user: '123', total: 0 }],
new Date());
assert.calledWith(updateAccount, accountId.toString(), {
patreon: PatreonFlags.Supporter2,
supporterDeclinedSince: undefined,
$push: push('added supporter (2)'),
});
assert.calledWith(log, accountId.toString(), 'added supporter (2)');
});
it('does not update if supporter level did not change (2 patreon accounts)', async () => {
const accountId = dbId();
await updateSupporters(
[auth({ account: accountId, openId: '123' }), auth({ account: accountId, openId: '321' })],
[account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter2 })],
[{ reward: rewardLevel2, user: '123', total: 0 }],
new Date());
assert.notCalled(updateAccount);
assert.notCalled(log);
});
it('does not remove support if decline is set but day of month is < 7', async () => {
const accountId = dbId();
const now = new Date('2018-04-02T09:00:00.000Z');
const date = fromDate(now, -1 * DAY);
await updateSupporters(
[auth({ account: accountId, openId: '123' })],
[account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter1 })],
[{ reward: rewardLevel1, user: '123', total: 0, declinedSince: date.toISOString() }],
now);
assert.calledWith(updateAccount, accountId.toString(), {
supporterDeclinedSince: date,
});
assert.notCalled(log);
});
it('removes support if decline is set and day of month is > 14', async () => {
const accountId = dbId();
const now = new Date('2018-04-16T09:00:00.000Z');
const date = fromDate(now, -1 * DAY);
await updateSupporters(
[auth({ account: accountId, openId: '123' })],
[account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter1 })],
[{ reward: rewardLevel1, user: '123', total: 0, declinedSince: date.toISOString() }],
now);
assert.calledWith(updateAccount, accountId.toString(), {
patreon: PatreonFlags.None,
supporterDeclinedSince: date,
$push: push('removed supporter (declined)'),
});
assert.calledWith(log, accountId.toString(), 'removed supporter (declined)');
});
it('removes support if decline is set and day of month is < 14 and decline is > 14 days old', async () => {
const accountId = dbId();
const now = new Date('2018-04-02T09:00:00.000Z');
const date = fromDate(now, -16 * DAY);
await updateSupporters(
[auth({ account: accountId, openId: '123' })],
[account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter1 })],
[{ reward: rewardLevel1, user: '123', total: 0, declinedSince: date.toISOString() }],
now);
assert.calledWith(updateAccount, accountId.toString(), {
patreon: PatreonFlags.None,
supporterDeclinedSince: date,
$push: push('removed supporter (declined)'),
});
assert.calledWith(log, accountId.toString(), 'removed supporter (declined)');
});
it('does not add log if declined but supporter is already removed', async () => {
const accountId = dbId();
const now = new Date('2018-04-16T09:00:00.000Z');
const date = fromDate(now, -1 * DAY);
await updateSupporters(
[auth({ account: accountId, openId: '123' })],
[],
[{ reward: rewardLevel1, user: '123', total: 0, declinedSince: date.toISOString() }],
now);
assert.calledWith(updateAccount, accountId.toString(), {
patreon: PatreonFlags.None,
supporterDeclinedSince: date,
});
assert.notCalled(log);
});
it('updates declined date', async () => {
const accountId = dbId();
const date = fromNow(-100);
await updateSupporters(
[auth({ account: accountId, openId: '123' })],
[account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter1 })],
[{ reward: rewardLevel2, user: '123', total: 0, declinedSince: date.toISOString() }],
new Date());
assert.calledWith(updateAccount, accountId.toString(), {
patreon: PatreonFlags.Supporter2,
supporterDeclinedSince: date,
$push: push('added supporter (2)'),
});
assert.calledWith(log, accountId.toString(), 'added supporter (2)');
});
it('updates declined date even if patreon is not changed', async () => {
const accountId = dbId();
const date = fromNow(-100);
await updateSupporters(
[auth({ account: accountId, openId: '123' })],
[account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter2 })],
[{ reward: rewardLevel2, user: '123', total: 0, declinedSince: date.toISOString() }],
new Date());
assert.calledWith(updateAccount, accountId.toString(), {
supporterDeclinedSince: date,
});
assert.notCalled(log);
});
it('sets supporter to none if reward has invalid ID', async () => {
const accountId = dbId();
await updateSupporters(
[auth({ account: accountId, openId: '123' })],
[account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter1 })],
[{ reward: 'invalid', user: '123', total: 0 }],
new Date());
assert.calledWith(updateAccount, accountId.toString(), {
patreon: PatreonFlags.None,
supporterDeclinedSince: undefined,
$push: push('removed supporter'),
});
assert.calledWith(log, accountId.toString(), 'removed supporter');
});
it('does nothing if patreon info is already set', async () => {
const accountId = dbId();
await updateSupporters(
[auth({ account: accountId, openId: '123' })],
[account({ _id: Types.ObjectId(accountId.toHexString()), patreon: PatreonFlags.Supporter1 })],
[{ reward: rewardLevel1, user: '123', total: 0 }],
new Date());
assert.notCalled(updateAccount);
});
it('does nothing if declined date is the same', async () => {
const accountId = dbId();
await updateSupporters(
[auth({ account: accountId, openId: '123' })],
[account({
_id: Types.ObjectId(accountId.toHexString()),
patreon: PatreonFlags.Supporter1,
supporterDeclinedSince: new Date(1234),
})],
[{ reward: rewardLevel1, user: '123', total: 0, declinedSince: (new Date(1234).toISOString()) }],
new Date());
assert.notCalled(updateAccount);
});
});
describe('addTotalPledged()', () => {
let updateAuth: SinonStub;
let addTotalPledged: AddTotalPledged;
beforeEach(() => {
updateAuth = stub();
addTotalPledged = createAddTotalPledged(updateAuth);
});
it('does nothing if lists of auth and pledges are empty', async () => {
await addTotalPledged([], []);
assert.notCalled(updateAuth);
});
it('does nothing if cannot find auths for pledges', async () => {
await addTotalPledged([], [{ reward: 'some', user: '123', total: 0 }]);
assert.notCalled(updateAuth);
});
it('does nothing if cannot find pledges for auth', async () => {
await addTotalPledged([auth({ openId: '123' })], []);
assert.notCalled(updateAuth);
});
it('does nothing if total is already correct', async () => {
await addTotalPledged([auth({ openId: '123', pledged: 10 })], [{ reward: 'some', user: '123', total: 10 }]);
assert.notCalled(updateAuth);
});
it('updates total if different', async () => {
const authId = dbId();
await addTotalPledged([auth({ _id: authId, openId: '123' })], [{ reward: 'some', user: '123', total: 10 }]);
assert.calledWith(updateAuth, authId, { pledged: 10 });
});
});
});