Skip to content

Commit

Permalink
feat: add real world version test
Browse files Browse the repository at this point in the history
  • Loading branch information
darkskygit committed Dec 30, 2024
1 parent c1e30e4 commit d38ebcd
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 34 deletions.
10 changes: 7 additions & 3 deletions packages/backend/server/src/base/error/def.ts
Original file line number Diff line number Diff line change
Expand Up @@ -596,8 +596,12 @@ export const USER_FRIENDLY_ERRORS = {
// version errors
unsupported_client_version: {
type: 'action_forbidden',
args: { minVersion: 'string' },
message: ({ minVersion }) =>
`Unsupported client version. Please upgrade to ${minVersion}.`,
args: {
clientVersion: 'string',
recommendedVersion: 'string',
action: 'string',
},
message: ({ clientVersion, recommendedVersion, action }) =>
`Unsupported client version: ${clientVersion}, please ${action} to ${recommendedVersion}.`,
},
} satisfies Record<string, UserFriendlyErrorOptions>;
4 changes: 3 additions & 1 deletion packages/backend/server/src/base/error/errors.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -593,7 +593,9 @@ export class CaptchaVerificationFailed extends UserFriendlyError {
}
@ObjectType()
class UnsupportedClientVersionDataType {
@Field() minVersion!: string
@Field() clientVersion!: string
@Field() recommendedVersion!: string
@Field() action!: string
}

export class UnsupportedClientVersion extends UserFriendlyError {
Expand Down
8 changes: 4 additions & 4 deletions packages/backend/server/src/core/version/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { defineRuntimeConfig, ModuleConfig } from '../../base/config';

export interface VersionConfig {
enable: boolean;
minVersion: string;
allowedVersion: string;
}

declare module '../../base/config' {
Expand All @@ -22,8 +22,8 @@ defineRuntimeConfig('version', {
desc: 'Check version of the app',
default: false,
},
minVersion: {
desc: 'Minimum version of the app that can access the server',
default: '0.0.0',
allowedVersion: {
desc: 'Allowed version range of the app that can access the server',
default: '>=0.0.1',
},
});
34 changes: 22 additions & 12 deletions packages/backend/server/src/core/version/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,38 +11,46 @@ export class VersionService {

constructor(private readonly runtime: Runtime) {}

private async getRecommendedVersion(minVersion: string) {
private async getRecommendedVersion(versionRange: string) {
try {
const range = new semver.Range(minVersion);
const range = new semver.Range(versionRange);
const versions = range.set
.flat()
.map(c => c.semver)
.toSorted((a, b) => semver.rcompare(a, b));
return versions[0]?.toString();
} catch {
return semver.valid(semver.coerce(minVersion));
return semver.valid(semver.coerce(versionRange));
}
}

async checkVersion(clientVersion?: any) {
const minVersion = await this.runtime.fetch('version/minVersion');
const readableMinVersion = await this.getRecommendedVersion(minVersion);
if (!minVersion || !readableMinVersion) {
// ignore invalid min version config
const allowedVersion = await this.runtime.fetch('version/allowedVersion');
const recommendedVersion = await this.getRecommendedVersion(allowedVersion);
if (!allowedVersion || !recommendedVersion) {
// ignore invalid allowed version config
return true;
}

const parsedClientVersion = semver.valid(clientVersion);
const action = semver.lt(parsedClientVersion || '0.0.0', recommendedVersion)
? 'upgrade'
: 'downgrade';
assert(
typeof clientVersion === 'string' && clientVersion.length > 0,
new UnsupportedClientVersion({
minVersion: readableMinVersion,
clientVersion: '[Not Provided]',
recommendedVersion,
action,
})
);

if (semver.valid(clientVersion)) {
if (!semver.satisfies(clientVersion, minVersion)) {
if (parsedClientVersion) {
if (!semver.satisfies(parsedClientVersion, allowedVersion)) {
throw new UnsupportedClientVersion({
minVersion: readableMinVersion,
clientVersion,
recommendedVersion,
action,
});
}
return true;
Expand All @@ -51,7 +59,9 @@ export class VersionService {
this.logger.warn(`Invalid client version: ${clientVersion}`);
}
throw new UnsupportedClientVersion({
minVersion: readableMinVersion,
clientVersion,
recommendedVersion,
action,
});
}
}
Expand Down
4 changes: 3 additions & 1 deletion packages/backend/server/src/schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -863,7 +863,9 @@ type UnknownOauthProviderDataType {
}

type UnsupportedClientVersionDataType {
minVersion: String!
action: String!
clientVersion: String!
recommendedVersion: String!
}

type UnsupportedSubscriptionPlanDataType {
Expand Down
118 changes: 105 additions & 13 deletions packages/backend/server/tests/version.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ test.beforeEach(async t => {
await initTestingDB(t.context.app.get(PrismaClient));
// reset runtime
await t.context.runtime.loadDb('version/enable');
await t.context.runtime.loadDb('version/minVersion');
await t.context.runtime.loadDb('version/allowedVersion');
await t.context.runtime.set('version/enable', false);
await t.context.runtime.set('version/minVersion', '0.0.0');
await t.context.runtime.set('version/allowedVersion', '>=0.0.1');
});

test.after.always(async t => {
Expand Down Expand Up @@ -80,46 +80,138 @@ test('should be able to prevent requests if version outdated', async t => {
await runtime.set('version/enable', true);
await t.throwsAsync(
fetchWithVersion(app.getHttpServer(), undefined, HttpStatus.FORBIDDEN),
{ message: 'Unsupported client version. Please upgrade to 0.0.0.' },
{
message:
'Unsupported client version: [Not Provided], please upgrade to 0.0.1.',
},
'should check version exists'
);
await t.throwsAsync(
fetchWithVersion(
app.getHttpServer(),
'not_a_version',
HttpStatus.FORBIDDEN
),
{
message:
'Unsupported client version: not_a_version, please upgrade to 0.0.1.',
},
'should check version exists'
);
await t.notThrowsAsync(
fetchWithVersion(app.getHttpServer(), '0.0.0', HttpStatus.OK),
fetchWithVersion(app.getHttpServer(), '0.0.1', HttpStatus.OK),
'should check version exists'
);
}

{
await runtime.set('version/minVersion', 'unknownVersion');
await runtime.set('version/allowedVersion', 'unknownVersion');
await t.notThrowsAsync(
fetchWithVersion(app.getHttpServer(), undefined, HttpStatus.OK),
'should not check version if invalid minVersion provided'
);
await t.notThrowsAsync(
fetchWithVersion(app.getHttpServer(), '0.0.0', HttpStatus.OK),
fetchWithVersion(app.getHttpServer(), '0.0.1', HttpStatus.OK),
'should not check version if invalid minVersion provided'
);

await runtime.set('version/minVersion', '0.0.1');
await runtime.set('version/allowedVersion', '0.0.1');
await t.throwsAsync(
fetchWithVersion(app.getHttpServer(), '0.0.0', HttpStatus.FORBIDDEN),
{ message: 'Unsupported client version. Please upgrade to 0.0.1.' },
{
message: 'Unsupported client version: 0.0.0, please upgrade to 0.0.1.',
},
'should reject version if valid minVersion provided'
);

await runtime.set('version/minVersion', '0.0.5 || >=0.0.7');
await runtime.set(
'version/allowedVersion',
'0.17.5 || >=0.18.0-nightly || >=0.18.0'
);
await t.notThrowsAsync(
fetchWithVersion(app.getHttpServer(), '0.0.5', HttpStatus.OK),
fetchWithVersion(app.getHttpServer(), '0.17.5', HttpStatus.OK),
'should pass version if version satisfies minVersion'
);
await t.throwsAsync(
fetchWithVersion(app.getHttpServer(), '0.0.6', HttpStatus.FORBIDDEN),
{ message: 'Unsupported client version. Please upgrade to 0.0.7.' },
fetchWithVersion(app.getHttpServer(), '0.17.4', HttpStatus.FORBIDDEN),
{
message:
'Unsupported client version: 0.17.4, please upgrade to 0.18.0.',
},
'should reject version if valid minVersion provided'
);
await t.throwsAsync(
fetchWithVersion(
app.getHttpServer(),
'0.17.6-nightly-f0d99f4',
HttpStatus.FORBIDDEN
),
{
message:
'Unsupported client version: 0.17.6-nightly-f0d99f4, please upgrade to 0.18.0.',
},
'should reject version if valid minVersion provided'
);
await t.notThrowsAsync(
fetchWithVersion(
app.getHttpServer(),
'0.18.0-nightly-cc9b38c',
HttpStatus.OK
),
'should pass version if version satisfies minVersion'
);
await t.notThrowsAsync(
fetchWithVersion(app.getHttpServer(), '0.1.0', HttpStatus.OK),
fetchWithVersion(app.getHttpServer(), '0.18.1', HttpStatus.OK),
'should pass version if version satisfies minVersion'
);
}

{
await runtime.set(
'version/allowedVersion',
'>=0.0.1 <=0.1.2 || ^0.2.0-nightly <0.2.0 || 0.3.0'
);

await t.notThrowsAsync(
fetchWithVersion(app.getHttpServer(), '0.0.1', HttpStatus.OK),
'should pass version if version satisfies minVersion'
);
await t.notThrowsAsync(
fetchWithVersion(app.getHttpServer(), '0.1.2', HttpStatus.OK),
'should pass version if version satisfies maxVersion'
);
await t.throwsAsync(
fetchWithVersion(app.getHttpServer(), '0.1.3', HttpStatus.FORBIDDEN),
{
message: 'Unsupported client version: 0.1.3, please upgrade to 0.3.0.',
},
'should reject version if valid maxVersion provided'
);

await t.notThrowsAsync(
fetchWithVersion(
app.getHttpServer(),
'0.2.0-nightly-cc9b38c',
HttpStatus.OK
),
'should pass version if version satisfies maxVersion'
);

await t.throwsAsync(
fetchWithVersion(app.getHttpServer(), '0.2.0', HttpStatus.FORBIDDEN),
{
message: 'Unsupported client version: 0.2.0, please upgrade to 0.3.0.',
},
'should reject version if valid maxVersion provided'
);

await t.throwsAsync(
fetchWithVersion(app.getHttpServer(), '0.3.1', HttpStatus.FORBIDDEN),
{
message:
'Unsupported client version: 0.3.1, please downgrade to 0.3.0.',
},
'should reject version if valid maxVersion provided'
);
}
});

0 comments on commit d38ebcd

Please sign in to comment.