mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-22 20:46:38 +08:00
fix(server): mail test & retry (#15044)
#### PR Dependency Tree * **PR #15044** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Stop sending notifications to disabled users; skip member invites when workspace names contain URLs/domains * Improve mail retry handling (per-recipient exhaustion, expiry, and cache cleanup) * Make many email headers/lead lines more generic and consistent * Fail-safe workspace content parsing to avoid crashes * **New Features** * 24-hour signup protection for sharing, invites, and invite-link creation * Job-queue: remove jobs by payload predicate * **Tests** * Expanded tests for mail jobs, SMTP hostname handling, payment checkout, job-queue removal, and abuse-detection utilities * Updated test fixtures to set createdAt timestamps for new users * **Chores** * Added required name input for test-email mutation * Database flush retry with deadlock detection/backoff <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/toeverything/AFFiNE/pull/15044?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -858,7 +858,7 @@ Generated by [AVA](https://avajs.dev).
|
||||
<!--/$-->␊
|
||||
`
|
||||
|
||||
> test@test.com invited you to join Test Workspace
|
||||
> You were invited to join a workspace on AFFiNE
|
||||
|
||||
`<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">␊
|
||||
<link␊
|
||||
@@ -973,7 +973,7 @@ Generated by [AVA](https://avajs.dev).
|
||||
<!--/$-->␊
|
||||
`
|
||||
|
||||
> test@test.com accepted your invitation
|
||||
> Your workspace invitation was accepted
|
||||
|
||||
`<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">␊
|
||||
<link␊
|
||||
@@ -1072,7 +1072,7 @@ Generated by [AVA](https://avajs.dev).
|
||||
<!--/$-->␊
|
||||
`
|
||||
|
||||
> test@test.com left Test Workspace
|
||||
> A workspace member left
|
||||
|
||||
`<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">␊
|
||||
<link␊
|
||||
@@ -1132,7 +1132,7 @@ Generated by [AVA](https://avajs.dev).
|
||||
<!--/$-->␊
|
||||
`
|
||||
|
||||
> New request to join Test Workspace
|
||||
> New request to join a workspace
|
||||
|
||||
`<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">␊
|
||||
<link␊
|
||||
@@ -1239,7 +1239,7 @@ Generated by [AVA](https://avajs.dev).
|
||||
<!--/$-->␊
|
||||
`
|
||||
|
||||
> Your request to join Test Workspace has been approved
|
||||
> Your request to join a workspace has been approved
|
||||
|
||||
`<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">␊
|
||||
<link␊
|
||||
@@ -1294,7 +1294,7 @@ Generated by [AVA](https://avajs.dev).
|
||||
<!--/$-->␊
|
||||
`
|
||||
|
||||
> Your request to join Test Workspace was declined
|
||||
> Your request to join a workspace was declined
|
||||
|
||||
`<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">␊
|
||||
<link␊
|
||||
@@ -1348,7 +1348,7 @@ Generated by [AVA](https://avajs.dev).
|
||||
<!--/$-->␊
|
||||
`
|
||||
|
||||
> You have been removed from Test Workspace
|
||||
> You have been removed from a workspace
|
||||
|
||||
`<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">␊
|
||||
<link␊
|
||||
@@ -1401,7 +1401,7 @@ Generated by [AVA](https://avajs.dev).
|
||||
<!--/$-->␊
|
||||
`
|
||||
|
||||
> Your ownership of Test Workspace has been transferred
|
||||
> Your workspace ownership has been transferred
|
||||
|
||||
`<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">␊
|
||||
<link␊
|
||||
@@ -1454,7 +1454,7 @@ Generated by [AVA](https://avajs.dev).
|
||||
<!--/$-->␊
|
||||
`
|
||||
|
||||
> You are now the owner of Test Workspace
|
||||
> You are now the owner of a workspace
|
||||
|
||||
`<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">␊
|
||||
<link␊
|
||||
@@ -1506,7 +1506,7 @@ Generated by [AVA](https://avajs.dev).
|
||||
<!--/$-->␊
|
||||
`
|
||||
|
||||
> test@test.com mentioned you in Test Doc
|
||||
> You were mentioned in AFFiNE
|
||||
|
||||
`<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">␊
|
||||
<!--$-->␊
|
||||
@@ -1601,7 +1601,7 @@ Generated by [AVA](https://avajs.dev).
|
||||
<!--/$-->␊
|
||||
`
|
||||
|
||||
> test@test.com commented on Test Doc
|
||||
> New comment in AFFiNE
|
||||
|
||||
`<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">␊
|
||||
<!--$-->␊
|
||||
@@ -1695,7 +1695,7 @@ Generated by [AVA](https://avajs.dev).
|
||||
<!--/$-->␊
|
||||
`
|
||||
|
||||
> test@test.com mentioned you in a comment on Test Doc
|
||||
> You were mentioned in a comment
|
||||
|
||||
`<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">␊
|
||||
<!--$-->␊
|
||||
@@ -1894,7 +1894,7 @@ Generated by [AVA](https://avajs.dev).
|
||||
<!--/$-->␊
|
||||
`
|
||||
|
||||
> You are now an admin of Test Workspace
|
||||
> You are now a workspace admin
|
||||
|
||||
`<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">␊
|
||||
<link␊
|
||||
@@ -1993,7 +1993,7 @@ Generated by [AVA](https://avajs.dev).
|
||||
<!--/$-->␊
|
||||
`
|
||||
|
||||
> Your role has been changed in Test Workspace
|
||||
> Your workspace role has been changed
|
||||
|
||||
`<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">␊
|
||||
<link␊
|
||||
@@ -2094,7 +2094,7 @@ Generated by [AVA](https://avajs.dev).
|
||||
<!--/$-->␊
|
||||
`
|
||||
|
||||
> [Action Required] Final warning: Your workspace Test Workspace will be deleted in 24 hours
|
||||
> [Action Required] Final warning: Your workspace will be deleted in 24 hours
|
||||
|
||||
`<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">␊
|
||||
<link␊
|
||||
@@ -2208,7 +2208,7 @@ Generated by [AVA](https://avajs.dev).
|
||||
<!--/$-->␊
|
||||
`
|
||||
|
||||
> [Action Required] Important: Your workspace Test Workspace will be deleted soon
|
||||
> [Action Required] Important: Your workspace will be deleted soon
|
||||
|
||||
`<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">␊
|
||||
<link␊
|
||||
@@ -2324,7 +2324,7 @@ Generated by [AVA](https://avajs.dev).
|
||||
<!--/$-->␊
|
||||
`
|
||||
|
||||
> Your workspace Test Workspace has been deleted
|
||||
> Your workspace has been deleted
|
||||
|
||||
`<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">␊
|
||||
<link␊
|
||||
@@ -2408,7 +2408,7 @@ Generated by [AVA](https://avajs.dev).
|
||||
<!--/$-->␊
|
||||
`
|
||||
|
||||
> [Action Required] Your Test Workspace team workspace will expire soon
|
||||
> [Action Required] Your team workspace will expire soon
|
||||
|
||||
`<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">␊
|
||||
<link␊
|
||||
@@ -2511,7 +2511,7 @@ Generated by [AVA](https://avajs.dev).
|
||||
<!--/$-->␊
|
||||
`
|
||||
|
||||
> Your Test Workspace team workspace has expired
|
||||
> Your team workspace has expired
|
||||
|
||||
`<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">␊
|
||||
<link␊
|
||||
@@ -2689,7 +2689,7 @@ Generated by [AVA](https://avajs.dev).
|
||||
|
||||
## should render mention email with empty doc title
|
||||
|
||||
> test@test.com mentioned you in
|
||||
> You were mentioned in AFFiNE
|
||||
|
||||
`<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">␊
|
||||
<!--$-->␊
|
||||
|
||||
Binary file not shown.
@@ -31,6 +31,7 @@ test('should normalize valid SMTP HELO hostnames', t => {
|
||||
});
|
||||
|
||||
test('should reject invalid SMTP HELO hostnames', t => {
|
||||
t.is(normalizeSMTPHeloHostname(), undefined);
|
||||
t.is(normalizeSMTPHeloHostname(''), undefined);
|
||||
t.is(normalizeSMTPHeloHostname(' '), undefined);
|
||||
t.is(normalizeSMTPHeloHostname('AFFiNE Server'), undefined);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { JobQueue } from '../../base';
|
||||
export class MockJobQueue {
|
||||
add = Sinon.createStubInstance(JobQueue).add.resolves();
|
||||
remove = Sinon.createStubInstance(JobQueue).remove.resolves();
|
||||
removeWhere = Sinon.createStubInstance(JobQueue).removeWhere.resolves([]);
|
||||
|
||||
last<Job extends JobName>(name: Job): { name: Job; payload: Jobs[Job] } {
|
||||
const addJobName = this.add.lastCall?.args[0];
|
||||
|
||||
@@ -19,6 +19,7 @@ export class MockUser extends Mocker<MockUserInput, MockedUser> {
|
||||
const password = input?.password ?? faker.internet.password();
|
||||
const user = await this.db.user.create({
|
||||
data: {
|
||||
createdAt: new Date(Date.now() - 25 * 60 * 60 * 1000),
|
||||
email: faker.internet.email(),
|
||||
name: faker.person.fullName(),
|
||||
password: password ? hashSync(password) : undefined,
|
||||
|
||||
@@ -438,6 +438,37 @@ test('should throw if user has subscription already', async t => {
|
||||
);
|
||||
});
|
||||
|
||||
test('should allow checkout after local subscription period ended', async t => {
|
||||
const { service, u1, db, stripe } = t.context;
|
||||
|
||||
await db.subscription.create({
|
||||
data: {
|
||||
targetId: u1.id,
|
||||
stripeSubscriptionId: 'sub_expired_ai',
|
||||
plan: SubscriptionPlan.AI,
|
||||
recurring: SubscriptionRecurring.Yearly,
|
||||
status: SubscriptionStatus.Active,
|
||||
start: new Date('2026-05-04T13:11:45.000Z'),
|
||||
end: new Date('2026-05-11T13:11:45.000Z'),
|
||||
},
|
||||
});
|
||||
|
||||
await service.checkout(
|
||||
{
|
||||
plan: SubscriptionPlan.AI,
|
||||
recurring: SubscriptionRecurring.Yearly,
|
||||
successCallbackLink: '',
|
||||
},
|
||||
{ user: u1 }
|
||||
);
|
||||
|
||||
t.true(stripe.checkout.sessions.create.calledOnce);
|
||||
t.deepEqual(getLastCheckoutPrice(stripe.checkout.sessions.create), {
|
||||
price: AI_YEARLY,
|
||||
coupon: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
test('should get correct pro plan price for checking out', async t => {
|
||||
const { app, service, u1, stripe, feature } = t.context;
|
||||
// non-ea user
|
||||
|
||||
@@ -280,6 +280,7 @@ export class TestingApp extends ApplyType<INestApplication>() {
|
||||
password: '1',
|
||||
name: email,
|
||||
emailVerifiedAt: new Date(),
|
||||
createdAt: new Date(Date.now() - 25 * 60 * 60 * 1000),
|
||||
...override,
|
||||
});
|
||||
|
||||
|
||||
@@ -11,13 +11,39 @@ async function flushDB(client: PrismaClient) {
|
||||
FROM pg_catalog.pg_tables
|
||||
WHERE schemaname != 'pg_catalog'
|
||||
AND schemaname != 'information_schema'`;
|
||||
const query = `TRUNCATE TABLE ${result
|
||||
.map(({ tablename }) => tablename)
|
||||
.filter(name => !name.includes('migrations'))
|
||||
.join(', ')}`;
|
||||
|
||||
// remove all table data
|
||||
await client.$executeRawUnsafe(
|
||||
`TRUNCATE TABLE ${result
|
||||
.map(({ tablename }) => tablename)
|
||||
.filter(name => !name.includes('migrations'))
|
||||
.join(', ')}`
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
try {
|
||||
// remove all table data
|
||||
await client.$executeRawUnsafe(query);
|
||||
return;
|
||||
} catch (error) {
|
||||
if (!isDeadlockError(error) || attempt === 2) {
|
||||
throw error;
|
||||
}
|
||||
await sleep((attempt + 1) * 50);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isDeadlockError(error: unknown) {
|
||||
if (typeof error !== 'object' || error === null || !('code' in error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const prismaError = error as {
|
||||
code?: string;
|
||||
meta?: { code?: string; message?: string };
|
||||
};
|
||||
|
||||
return (
|
||||
prismaError.code === 'P2010' &&
|
||||
(prismaError.meta?.code === '40P01' ||
|
||||
/deadlock detected/i.test(prismaError.meta?.message ?? ''))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user