fix(server): test & schema

This commit is contained in:
DarkSky
2026-05-04 03:56:14 +08:00
parent 74d5ebad13
commit 1ad088398f
4 changed files with 119 additions and 72 deletions

View File

@@ -469,6 +469,13 @@
"type": "string",
"description": "The account id for the cloudflare r2 storage provider."
},
"jurisdiction": {
"type": "string",
"enum": [
"eu"
],
"description": "Optional jurisdiction for the cloudflare r2 endpoint. Set to \"eu\" for EU buckets."
},
"usePresignedURL": {
"type": "object",
"description": "The presigned url config for the cloudflare r2 storage provider.",
@@ -486,13 +493,6 @@
"description": "The presigned key for the cloudflare r2 storage provider."
}
}
},
"jurisdiction": {
"type": "string",
"enum": [
"eu"
],
"description": "Optional jurisdiction for the cloudflare r2 endpoint. Set to \"eu\" for EU buckets."
}
}
}
@@ -667,6 +667,13 @@
"type": "string",
"description": "The account id for the cloudflare r2 storage provider."
},
"jurisdiction": {
"type": "string",
"enum": [
"eu"
],
"description": "Optional jurisdiction for the cloudflare r2 endpoint. Set to \"eu\" for EU buckets."
},
"usePresignedURL": {
"type": "object",
"description": "The presigned url config for the cloudflare r2 storage provider.",
@@ -684,13 +691,6 @@
"description": "The presigned key for the cloudflare r2 storage provider."
}
}
},
"jurisdiction": {
"type": "string",
"enum": [
"eu"
],
"description": "Optional jurisdiction for the cloudflare r2 endpoint. Set to \"eu\" for EU buckets."
}
}
}
@@ -861,11 +861,14 @@
"properties": {
"google": {
"type": "object",
"description": "Google Calendar integration config\n@default {\"enabled\":false,\"clientId\":\"\",\"clientSecret\":\"\",\"externalWebhookUrl\":\"\",\"webhookVerificationToken\":\"\",\"requestTimeoutMs\":10000}\n@link https://developers.google.com/calendar/api/guides/push",
"description": "Google Calendar integration config\n@default {\"enabled\":false,\"allowNewAccounts\":true,\"clientId\":\"\",\"clientSecret\":\"\",\"externalWebhookUrl\":\"\",\"webhookVerificationToken\":\"\",\"requestTimeoutMs\":10000}\n@link https://developers.google.com/calendar/api/guides/push",
"properties": {
"enabled": {
"type": "boolean"
},
"allowNewAccounts": {
"type": "boolean"
},
"clientId": {
"type": "string"
},
@@ -884,6 +887,7 @@
},
"default": {
"enabled": false,
"allowNewAccounts": true,
"clientId": "",
"clientSecret": "",
"externalWebhookUrl": "",
@@ -1296,6 +1300,13 @@
"type": "string",
"description": "The account id for the cloudflare r2 storage provider."
},
"jurisdiction": {
"type": "string",
"enum": [
"eu"
],
"description": "Optional jurisdiction for the cloudflare r2 endpoint. Set to \"eu\" for EU buckets."
},
"usePresignedURL": {
"type": "object",
"description": "The presigned url config for the cloudflare r2 storage provider.",
@@ -1313,13 +1324,6 @@
"description": "The presigned key for the cloudflare r2 storage provider."
}
}
},
"jurisdiction": {
"type": "string",
"enum": [
"eu"
],
"description": "Optional jurisdiction for the cloudflare r2 endpoint. Set to \"eu\" for EU buckets."
}
}
}

View File

@@ -209,47 +209,77 @@ test('listAccounts includes calendars count', async t => {
t.is(counts.get(accountB.id), 1);
});
test('assertCanLinkProvider blocks new google calendar accounts when disabled', async t => {
config.calendar.google.allowNewAccounts = false;
const user = await module.create(Mockers.User);
test.serial(
'assertCanLinkProvider blocks new google calendar accounts when disabled',
async t => {
config.calendar.google.allowNewAccounts = false;
const user = await module.create(Mockers.User);
const error = await t.throwsAsync(
calendarService.assertCanLinkProvider(user.id, CalendarProviderName.Google)
);
t.true(error instanceof GraphqlBadRequest);
t.is(
(error as GraphqlBadRequest).data?.code,
'calendar_provider_link_disabled'
);
});
const error = await t.throwsAsync(
calendarService.assertCanLinkProvider(
user.id,
CalendarProviderName.Google
)
);
t.true(error instanceof GraphqlBadRequest);
t.is(
(error as GraphqlBadRequest).data?.code,
'calendar_provider_link_disabled'
);
}
);
test('assertCanLinkProvider allows users with an existing google calendar account', async t => {
config.calendar.google.allowNewAccounts = false;
const user = await module.create(Mockers.User);
await createAccount(user.id);
test.serial(
'assertCanLinkProvider allows users with an existing google calendar account',
async t => {
config.calendar.google.allowNewAccounts = false;
const user = await module.create(Mockers.User);
await createAccount(user.id);
await t.notThrowsAsync(
calendarService.assertCanLinkProvider(user.id, CalendarProviderName.Google)
);
});
await t.notThrowsAsync(
calendarService.assertCanLinkProvider(
user.id,
CalendarProviderName.Google
)
);
}
);
test('handleOAuthCallback does not persist new google account when linking is disabled', async t => {
config.calendar.google.allowNewAccounts = false;
const provider = new MockCalendarProvider();
providerFactory.register(provider);
const user = await module.create(Mockers.User);
test.serial(
'handleOAuthCallback does not persist new google account when linking is disabled',
async t => {
config.calendar.google.allowNewAccounts = false;
const provider = new MockCalendarProvider();
mock.method(providerFactory, 'get', () => provider);
const user = await module.create(Mockers.User);
const error = await t.throwsAsync(
calendarService.handleOAuthCallback({
provider: CalendarProviderName.Google,
code: 'code',
redirectUri: 'https://example.com/callback',
userId: user.id,
})
);
t.true(error instanceof GraphqlBadRequest);
t.is((await models.calendarAccount.listByUser(user.id)).length, 0);
});
const error = await t.throwsAsync(
calendarService.handleOAuthCallback({
provider: CalendarProviderName.Google,
code: 'code',
redirectUri: 'https://example.com/callback',
userId: user.id,
})
);
t.true(error instanceof GraphqlBadRequest);
t.is((await models.calendarAccount.listByUser(user.id)).length, 0);
}
);
test.serial(
'canLinkProvider returns false for new google calendar accounts when disabled',
async t => {
config.calendar.google.allowNewAccounts = false;
const user = await module.create(Mockers.User);
t.false(
await calendarService.canLinkProvider(
user.id,
CalendarProviderName.Google
)
);
}
);
test('syncSubscription resets invalid sync token and maps events', async t => {
const user = await module.create(Mockers.User);

View File

@@ -33,17 +33,20 @@ import {
export class CalendarServerConfigResolver {
constructor(
private readonly providerFactory: CalendarProviderFactory,
private readonly config: Config
private readonly config: Config,
private readonly calendar: CalendarService
) {}
@ResolveField(() => [CalendarProviderName])
calendarProviders() {
return this.providerFactory.providers.filter(provider => {
return (
provider !== CalendarProviderName.Google ||
this.config.calendar.google.allowNewAccounts !== false
);
});
async calendarProviders(@CurrentUser() user?: CurrentUser) {
const providers = [];
for (const provider of this.providerFactory.providers) {
if (!(await this.calendar.canLinkProvider(user?.id, provider))) {
continue;
}
providers.push(provider);
}
return providers;
}
@ResolveField(() => [CalendarCalDAVProviderPresetObjectType])

View File

@@ -571,18 +571,28 @@ export class CalendarService {
}
async assertCanLinkProvider(userId: string, provider: CalendarProviderName) {
if (this.canCreateNewAccounts(provider)) {
return;
}
const accounts = await this.models.calendarAccount.listByUser(userId);
if (accounts.some(account => account.provider === provider)) {
if (await this.canLinkProvider(userId, provider)) {
return;
}
throw this.providerLinkDisabledError(provider);
}
async canLinkProvider(
userId: string | null | undefined,
provider: CalendarProviderName
) {
if (this.canCreateNewAccounts(provider)) {
return true;
}
if (!userId) {
return false;
}
const accounts = await this.models.calendarAccount.listByUser(userId);
return accounts.some(account => account.provider === provider);
}
getAuthUrl(
provider: CalendarProviderName,
state: string,