mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-29 00:06:09 +08:00
chore: cleanup legacy logic (#15072)
This commit is contained in:
@@ -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',
|
||||
]
|
||||
Binary file not shown.
@@ -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}")
|
||||
}
|
||||
`);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user