chore: cleanup legacy logic (#15072)

This commit is contained in:
DarkSky
2026-06-03 16:20:15 +08:00
committed by GitHub
parent 8c0e1ba04e
commit 81760fd45c
142 changed files with 3351 additions and 2449 deletions
@@ -38,7 +38,7 @@ test('change email', async t => {
const jwt = signedIn?.token.token;
t.truthy(jwt);
await sendChangeEmail(app, u1Email, '/email-change');
await sendChangeEmail(app, '/email-change');
const changeMail = app.mails.last('ChangeEmail');
@@ -157,12 +157,11 @@ test('should forbid graphql callbackUrl to external origin', async t => {
.set({ 'x-request-id': 'test', 'x-operation-name': 'test' })
.send({
query: `
mutation($email: String!, $callbackUrl: String!) {
sendChangeEmail(email: $email, callbackUrl: $callbackUrl)
mutation($callbackUrl: String!) {
sendChangeEmail(callbackUrl: $callbackUrl)
}
`,
variables: {
email: u1Email,
callbackUrl: 'https://evil.example',
},
})
@@ -787,7 +787,7 @@ test('test key failure disables a saved key and success restores it', async t =>
apiKey: 'sk-test-primary',
});
const fetch = Sinon.stub(globalThis, 'fetch');
const fetch = Sinon.stub(t.context.byok as any, 'probeFetch');
fetch
.onFirstCall()
.resolves(
@@ -858,7 +858,7 @@ test('local key test does not mutate saved server config', async t => {
apiKey: 'sk-server',
});
const fetch = Sinon.stub(globalThis, 'fetch').resolves(
const fetch = Sinon.stub(t.context.byok as any, 'probeFetch').resolves(
new Response('{"error":"invalid sk-local"}', { status: 401 })
);
t.teardown(() => fetch.restore());
@@ -889,7 +889,7 @@ test('Gemini key test sends key in header and returns safe failure message', asy
const { user, workspace } = await createUserWorkspace(t);
await grantUserPlan(t, user.id);
const fetch = Sinon.stub(globalThis, 'fetch').resolves(
const fetch = Sinon.stub(t.context.byok as any, 'probeFetch').resolves(
new Response(
'failed https://generativelanguage.googleapis.com/v1beta/models?key=gemini-secret',
{ status: 401 }
@@ -916,6 +916,7 @@ test('Gemini key test sends key in header and returns safe failure message', asy
],
'gemini-secret'
);
t.deepEqual(fetch.firstCall.args[2]?.allowedHeaders, ['x-goog-api-key']);
t.false(result.message?.includes('gemini-secret'));
t.is(result.message, 'Provider rejected the BYOK key.');
});
@@ -924,7 +925,7 @@ test('FAL key test uses read-only platform API probe endpoint', async t => {
const { user, workspace } = await createUserWorkspace(t);
await grantUserPlan(t, user.id);
const fetch = Sinon.stub(globalThis, 'fetch').resolves(
const fetch = Sinon.stub(t.context.byok as any, 'probeFetch').resolves(
new Response('{}', { status: 200 })
);
t.teardown(() => fetch.restore());
@@ -943,6 +944,7 @@ test('FAL key test uses read-only platform API probe endpoint', async t => {
(fetch.firstCall.args[1]!.headers as Record<string, string>).Authorization,
'Key fal-secret'
);
t.deepEqual(fetch.firstCall.args[2]?.allowedHeaders, ['Authorization']);
});
test('provider test failures do not return raw provider response body', async t => {
@@ -970,7 +972,7 @@ test('provider test failures do not return raw provider response body', async t
message: 'Provider service is unavailable.',
},
];
const fetch = Sinon.stub(globalThis, 'fetch');
const fetch = Sinon.stub(t.context.byok as any, 'probeFetch');
for (const [index, matrixCase] of cases.entries()) {
fetch
.onCall(index)
@@ -1673,6 +1673,7 @@ test('should be able to manage context', async t => {
'workspace.file.embed.finished',
{
contextId: session.id,
workspaceId: session.workspaceId,
fileId: file.id,
chunkSize: 1,
},
@@ -53,7 +53,7 @@ test('admin feature resolver rejects commercial projection features', async t =>
test('should get null if user feature not found', async t => {
const { model, u1 } = t.context;
const userFeature = await model.get(u1.id, 'ai_early_access');
const userFeature = await model.get(u1.id, 'administrator');
t.is(userFeature, null);
});
@@ -93,7 +93,7 @@ test('should directly test user feature existence', async t => {
await model.add(u1.id, 'free_plan_v1', 'legacy projection');
t.true(await model.has(u1.id, 'free_plan_v1'));
t.false(await model.has(u1.id, 'ai_early_access'));
t.false(await model.has(u1.id, 'administrator'));
});
test('should add user feature', async t => {
@@ -924,7 +924,7 @@ test('oidc should not fall back to default email claim when custom claim is conf
test('oidc discovery should remove oauth feature on failure and restore it after backoff retry succeeds', async t => {
const { provider, factory, server } = createOidcRegistrationHarness();
const fetchStub = Sinon.stub(globalThis, 'fetch');
const fetchStub = Sinon.stub(provider as any, 'oidcFetch');
const scheduledRetries: Array<() => void> = [];
const retryDelays: number[] = [];
const setTimeoutStub = Sinon.stub(globalThis, 'setTimeout').callsFake(((
@@ -1,121 +0,0 @@
# Snapshot report for `src/__tests__/payment/service.spec.ts`
The actual snapshot is saved in `service.spec.ts.snap`.
Generated by [AVA](https://avajs.dev).
## should list normal price for unauthenticated user
> Snapshot 1
[
'pro_monthly',
'pro_yearly',
'pro_lifetime',
'ai_yearly',
'team_monthly',
'team_yearly',
]
## should list normal prices for authenticated user
> Snapshot 1
[
'pro_monthly',
'pro_yearly',
'pro_lifetime',
'ai_yearly',
'team_monthly',
'team_yearly',
]
## should not show lifetime price if not enabled
> Snapshot 1
[
'pro_monthly',
'pro_yearly',
'ai_yearly',
'team_monthly',
'team_yearly',
]
## should list early access prices for pro ea user
> Snapshot 1
[
'pro_monthly',
'pro_lifetime',
'pro_yearly_earlyaccess',
'ai_yearly',
'team_monthly',
'team_yearly',
]
## should list normal prices for pro ea user with old subscriptions
> Snapshot 1
[
'pro_monthly',
'pro_yearly',
'pro_lifetime',
'ai_yearly',
'team_monthly',
'team_yearly',
]
## should list early access prices for ai ea user
> Snapshot 1
[
'pro_monthly',
'pro_yearly',
'pro_lifetime',
'ai_yearly_earlyaccess',
'team_monthly',
'team_yearly',
]
## should list early access prices for pro and ai ea user
> Snapshot 1
[
'pro_monthly',
'pro_lifetime',
'pro_yearly_earlyaccess',
'ai_yearly_earlyaccess',
'team_monthly',
'team_yearly',
]
## should list normal prices for ai ea user with old subscriptions
> Snapshot 1
[
'pro_monthly',
'pro_yearly',
'pro_lifetime',
'ai_yearly',
'team_monthly',
'team_yearly',
]
## should be able to list prices for team
> Snapshot 1
[
'pro_monthly',
'pro_yearly',
'pro_lifetime',
'ai_yearly',
'team_monthly',
'team_yearly',
]
@@ -7,7 +7,7 @@ import { WorkspacePolicyService } from '../../core/permission';
import { QuotaStateService } from '../../core/quota/state';
import { WorkspaceService } from '../../core/workspaces';
import { Models } from '../../models';
import { LicenseService } from '../../plugins/license/service';
import { licenseClient, LicenseService } from '../../plugins/license/service';
import { PaymentEventHandlers } from '../../plugins/payment/event';
import {
SubscriptionPlan,
@@ -19,6 +19,12 @@ type Context = Record<string, never>;
const test = ava as TestFn<Context>;
const originalActivateLicense = licenseClient.activate;
test.afterEach.always(() => {
licenseClient.activate = originalActivateLicense;
});
test('workspace subscription activation only sends upgrade notification', async t => {
const events: Array<{ name: string; payload: unknown }> = [];
let reconciled = false;
@@ -120,7 +126,6 @@ test('onetime selfhost license seat allocation ignores projected license quantit
test('recurring selfhost license activation returns activation projection without remote health recheck', async t => {
const events: Array<{ name: string; payload: unknown }> = [];
const affineProRequests: string[] = [];
const upserts: unknown[] = [];
const entitlements: unknown[] = [];
const expiresAt = Date.now() + 30 * 24 * 60 * 60 * 1000;
@@ -154,28 +159,17 @@ test('recurring selfhost license activation returns activation projection withou
{} as unknown as QuotaStateService
);
(
service as unknown as {
fetchAffinePro: (path: string) => Promise<{
plan: SubscriptionPlan;
recurring: SubscriptionRecurring;
quantity: number;
endAt: number;
res: Response;
}>;
}
).fetchAffinePro = async (path: string) => {
affineProRequests.push(path);
let activatedLicenseKey: string | undefined;
licenseClient.activate = async ({ licenseKey }) => {
activatedLicenseKey = licenseKey;
return {
plan: SubscriptionPlan.SelfHostedTeam,
recurring: SubscriptionRecurring.Monthly,
quantity: 3,
endAt: expiresAt,
res: new Response(null, {
headers: {
'x-next-validate-key': 'next-validate-key',
},
}),
license: {
plan: SubscriptionPlan.SelfHostedTeam,
recurring: SubscriptionRecurring.Monthly,
quantity: 3,
expiresAt,
validateKey: 'next-validate-key',
},
};
};
@@ -189,7 +183,7 @@ test('recurring selfhost license activation returns activation projection withou
});
t.is(entitlements.length, 1);
t.is(upserts.length, 1);
t.deepEqual(affineProRequests, ['/api/team/licenses/license-key/activate']);
t.is(activatedLicenseKey, 'license-key');
t.deepEqual(events, [
{
name: 'workspace.subscription.activated',
@@ -10,17 +10,14 @@ import { EventBus } from '../../base';
import { ConfigFactory, ConfigModule } from '../../base/config';
import { CurrentUser } from '../../core/auth';
import { AuthService } from '../../core/auth/service';
import { EarlyAccessType, FeatureService } from '../../core/features';
import { SubscriptionCronJobs } from '../../plugins/payment/cron';
import { SubscriptionService } from '../../plugins/payment/service';
import { StripeFactory } from '../../plugins/payment/stripe';
import {
CouponType,
encodeLookupKey,
SubscriptionPlan,
SubscriptionRecurring,
SubscriptionStatus,
SubscriptionVariant,
} from '../../plugins/payment/types';
import { createTestingApp, type TestingApp } from '../utils';
@@ -31,15 +28,25 @@ const unixNow = () => {
const PRO_MONTHLY = `${SubscriptionPlan.Pro}_${SubscriptionRecurring.Monthly}`;
const PRO_YEARLY = `${SubscriptionPlan.Pro}_${SubscriptionRecurring.Yearly}`;
const PRO_LIFETIME = `${SubscriptionPlan.Pro}_${SubscriptionRecurring.Lifetime}`;
const PRO_EA_YEARLY = `${SubscriptionPlan.Pro}_${SubscriptionRecurring.Yearly}_${SubscriptionVariant.EA}`;
const AI_YEARLY = `${SubscriptionPlan.AI}_${SubscriptionRecurring.Yearly}`;
const AI_YEARLY_EA = `${SubscriptionPlan.AI}_${SubscriptionRecurring.Yearly}_${SubscriptionVariant.EA}`;
const TEAM_MONTHLY = `${SubscriptionPlan.Team}_${SubscriptionRecurring.Monthly}`;
const TEAM_YEARLY = `${SubscriptionPlan.Team}_${SubscriptionRecurring.Yearly}`;
// prices for code redeeming
const PRO_MONTHLY_CODE = `${SubscriptionPlan.Pro}_${SubscriptionRecurring.Monthly}_${SubscriptionVariant.Onetime}`;
const PRO_YEARLY_CODE = `${SubscriptionPlan.Pro}_${SubscriptionRecurring.Yearly}_${SubscriptionVariant.Onetime}`;
const AI_YEARLY_CODE = `${SubscriptionPlan.AI}_${SubscriptionRecurring.Yearly}_${SubscriptionVariant.Onetime}`;
const NORMAL_USER_PRICES = [
PRO_MONTHLY,
PRO_YEARLY,
PRO_LIFETIME,
AI_YEARLY,
TEAM_MONTHLY,
TEAM_YEARLY,
];
const NORMAL_USER_PRICES_WITHOUT_LIFETIME = [
PRO_MONTHLY,
PRO_YEARLY,
AI_YEARLY,
TEAM_MONTHLY,
TEAM_YEARLY,
];
const PRICES = {
[PRO_MONTHLY]: {
@@ -66,15 +73,6 @@ const PRICES = {
id: PRO_LIFETIME,
lookup_key: PRO_LIFETIME,
},
[PRO_EA_YEARLY]: {
recurring: {
interval: 'year',
},
unit_amount: 5000,
currency: 'usd',
id: PRO_EA_YEARLY,
lookup_key: PRO_EA_YEARLY,
},
[AI_YEARLY]: {
recurring: {
interval: 'year',
@@ -84,33 +82,6 @@ const PRICES = {
id: AI_YEARLY,
lookup_key: AI_YEARLY,
},
[AI_YEARLY_EA]: {
recurring: {
interval: 'year',
},
unit_amount: 9999,
currency: 'usd',
id: AI_YEARLY_EA,
lookup_key: AI_YEARLY_EA,
},
[PRO_MONTHLY_CODE]: {
unit_amount: 799,
currency: 'usd',
id: PRO_MONTHLY_CODE,
lookup_key: PRO_MONTHLY_CODE,
},
[PRO_YEARLY_CODE]: {
unit_amount: 8100,
currency: 'usd',
id: PRO_YEARLY_CODE,
lookup_key: PRO_YEARLY_CODE,
},
[AI_YEARLY_CODE]: {
unit_amount: 10680,
currency: 'usd',
id: AI_YEARLY_CODE,
lookup_key: AI_YEARLY_CODE,
},
[TEAM_MONTHLY]: {
unit_amount: 1500,
currency: 'usd',
@@ -164,7 +135,6 @@ const test = ava as TestFn<{
app: TestingApp;
service: SubscriptionService;
event: Sinon.SinonStubbedInstance<EventBus>;
feature: Sinon.SinonStubbedInstance<FeatureService>;
stripe: {
customers: Sinon.SinonStubbedInstance<Stripe.CustomersResource>;
prices: Sinon.SinonStubbedInstance<Stripe.PricesResource>;
@@ -203,16 +173,12 @@ test.before(async t => {
AppModule,
],
tapModule: m => {
m.overrideProvider(FeatureService).useValue(
Sinon.createStubInstance(FeatureService)
);
m.overrideProvider(EventBus).useValue(Sinon.createStubInstance(EventBus));
},
});
t.context.event = app.get(EventBus);
t.context.service = app.get(SubscriptionService);
t.context.feature = app.get(FeatureService);
t.context.db = app.get(PrismaClient);
t.context.app = app;
@@ -286,18 +252,21 @@ test('should list normal price for unauthenticated user', async t => {
const prices = await service.listPrices();
t.snapshot(prices.map(p => encodeLookupKey(p.lookupKey)));
t.deepEqual(
prices.map(p => encodeLookupKey(p.lookupKey)),
NORMAL_USER_PRICES
);
});
test('should list normal prices for authenticated user', async t => {
const { feature, service, u1 } = t.context;
feature.isEarlyAccessUser.withArgs(u1.id).resolves(false);
feature.isEarlyAccessUser.withArgs(u1.id, EarlyAccessType.AI).resolves(false);
const { service, u1 } = t.context;
const prices = await service.listPrices(u1);
t.snapshot(prices.map(p => encodeLookupKey(p.lookupKey)));
t.deepEqual(
prices.map(p => encodeLookupKey(p.lookupKey)),
NORMAL_USER_PRICES
);
});
test('should not show lifetime price if not enabled', async t => {
@@ -311,25 +280,14 @@ test('should not show lifetime price if not enabled', async t => {
const prices = await service.listPrices(t.context.u1);
t.snapshot(prices.map(p => encodeLookupKey(p.lookupKey)));
t.deepEqual(
prices.map(p => encodeLookupKey(p.lookupKey)),
NORMAL_USER_PRICES_WITHOUT_LIFETIME
);
});
test('should list early access prices for pro ea user', async t => {
const { feature, service, u1 } = t.context;
feature.isEarlyAccessUser.withArgs(u1.id).resolves(true);
feature.isEarlyAccessUser.withArgs(u1.id, EarlyAccessType.AI).resolves(false);
const prices = await service.listPrices(u1);
t.snapshot(prices.map(p => encodeLookupKey(p.lookupKey)));
});
test('should list normal prices for pro ea user with old subscriptions', async t => {
const { feature, service, u1, stripe } = t.context;
feature.isEarlyAccessUser.withArgs(u1.id).resolves(true);
feature.isEarlyAccessUser.withArgs(u1.id, EarlyAccessType.AI).resolves(false);
test('should list normal prices for user with old pro subscriptions', async t => {
const { service, u1, stripe } = t.context;
stripe.subscriptions.list.resolves({
data: [
@@ -352,35 +310,14 @@ test('should list normal prices for pro ea user with old subscriptions', async t
const prices = await service.listPrices(u1);
t.snapshot(prices.map(p => encodeLookupKey(p.lookupKey)));
t.deepEqual(
prices.map(p => encodeLookupKey(p.lookupKey)),
NORMAL_USER_PRICES
);
});
test('should list early access prices for ai ea user', async t => {
const { feature, service, u1 } = t.context;
feature.isEarlyAccessUser.withArgs(u1.id).resolves(false);
feature.isEarlyAccessUser.withArgs(u1.id, EarlyAccessType.AI).resolves(true);
const prices = await service.listPrices(u1);
t.snapshot(prices.map(p => encodeLookupKey(p.lookupKey)));
});
test('should list early access prices for pro and ai ea user', async t => {
const { feature, service, u1 } = t.context;
feature.isEarlyAccessUser.withArgs(u1.id).resolves(true);
const prices = await service.listPrices(u1);
t.snapshot(prices.map(p => encodeLookupKey(p.lookupKey)));
});
test('should list normal prices for ai ea user with old subscriptions', async t => {
const { feature, service, u1, stripe } = t.context;
feature.isEarlyAccessUser.withArgs(u1.id).resolves(false);
feature.isEarlyAccessUser.withArgs(u1.id, EarlyAccessType.AI).resolves(true);
test('should list normal prices for user with old ai subscriptions', async t => {
const { service, u1, stripe } = t.context;
stripe.subscriptions.list.resolves({
data: [
@@ -403,7 +340,10 @@ test('should list normal prices for ai ea user with old subscriptions', async t
const prices = await service.listPrices(u1);
t.snapshot(prices.map(p => encodeLookupKey(p.lookupKey)));
t.deepEqual(
prices.map(p => encodeLookupKey(p.lookupKey)),
NORMAL_USER_PRICES
);
});
// ============= end prices ================
@@ -470,11 +410,9 @@ test('should allow checkout after local subscription period ended', async t => {
});
test('should get correct pro plan price for checking out', async t => {
const { app, service, u1, stripe, feature } = t.context;
// non-ea user
const { app, service, u1, stripe } = t.context;
// monthly
{
feature.isEarlyAccessUser.resolves(false);
await service.checkout(
{
plan: SubscriptionPlan.Pro,
@@ -490,27 +428,8 @@ test('should get correct pro plan price for checking out', async t => {
});
}
// ea user, but monthly
// yearly
{
feature.isEarlyAccessUser.resolves(true);
await service.checkout(
{
plan: SubscriptionPlan.Pro,
recurring: SubscriptionRecurring.Monthly,
successCallbackLink: '',
},
{ user: u1 }
);
t.deepEqual(getLastCheckoutPrice(stripe.checkout.sessions.create), {
price: PRO_MONTHLY,
coupon: undefined,
});
}
// ea user, yearly
{
feature.isEarlyAccessUser.resolves(true);
await service.checkout(
{
plan: SubscriptionPlan.Pro,
@@ -521,14 +440,13 @@ test('should get correct pro plan price for checking out', async t => {
);
t.deepEqual(getLastCheckoutPrice(stripe.checkout.sessions.create), {
price: PRO_EA_YEARLY,
coupon: CouponType.ProEarlyAccessOneYearFree,
price: PRO_YEARLY,
coupon: undefined,
});
}
// ea user, yearly recurring, but has old subscription
// yearly recurring, but has old subscription
{
feature.isEarlyAccessUser.resolves(true);
stripe.subscriptions.list.resolves({
data: [
{
@@ -561,27 +479,10 @@ test('should get correct pro plan price for checking out', async t => {
price: PRO_YEARLY,
coupon: undefined,
});
await t.throwsAsync(
() =>
service.checkout(
{
plan: SubscriptionPlan.Pro,
recurring: SubscriptionRecurring.Yearly,
variant: SubscriptionVariant.EA,
successCallbackLink: '',
},
{ user: u1 }
),
{
message: 'You are trying to access a unknown subscription plan.',
}
);
}
// any user, lifetime recurring
{
feature.isEarlyAccessUser.resolves(false);
app.get(ConfigFactory).override({
payment: {
showLifetimePrice: true,
@@ -605,12 +506,10 @@ test('should get correct pro plan price for checking out', async t => {
});
test('should get correct ai plan price for checking out', async t => {
const { service, u1, stripe, feature } = t.context;
const { service, u1, stripe } = t.context;
// non-ea user
// user
{
feature.isEarlyAccessUser.resolves(false);
await service.checkout(
{
plan: SubscriptionPlan.AI,
@@ -626,111 +525,8 @@ test('should get correct ai plan price for checking out', async t => {
});
}
// ea user
// user with old subscription
{
feature.isEarlyAccessUser.resolves(true);
await service.checkout(
{
plan: SubscriptionPlan.AI,
recurring: SubscriptionRecurring.Yearly,
successCallbackLink: '',
},
{ user: u1 }
);
t.deepEqual(getLastCheckoutPrice(stripe.checkout.sessions.create), {
price: AI_YEARLY_EA,
coupon: CouponType.AIEarlyAccessOneYearFree,
});
}
// ea user, but has old subscription
{
feature.isEarlyAccessUser.withArgs(u1.id).resolves(false);
feature.isEarlyAccessUser
.withArgs(u1.id, EarlyAccessType.AI)
.resolves(true);
stripe.subscriptions.list.resolves({
data: [
{
id: 'sub_1',
status: 'canceled',
items: {
data: [
{
// @ts-expect-error stub
price: {
lookup_key: AI_YEARLY,
},
},
],
},
},
],
});
await service.checkout(
{
plan: SubscriptionPlan.AI,
recurring: SubscriptionRecurring.Yearly,
successCallbackLink: '',
},
{ user: u1 }
);
t.deepEqual(getLastCheckoutPrice(stripe.checkout.sessions.create), {
price: AI_YEARLY,
coupon: undefined,
});
await t.throwsAsync(
() =>
service.checkout(
{
plan: SubscriptionPlan.AI,
recurring: SubscriptionRecurring.Yearly,
variant: SubscriptionVariant.EA,
successCallbackLink: '',
},
{ user: u1 }
),
{
message: 'You are trying to access a unknown subscription plan.',
}
);
}
// pro ea user
{
feature.isEarlyAccessUser.withArgs(u1.id).resolves(true);
feature.isEarlyAccessUser
.withArgs(u1.id, EarlyAccessType.AI)
.resolves(false);
// @ts-expect-error stub
stripe.subscriptions.list.resolves({ data: [] });
await service.checkout(
{
plan: SubscriptionPlan.AI,
recurring: SubscriptionRecurring.Yearly,
successCallbackLink: '',
},
{ user: u1 }
);
t.deepEqual(getLastCheckoutPrice(stripe.checkout.sessions.create), {
price: AI_YEARLY,
coupon: CouponType.ProEarlyAccessAIOneYearFree,
});
}
// pro ea user, but has old subscription
{
feature.isEarlyAccessUser.withArgs(u1.id).resolves(true);
feature.isEarlyAccessUser
.withArgs(u1.id, EarlyAccessType.AI)
.resolves(false);
stripe.subscriptions.list.resolves({
data: [
{
@@ -1218,44 +1014,6 @@ const lifetimeInvoice: Stripe.Invoice = {
},
};
const onetimeMonthlyInvoice: Stripe.Invoice = {
id: 'in_2',
object: 'invoice',
amount_paid: 799,
total: 799,
customer: 'cus_1',
customer_email: 'u1@affine.pro',
currency: 'usd',
status: 'paid',
lines: {
data: [
// @ts-expect-error stub
{
price: PRICES[PRO_MONTHLY_CODE],
},
],
},
};
const onetimeYearlyInvoice: Stripe.Invoice = {
id: 'in_3',
object: 'invoice',
amount_paid: 8100,
total: 8100,
customer: 'cus_1',
customer_email: 'u1@affine.pro',
currency: 'usd',
status: 'paid',
lines: {
data: [
// @ts-expect-error stub
{
price: PRICES[PRO_YEARLY_CODE],
},
],
},
};
test('should not be able to checkout for lifetime recurring if not enabled', async t => {
const { service, u1, app } = t.context;
app.get(ConfigFactory).override({
@@ -1337,32 +1095,6 @@ test('should not be able to checkout for lifetime recurring if already subscribe
),
{ message: 'You have already subscribed to the pro plan.' }
);
await db.subscription.updateMany({
where: { targetId: u1.id },
data: {
stripeSubscriptionId: null,
recurring: SubscriptionRecurring.Monthly,
variant: SubscriptionVariant.Onetime,
end: new Date(Date.now() + 100000),
},
});
await t.throwsAsync(
() =>
service.checkout(
{
plan: SubscriptionPlan.Pro,
recurring: SubscriptionRecurring.Lifetime,
variant: null,
successCallbackLink: '',
},
{
user: u1,
}
),
{ message: 'You have already subscribed to the pro plan.' }
);
});
test('should be able to subscribe to lifetime recurring', async t => {
@@ -1474,226 +1206,16 @@ test('should not be able to update lifetime recurring', async t => {
);
});
// ============== Onetime Subscription ===============
test('should be able to checkout for onetime payment', async t => {
const { service, u1, stripe } = t.context;
await service.checkout(
{
plan: SubscriptionPlan.Pro,
recurring: SubscriptionRecurring.Monthly,
variant: SubscriptionVariant.Onetime,
successCallbackLink: '',
},
{
user: u1,
}
);
t.true(stripe.checkout.sessions.create.calledOnce);
const arg = stripe.checkout.sessions.create.firstCall
.args[0] as Stripe.Checkout.SessionCreateParams;
t.is(arg.mode, 'payment');
t.deepEqual(getLastCheckoutPrice(stripe.checkout.sessions.create), {
price: PRO_MONTHLY_CODE,
coupon: undefined,
});
});
test('should be able to checkout onetime payment if previous subscription is onetime', async t => {
const { service, u1, stripe, db } = t.context;
await db.subscription.create({
data: {
targetId: u1.id,
stripeSubscriptionId: 'sub_1',
plan: SubscriptionPlan.Pro,
recurring: SubscriptionRecurring.Monthly,
variant: SubscriptionVariant.Onetime,
status: SubscriptionStatus.Active,
start: new Date(),
end: new Date(Date.now() + 100000),
},
});
await service.checkout(
{
plan: SubscriptionPlan.Pro,
recurring: SubscriptionRecurring.Monthly,
variant: SubscriptionVariant.Onetime,
successCallbackLink: '',
},
{
user: u1,
}
);
t.true(stripe.checkout.sessions.create.calledOnce);
const arg = stripe.checkout.sessions.create.firstCall
.args[0] as Stripe.Checkout.SessionCreateParams;
t.is(arg.mode, 'payment');
t.deepEqual(getLastCheckoutPrice(stripe.checkout.sessions.create), {
price: PRO_MONTHLY_CODE,
coupon: undefined,
});
});
test('should not be able to checkout out onetime payment if previous subscription is not onetime', async t => {
const { service, u1, db } = t.context;
await db.subscription.create({
data: {
targetId: u1.id,
stripeSubscriptionId: 'sub_1',
plan: SubscriptionPlan.Pro,
recurring: SubscriptionRecurring.Monthly,
status: SubscriptionStatus.Active,
start: new Date(),
end: new Date(Date.now() + 100000),
},
});
await t.throwsAsync(
() =>
service.checkout(
{
plan: SubscriptionPlan.Pro,
recurring: SubscriptionRecurring.Monthly,
variant: SubscriptionVariant.Onetime,
successCallbackLink: '',
},
{
user: u1,
}
),
{ message: 'You have already subscribed to the pro plan.' }
);
await db.subscription.updateMany({
where: { targetId: u1.id },
data: {
stripeSubscriptionId: null,
recurring: SubscriptionRecurring.Lifetime,
},
});
await t.throwsAsync(
() =>
service.checkout(
{
plan: SubscriptionPlan.Pro,
recurring: SubscriptionRecurring.Monthly,
variant: SubscriptionVariant.Onetime,
successCallbackLink: '',
},
{
user: u1,
}
),
{ message: 'You have already subscribed to the pro plan.' }
);
});
test('should be able to subscribe onetime payment subscription', async t => {
const { service, db, u1, event } = t.context;
await service.saveStripeInvoice(onetimeMonthlyInvoice);
const subInDB = await db.subscription.findFirst({
where: { targetId: u1.id },
});
t.true(
event.emit.calledOnceWith('user.subscription.activated', {
userId: u1.id,
plan: SubscriptionPlan.Pro,
recurring: SubscriptionRecurring.Monthly,
})
);
t.is(subInDB?.plan, SubscriptionPlan.Pro);
t.is(subInDB?.recurring, SubscriptionRecurring.Monthly);
t.is(subInDB?.status, SubscriptionStatus.Active);
t.is(subInDB?.stripeSubscriptionId, null);
t.is(
subInDB?.end?.toDateString(),
new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toDateString()
);
});
test('should be able to accumulate onetime payment subscription period', async t => {
const { service, db, u1 } = t.context;
await service.saveStripeInvoice(onetimeMonthlyInvoice);
let subInDB = await db.subscription.findFirst({
where: { targetId: u1.id },
});
t.truthy(subInDB);
let end = subInDB!.end!;
await service.saveStripeInvoice(onetimeYearlyInvoice);
subInDB = await db.subscription.findFirst({
where: { targetId: u1.id },
});
// add 365 days
t.is(subInDB!.end!.getTime(), end.getTime() + 365 * 24 * 60 * 60 * 1000);
});
test('should be able to recalculate onetime payment subscription period after expiration', async t => {
const { service, db, u1 } = t.context;
await service.saveStripeInvoice(onetimeMonthlyInvoice);
let subInDB = await db.subscription.findFirst({
where: { targetId: u1.id },
});
// make subscription expired
await db.subscription.update({
where: { id: subInDB!.id },
data: {
end: new Date(Date.now() - 1000),
},
});
await service.saveStripeInvoice(onetimeYearlyInvoice);
subInDB = await db.subscription.findFirst({
where: { targetId: u1.id },
});
// add 365 days from now
t.is(
subInDB?.end?.toDateString(),
new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toDateString()
);
});
test('should not accumulate onetime payment subscription period for redeemed invoices', async t => {
const { service, db, u1 } = t.context;
// save invoices received more than once, should only redeem them once.
await service.saveStripeInvoice(onetimeYearlyInvoice);
await service.saveStripeInvoice(onetimeYearlyInvoice);
await service.saveStripeInvoice(onetimeYearlyInvoice);
const subInDB = await db.subscription.findFirst({
where: { targetId: u1.id },
});
t.is(
subInDB?.end?.toDateString(),
new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toDateString()
);
});
// TEAM
test('should be able to list prices for team', async t => {
const { service } = t.context;
const prices = await service.listPrices(undefined);
t.snapshot(prices.map(p => encodeLookupKey(p.lookupKey)));
t.deepEqual(
prices.map(p => encodeLookupKey(p.lookupKey)),
NORMAL_USER_PRICES
);
});
test('should be able to checkout for team', async t => {
@@ -21,7 +21,7 @@ export async function inviteUsers(
app: TestingApp,
workspaceId: string,
emails: string[]
): Promise<Array<{ email: string; inviteId?: string; sentSuccess?: boolean }>> {
): Promise<Array<{ email: string; inviteId?: string }>> {
const res = await app.gql(
`
mutation inviteMembers($workspaceId: String!, $emails: [String!]!) {
@@ -31,7 +31,6 @@ export async function inviteUsers(
) {
email
inviteId
sentSuccess
}
}
`,
@@ -42,19 +42,6 @@ export async function listNotifications(
return res.currentUser.notifications;
}
export async function getNotificationCount(app: TestingApp): Promise<number> {
const res = await app.gql(
`
query notificationCount {
currentUser {
notificationCount
}
}
`
);
return res.currentUser.notificationCount;
}
export async function mentionUser(
app: TestingApp,
input: MentionInput
@@ -34,12 +34,11 @@ export async function getPublicUserById(
export async function sendChangeEmail(
app: TestingApp,
email: string,
callbackUrl: string
): Promise<boolean> {
const res = await app.gql(`
mutation {
sendChangeEmail(email: "${email}", callbackUrl: "${callbackUrl}")
sendChangeEmail(callbackUrl: "${callbackUrl}")
}
`);