feat: add workspace feature tests (#5501)

This commit is contained in:
DarkSky
2024-01-05 04:13:47 +00:00
parent 97f8927c21
commit 04ca554525
6 changed files with 217 additions and 101 deletions

View File

@@ -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;

View File

@@ -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"

View File

@@ -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")
} }

View File

@@ -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);
}
} }

View File

@@ -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!

View File

@@ -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');
});