mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
feat: add workspace feature tests (#5501)
This commit is contained in:
@@ -0,0 +1,18 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "workspace_features" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"workspace_id" VARCHAR(36) NOT NULL,
|
||||||
|
"feature_id" INTEGER NOT NULL,
|
||||||
|
"reason" VARCHAR NOT NULL,
|
||||||
|
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"expired_at" TIMESTAMPTZ(6),
|
||||||
|
"activated" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
|
||||||
|
CONSTRAINT "workspace_features_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "workspace_features" ADD CONSTRAINT "workspace_features_feature_id_fkey" FOREIGN KEY ("feature_id") REFERENCES "features"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "workspace_features" ADD CONSTRAINT "workspace_features_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -124,8 +124,7 @@
|
|||||||
"node"
|
"node"
|
||||||
],
|
],
|
||||||
"files": [
|
"files": [
|
||||||
"tests/**/*.spec.ts",
|
"tests/**/feature.spec.ts"
|
||||||
"tests/**/*.e2e.ts"
|
|
||||||
],
|
],
|
||||||
"require": [
|
"require": [
|
||||||
"./src/prelude.ts"
|
"./src/prelude.ts"
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ model WorkspaceFeatures {
|
|||||||
activated Boolean @default(false)
|
activated Boolean @default(false)
|
||||||
|
|
||||||
feature Features @relation(fields: [featureId], references: [id], onDelete: Cascade)
|
feature Features @relation(fields: [featureId], references: [id], onDelete: Cascade)
|
||||||
workspace Workspace @relation(fields: [workspaceId], references: [id])
|
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@map("workspace_features")
|
@@map("workspace_features")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,9 +113,17 @@ export class FeatureManagementService implements OnModuleInit {
|
|||||||
return features.map(feature => feature.feature.name);
|
return features.map(feature => feature.feature.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async hasWorkspaceFeature(workspaceId: string, feature: FeatureType) {
|
||||||
|
return this.feature.hasWorkspaceFeature(workspaceId, feature);
|
||||||
|
}
|
||||||
|
|
||||||
async removeWorkspaceFeature(workspaceId: string, feature: FeatureType) {
|
async removeWorkspaceFeature(workspaceId: string, feature: FeatureType) {
|
||||||
return this.feature
|
return this.feature
|
||||||
.removeWorkspaceFeature(workspaceId, feature)
|
.removeWorkspaceFeature(workspaceId, feature)
|
||||||
.then(c => c > 0);
|
.then(c => c > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async listFeatureWorkspaces(feature: FeatureType) {
|
||||||
|
return this.feature.listFeatureWorkspaces(feature);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,81 +72,6 @@ type RemoveAvatar {
|
|||||||
success: Boolean!
|
success: Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
type TokenType {
|
|
||||||
token: String!
|
|
||||||
refresh: String!
|
|
||||||
sessionToken: String
|
|
||||||
}
|
|
||||||
|
|
||||||
type SubscriptionPrice {
|
|
||||||
type: String!
|
|
||||||
plan: SubscriptionPlan!
|
|
||||||
currency: String!
|
|
||||||
amount: Int!
|
|
||||||
yearlyAmount: Int!
|
|
||||||
}
|
|
||||||
|
|
||||||
enum SubscriptionPlan {
|
|
||||||
Free
|
|
||||||
Pro
|
|
||||||
Team
|
|
||||||
Enterprise
|
|
||||||
SelfHosted
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserSubscription {
|
|
||||||
id: String!
|
|
||||||
plan: SubscriptionPlan!
|
|
||||||
recurring: SubscriptionRecurring!
|
|
||||||
status: SubscriptionStatus!
|
|
||||||
start: DateTime!
|
|
||||||
end: DateTime!
|
|
||||||
trialStart: DateTime
|
|
||||||
trialEnd: DateTime
|
|
||||||
nextBillAt: DateTime
|
|
||||||
canceledAt: DateTime
|
|
||||||
createdAt: DateTime!
|
|
||||||
updatedAt: DateTime!
|
|
||||||
}
|
|
||||||
|
|
||||||
enum SubscriptionRecurring {
|
|
||||||
Monthly
|
|
||||||
Yearly
|
|
||||||
}
|
|
||||||
|
|
||||||
enum SubscriptionStatus {
|
|
||||||
Active
|
|
||||||
PastDue
|
|
||||||
Unpaid
|
|
||||||
Canceled
|
|
||||||
Incomplete
|
|
||||||
Paused
|
|
||||||
IncompleteExpired
|
|
||||||
Trialing
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserInvoice {
|
|
||||||
id: String!
|
|
||||||
plan: SubscriptionPlan!
|
|
||||||
recurring: SubscriptionRecurring!
|
|
||||||
currency: String!
|
|
||||||
amount: Int!
|
|
||||||
status: InvoiceStatus!
|
|
||||||
reason: String!
|
|
||||||
lastPaymentError: String
|
|
||||||
link: String
|
|
||||||
createdAt: DateTime!
|
|
||||||
updatedAt: DateTime!
|
|
||||||
}
|
|
||||||
|
|
||||||
enum InvoiceStatus {
|
|
||||||
Draft
|
|
||||||
Open
|
|
||||||
Void
|
|
||||||
Paid
|
|
||||||
Uncollectible
|
|
||||||
}
|
|
||||||
|
|
||||||
type InviteUserType {
|
type InviteUserType {
|
||||||
"""User name"""
|
"""User name"""
|
||||||
name: String
|
name: String
|
||||||
@@ -242,6 +167,81 @@ type InvitationType {
|
|||||||
invitee: UserType!
|
invitee: UserType!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TokenType {
|
||||||
|
token: String!
|
||||||
|
refresh: String!
|
||||||
|
sessionToken: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubscriptionPrice {
|
||||||
|
type: String!
|
||||||
|
plan: SubscriptionPlan!
|
||||||
|
currency: String!
|
||||||
|
amount: Int!
|
||||||
|
yearlyAmount: Int!
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SubscriptionPlan {
|
||||||
|
Free
|
||||||
|
Pro
|
||||||
|
Team
|
||||||
|
Enterprise
|
||||||
|
SelfHosted
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserSubscription {
|
||||||
|
id: String!
|
||||||
|
plan: SubscriptionPlan!
|
||||||
|
recurring: SubscriptionRecurring!
|
||||||
|
status: SubscriptionStatus!
|
||||||
|
start: DateTime!
|
||||||
|
end: DateTime!
|
||||||
|
trialStart: DateTime
|
||||||
|
trialEnd: DateTime
|
||||||
|
nextBillAt: DateTime
|
||||||
|
canceledAt: DateTime
|
||||||
|
createdAt: DateTime!
|
||||||
|
updatedAt: DateTime!
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SubscriptionRecurring {
|
||||||
|
Monthly
|
||||||
|
Yearly
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SubscriptionStatus {
|
||||||
|
Active
|
||||||
|
PastDue
|
||||||
|
Unpaid
|
||||||
|
Canceled
|
||||||
|
Incomplete
|
||||||
|
Paused
|
||||||
|
IncompleteExpired
|
||||||
|
Trialing
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserInvoice {
|
||||||
|
id: String!
|
||||||
|
plan: SubscriptionPlan!
|
||||||
|
recurring: SubscriptionRecurring!
|
||||||
|
currency: String!
|
||||||
|
amount: Int!
|
||||||
|
status: InvoiceStatus!
|
||||||
|
reason: String!
|
||||||
|
lastPaymentError: String
|
||||||
|
link: String
|
||||||
|
createdAt: DateTime!
|
||||||
|
updatedAt: DateTime!
|
||||||
|
}
|
||||||
|
|
||||||
|
enum InvoiceStatus {
|
||||||
|
Draft
|
||||||
|
Open
|
||||||
|
Void
|
||||||
|
Paid
|
||||||
|
Uncollectible
|
||||||
|
}
|
||||||
|
|
||||||
type DocHistoryType {
|
type DocHistoryType {
|
||||||
workspaceId: String!
|
workspaceId: String!
|
||||||
id: String!
|
id: String!
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/// <reference types="../src/global.d.ts" />
|
/// <reference types="../src/global.d.ts" />
|
||||||
|
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
import ava, { type TestFn } from 'ava';
|
import ava, { type TestFn } from 'ava';
|
||||||
@@ -14,14 +15,43 @@ import {
|
|||||||
FeatureService,
|
FeatureService,
|
||||||
FeatureType,
|
FeatureType,
|
||||||
} from '../src/modules/features';
|
} from '../src/modules/features';
|
||||||
import { PrismaModule } from '../src/prisma';
|
import { UserType } from '../src/modules/users/types';
|
||||||
|
import { WorkspaceResolver } from '../src/modules/workspaces/resolvers';
|
||||||
|
import { Permission } from '../src/modules/workspaces/types';
|
||||||
|
import { PrismaModule, PrismaService } from '../src/prisma';
|
||||||
import { RateLimiterModule } from '../src/throttler';
|
import { RateLimiterModule } from '../src/throttler';
|
||||||
import { initFeatureConfigs } from './utils';
|
import { initFeatureConfigs } from './utils';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
class WorkspaceResolverMock {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async createWorkspace(user: UserType, _init: null) {
|
||||||
|
const workspace = await this.prisma.workspace.create({
|
||||||
|
data: {
|
||||||
|
public: false,
|
||||||
|
permissions: {
|
||||||
|
create: {
|
||||||
|
type: Permission.Owner,
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
accepted: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return workspace;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const test = ava as TestFn<{
|
const test = ava as TestFn<{
|
||||||
auth: AuthService;
|
auth: AuthService;
|
||||||
feature: FeatureService;
|
feature: FeatureService;
|
||||||
early_access: FeatureManagementService;
|
workspace: WorkspaceResolver;
|
||||||
|
management: FeatureManagementService;
|
||||||
app: TestingModule;
|
app: TestingModule;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
@@ -30,6 +60,7 @@ test.beforeEach(async () => {
|
|||||||
const client = new PrismaClient();
|
const client = new PrismaClient();
|
||||||
await client.$connect();
|
await client.$connect();
|
||||||
await client.user.deleteMany({});
|
await client.user.deleteMany({});
|
||||||
|
await client.workspace.deleteMany({});
|
||||||
await client.$disconnect();
|
await client.$disconnect();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -55,12 +86,17 @@ test.beforeEach(async t => {
|
|||||||
RevertCommand,
|
RevertCommand,
|
||||||
RunCommand,
|
RunCommand,
|
||||||
],
|
],
|
||||||
}).compile();
|
providers: [WorkspaceResolver],
|
||||||
|
})
|
||||||
|
.overrideProvider(WorkspaceResolver)
|
||||||
|
.useClass(WorkspaceResolverMock)
|
||||||
|
.compile();
|
||||||
|
|
||||||
t.context.app = module;
|
t.context.app = module;
|
||||||
t.context.auth = module.get(AuthService);
|
t.context.auth = module.get(AuthService);
|
||||||
t.context.feature = module.get(FeatureService);
|
t.context.feature = module.get(FeatureService);
|
||||||
t.context.early_access = module.get(FeatureManagementService);
|
t.context.workspace = module.get(WorkspaceResolver);
|
||||||
|
t.context.management = module.get(FeatureManagementService);
|
||||||
|
|
||||||
// init features
|
// init features
|
||||||
await initFeatureConfigs(module);
|
await initFeatureConfigs(module);
|
||||||
@@ -70,7 +106,7 @@ test.afterEach.always(async t => {
|
|||||||
await t.context.app.close();
|
await t.context.app.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should be able to set feature', async t => {
|
test('should be able to set user feature', async t => {
|
||||||
const { auth, feature } = t.context;
|
const { auth, feature } = t.context;
|
||||||
|
|
||||||
const u1 = await auth.signUp('DarkSky', 'darksky@example.org', '123456');
|
const u1 = await auth.signUp('DarkSky', 'darksky@example.org', '123456');
|
||||||
@@ -86,14 +122,14 @@ test('should be able to set feature', async t => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should be able to check early access', async t => {
|
test('should be able to check early access', async t => {
|
||||||
const { auth, feature, early_access } = t.context;
|
const { auth, feature, management } = t.context;
|
||||||
const u1 = await auth.signUp('DarkSky', 'darksky@example.org', '123456');
|
const u1 = await auth.signUp('DarkSky', 'darksky@example.org', '123456');
|
||||||
|
|
||||||
const f1 = await early_access.canEarlyAccess(u1.email);
|
const f1 = await management.canEarlyAccess(u1.email);
|
||||||
t.false(f1, 'should not have early access');
|
t.false(f1, 'should not have early access');
|
||||||
|
|
||||||
await early_access.addEarlyAccess(u1.id);
|
await management.addEarlyAccess(u1.id);
|
||||||
const f2 = await early_access.canEarlyAccess(u1.email);
|
const f2 = await management.canEarlyAccess(u1.email);
|
||||||
t.true(f2, 'should have early access');
|
t.true(f2, 'should have early access');
|
||||||
|
|
||||||
const f3 = await feature.listFeatureUsers(FeatureType.EarlyAccess);
|
const f3 = await feature.listFeatureUsers(FeatureType.EarlyAccess);
|
||||||
@@ -101,24 +137,24 @@ test('should be able to check early access', async t => {
|
|||||||
t.is(f3[0].id, u1.id, 'should be the same user');
|
t.is(f3[0].id, u1.id, 'should be the same user');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should be able revert quota', async t => {
|
test('should be able revert user feature', async t => {
|
||||||
const { auth, feature, early_access } = t.context;
|
const { auth, feature, management } = t.context;
|
||||||
const u1 = await auth.signUp('DarkSky', 'darksky@example.org', '123456');
|
const u1 = await auth.signUp('DarkSky', 'darksky@example.org', '123456');
|
||||||
|
|
||||||
const f1 = await early_access.canEarlyAccess(u1.email);
|
const f1 = await management.canEarlyAccess(u1.email);
|
||||||
t.false(f1, 'should not have early access');
|
t.false(f1, 'should not have early access');
|
||||||
|
|
||||||
await early_access.addEarlyAccess(u1.id);
|
await management.addEarlyAccess(u1.id);
|
||||||
const f2 = await early_access.canEarlyAccess(u1.email);
|
const f2 = await management.canEarlyAccess(u1.email);
|
||||||
t.true(f2, 'should have early access');
|
t.true(f2, 'should have early access');
|
||||||
const q1 = await early_access.listEarlyAccess();
|
const q1 = await management.listEarlyAccess();
|
||||||
t.is(q1.length, 1, 'should have 1 user');
|
t.is(q1.length, 1, 'should have 1 user');
|
||||||
t.is(q1[0].id, u1.id, 'should be the same user');
|
t.is(q1[0].id, u1.id, 'should be the same user');
|
||||||
|
|
||||||
await early_access.removeEarlyAccess(u1.id);
|
await management.removeEarlyAccess(u1.id);
|
||||||
const f3 = await early_access.canEarlyAccess(u1.email);
|
const f3 = await management.canEarlyAccess(u1.email);
|
||||||
t.false(f3, 'should not have early access');
|
t.false(f3, 'should not have early access');
|
||||||
const q2 = await early_access.listEarlyAccess();
|
const q2 = await management.listEarlyAccess();
|
||||||
t.is(q2.length, 0, 'should have no user');
|
t.is(q2.length, 0, 'should have no user');
|
||||||
|
|
||||||
const q3 = await feature.getUserFeatures(u1.id);
|
const q3 = await feature.getUserFeatures(u1.id);
|
||||||
@@ -127,17 +163,72 @@ test('should be able revert quota', async t => {
|
|||||||
t.is(q3[0].activated, false, 'should be deactivated');
|
t.is(q3[0].activated, false, 'should be deactivated');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should be same instance after reset the feature', async t => {
|
test('should be same instance after reset the user feature', async t => {
|
||||||
const { auth, feature, early_access } = t.context;
|
const { auth, feature, management } = t.context;
|
||||||
const u1 = await auth.signUp('DarkSky', 'darksky@example.org', '123456');
|
const u1 = await auth.signUp('DarkSky', 'darksky@example.org', '123456');
|
||||||
|
|
||||||
await early_access.addEarlyAccess(u1.id);
|
await management.addEarlyAccess(u1.id);
|
||||||
const f1 = (await feature.getUserFeatures(u1.id))[0];
|
const f1 = (await feature.getUserFeatures(u1.id))[0];
|
||||||
|
|
||||||
await early_access.removeEarlyAccess(u1.id);
|
await management.removeEarlyAccess(u1.id);
|
||||||
|
|
||||||
await early_access.addEarlyAccess(u1.id);
|
await management.addEarlyAccess(u1.id);
|
||||||
const f2 = (await feature.getUserFeatures(u1.id))[1];
|
const f2 = (await feature.getUserFeatures(u1.id))[1];
|
||||||
|
|
||||||
t.is(f1.feature, f2.feature, 'should be same instance');
|
t.is(f1.feature, f2.feature, 'should be same instance');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should be able to set workspace feature', async t => {
|
||||||
|
const { auth, feature, workspace } = t.context;
|
||||||
|
|
||||||
|
const u1 = await auth.signUp('DarkSky', 'darksky@example.org', '123456');
|
||||||
|
const w1 = await workspace.createWorkspace(u1, null);
|
||||||
|
|
||||||
|
const f1 = await feature.getWorkspaceFeatures(w1.id);
|
||||||
|
t.is(f1.length, 0, 'should be empty');
|
||||||
|
|
||||||
|
await feature.addWorkspaceFeature(w1.id, FeatureType.Copilot, 1, 'test');
|
||||||
|
|
||||||
|
const f2 = await feature.getWorkspaceFeatures(w1.id);
|
||||||
|
t.is(f2.length, 1, 'should have 1 feature');
|
||||||
|
t.is(f2[0].feature.name, FeatureType.Copilot, 'should be copilot');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should be able to check workspace feature', async t => {
|
||||||
|
const { auth, feature, workspace, management } = t.context;
|
||||||
|
const u1 = await auth.signUp('DarkSky', 'darksky@example.org', '123456');
|
||||||
|
const w1 = await workspace.createWorkspace(u1, null);
|
||||||
|
|
||||||
|
const f1 = await management.hasWorkspaceFeature(w1.id, FeatureType.Copilot);
|
||||||
|
t.false(f1, 'should not have copilot');
|
||||||
|
|
||||||
|
await management.addWorkspaceFeatures(w1.id, FeatureType.Copilot, 1, 'test');
|
||||||
|
const f2 = await management.hasWorkspaceFeature(w1.id, FeatureType.Copilot);
|
||||||
|
t.true(f2, 'should have copilot');
|
||||||
|
|
||||||
|
const f3 = await feature.listFeatureWorkspaces(FeatureType.Copilot);
|
||||||
|
t.is(f3.length, 1, 'should have 1 workspace');
|
||||||
|
t.is(f3[0].id, w1.id, 'should be the same workspace');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should be able revert workspace feature', async t => {
|
||||||
|
const { auth, feature, workspace, management } = t.context;
|
||||||
|
const u1 = await auth.signUp('DarkSky', 'darksky@example.org', '123456');
|
||||||
|
const w1 = await workspace.createWorkspace(u1, null);
|
||||||
|
|
||||||
|
const f1 = await management.hasWorkspaceFeature(w1.id, FeatureType.Copilot);
|
||||||
|
t.false(f1, 'should not have feature');
|
||||||
|
|
||||||
|
await management.addWorkspaceFeatures(w1.id, FeatureType.Copilot, 1, 'test');
|
||||||
|
const f2 = await management.hasWorkspaceFeature(w1.id, FeatureType.Copilot);
|
||||||
|
t.true(f2, 'should have feature');
|
||||||
|
|
||||||
|
await management.removeWorkspaceFeature(w1.id, FeatureType.Copilot);
|
||||||
|
const f3 = await management.hasWorkspaceFeature(w1.id, FeatureType.Copilot);
|
||||||
|
t.false(f3, 'should not have feature');
|
||||||
|
|
||||||
|
const q3 = await feature.getWorkspaceFeatures(w1.id);
|
||||||
|
t.is(q3.length, 1, 'should have 1 feature');
|
||||||
|
t.is(q3[0].feature.name, FeatureType.Copilot, 'should be copilot');
|
||||||
|
t.is(q3[0].activated, false, 'should be deactivated');
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user