Files
calendar/packages/features/flags/features.repository.integration-test.ts
T
Eunjae LeeGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
f4248bf20d feat: implement FeatureOptInService (#25805)
* feat: implement FeatureOptInService WIP

* clean up

* feat: consolidate feature repositories and add updateFeatureForUser

- Implement updateFeatureForUser in FeaturesRepository (similar to updateFeatureForTeam)
- Move getUserFeatureState and getTeamFeatureState from PrismaFeatureOptInRepository to FeaturesRepository
- Update FeatureOptInService to use only FeaturesRepository
- Add setUserFeatureState and setTeamFeatureState methods to FeatureOptInService
- Update _router.ts to remove PrismaFeatureOptInRepository usage
- Remove PrismaFeatureOptInRepository.ts and FeatureOptInRepositoryInterface.ts
- Update features.repository.interface.ts and features.repository.mock.ts
- Add integration tests for updateFeatureForUser, getUserFeatureState, getTeamFeatureState
- Update service.integration-test.ts to use FeaturesRepository

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* refactor: rename updateFeatureForUser to setUserFeatureState

Rename to match the convention used for setTeamFeatureState

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* refactor: return FeatureState type from getUserFeatureState and getTeamFeatureState

* fix integration tests

* clean up logics

* update services and router

* refactor: change getUserFeatureState and getTeamFeatureState to accept featureIds array

- Renamed getUserFeatureState to getUserFeatureStates
- Renamed getTeamFeatureState to getTeamFeatureStates
- Changed parameter from featureId: string to featureIds: string[]
- Changed return type from FeatureState to Record<string, FeatureState>
- Updated FeatureOptInService to use the new batch methods
- Added tests for querying multiple features in a single call
- Optimized listFeaturesForTeam to fetch all feature states in one query

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* feat: add getFeatureStateForTeams for batch querying multiple teams

- Added getFeatureStateForTeams method to query a single feature across multiple teams in one call
- Updated FeatureOptInService.resolveFeatureStateAcrossTeams to use the new batch method
- Replaces N+1 queries with a single database query for team states
- Added comprehensive integration tests for the new method

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* refactor: combine org and team state queries into single call

- Include orgId in the teamIds array passed to getFeatureStateForTeams
- Extract org state and team states from the combined result
- Reduces database queries from 3 to 2 in resolveFeatureStateAcrossTeams

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* refactor: use team.isOrganization and clarify computeEffectiveState comment

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* refactor: use MembershipRepository.findAllByUserId with isOrganization

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* feat: add featureId validation using isOptInFeature type guard

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* less queries

* add fallback value

* fix type error

* move files

* add autoOptInFeatures column

* use autoOptInFeatures flag within FeatureOptInService

* add setUserAutoOptIn and setTeamAutoOptIn

* fix computeEffectiveState logic

* rewrite computeEffectiveState

* clean up integration tests

* clean up in afterEach

* fix type error

* refactor: use FeaturesRepository methods instead of direct Prisma calls

Replace all manual userFeatures and teamFeatures Prisma operations with
the new setUserFeatureState and setTeamFeatureState repository methods.

Changes include:
- Admin handlers (assignFeatureToTeam, unassignFeatureFromTeam)
- Test fixtures and integration tests
- Playwright fixtures
- Development scripts

This ensures consistent feature flag management through the repository
pattern and supports the new tri-state semantics (enabled/disabled/inherit).

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* clean up

* fix the logic

* extract some logic into applyAutoOptIn()

* remove wrong code

* refactor: convert setUserFeatureState and setTeamFeatureState to object params with discriminated union

- Convert multiple positional parameters to single object parameter
- Use discriminated union types: assignedBy required for enabled/disabled, omitted for inherit
- Update all callers across repository, service, handlers, fixtures, and tests

* fix type error

* use Promise.all

* fix

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-06 16:55:53 +01:00

1584 lines
44 KiB
TypeScript

import { afterEach, describe, expect, it } from "vitest";
import { prisma } from "@calcom/prisma";
import type { FeatureId } from "./config";
import { FeaturesRepository } from "./features.repository";
// Helper to generate unique identifiers per test
const uniqueId = () => `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
// Store cleanup functions to ensure they run even if tests fail
const cleanupFunctions: Array<() => Promise<void>> = [];
afterEach(async () => {
for (const cleanup of cleanupFunctions) {
await cleanup();
}
cleanupFunctions.length = 0;
});
interface TestEntities {
user: { id: number };
team: { id: number };
testFeature: FeatureId;
featuresRepository: FeaturesRepository;
clearCache: () => void;
}
/**
* Creates isolated test entities (user, team) and returns them along with a cleanup function.
* Each test gets its own entities, ensuring complete isolation even when tests run in parallel.
*/
async function setup(): Promise<TestEntities> {
const id = uniqueId();
const testFeature = `test-feature-${id}` as FeatureId;
// Create test user
const user = await prisma.user.create({
data: {
email: `test-${id}@example.com`,
username: `test-${id}`,
},
});
// Create test team
const team = await prisma.team.create({
data: {
name: `Test Team ${id}`,
slug: `test-team-${id}`,
},
});
const featuresRepository = new FeaturesRepository(prisma);
// Access private clearCache method through type assertion
const clearCache = () => {
(featuresRepository as unknown as { clearCache: () => void }).clearCache();
};
// Cleanup function - automatically registered with afterEach
const cleanup = async () => {
// Clean up in correct order to respect foreign key constraints
await prisma.userFeatures.deleteMany({
where: { userId: user.id },
});
await prisma.teamFeatures.deleteMany({
where: { teamId: team.id },
});
await prisma.feature.deleteMany({
where: { slug: testFeature },
});
await prisma.membership.deleteMany({
where: { userId: user.id },
});
await prisma.team.deleteMany({
where: { id: team.id },
});
await prisma.user.delete({
where: { id: user.id },
});
};
// Register cleanup to run automatically after each test
cleanupFunctions.push(cleanup);
return {
user,
team,
testFeature,
featuresRepository,
clearCache,
};
}
describe("FeaturesRepository Integration Tests", () => {
describe("checkIfFeatureIsEnabledGlobally", () => {
it("should return false when feature is not enabled globally", async () => {
const { testFeature, featuresRepository } = await setup();
// Verify database state
const dbFeature = await prisma.feature.findUnique({
where: { slug: testFeature },
});
expect(dbFeature).toBeNull();
// First call to initialize cache
await featuresRepository.checkIfFeatureIsEnabledGlobally(testFeature);
// Second call to get actual result
const result = await featuresRepository.checkIfFeatureIsEnabledGlobally(testFeature);
expect(result).toBe(false);
});
it("should return true when feature is enabled globally", async () => {
const { testFeature, featuresRepository, clearCache } = await setup();
// Create the feature with enabled set to true
await prisma.feature.create({
data: {
slug: testFeature,
enabled: true,
type: "OPERATIONAL",
},
});
// Clear the feature flag cache
clearCache();
// Verify database state
const dbFeature = await prisma.feature.findUnique({
where: { slug: testFeature },
});
expect(dbFeature).not.toBeNull();
expect(dbFeature?.enabled).toBe(true);
// First call to initialize cache
await featuresRepository.checkIfFeatureIsEnabledGlobally(testFeature);
// Second call to get actual result
const result = await featuresRepository.checkIfFeatureIsEnabledGlobally(testFeature);
expect(result).toBe(true);
});
});
describe("checkIfUserHasFeature", () => {
it("should return false when user does not have feature", async () => {
const { user, testFeature, featuresRepository } = await setup();
const result = await featuresRepository.checkIfUserHasFeature(user.id, testFeature);
expect(result).toBe(false);
});
it("should return false when no UserFeatures row and no TeamFeatures row exist (tri-state inheritance)", async () => {
const { user, team, testFeature, featuresRepository } = await setup();
// Create the feature globally
await prisma.feature.create({
data: {
slug: testFeature,
enabled: true,
type: "OPERATIONAL",
},
});
// User is a member of a team
await prisma.membership.create({
data: {
teamId: team.id,
userId: user.id,
role: "MEMBER",
accepted: true,
},
});
// No UserFeatures row, no TeamFeatures row - should return false
const result = await featuresRepository.checkIfUserHasFeature(user.id, testFeature);
expect(result).toBe(false);
});
it("should return true when user has feature directly assigned", async () => {
const { user, testFeature, featuresRepository } = await setup();
await prisma.feature.create({
data: {
slug: testFeature,
enabled: true,
type: "OPERATIONAL",
},
});
await prisma.userFeatures.create({
data: {
userId: user.id,
featureId: testFeature,
assignedBy: "test",
enabled: true,
},
});
const result = await featuresRepository.checkIfUserHasFeature(user.id, testFeature);
expect(result).toBe(true);
});
it("should return false when user has feature with enabled=false (tri-state)", async () => {
const { user, testFeature, featuresRepository } = await setup();
await prisma.feature.create({
data: {
slug: testFeature,
enabled: true,
type: "OPERATIONAL",
},
});
await prisma.userFeatures.create({
data: {
userId: user.id,
featureId: testFeature,
enabled: false,
assignedBy: "test",
},
});
const result = await featuresRepository.checkIfUserHasFeature(user.id, testFeature);
expect(result).toBe(false);
});
it("should return false when user has enabled=false even if team has feature (tri-state blocks inheritance)", async () => {
const { user, team, testFeature, featuresRepository } = await setup();
await prisma.feature.create({
data: {
slug: testFeature,
enabled: true,
type: "OPERATIONAL",
},
});
// Team has the feature enabled
await prisma.teamFeatures.create({
data: {
teamId: team.id,
featureId: testFeature,
enabled: true,
assignedBy: "test",
},
});
// User is a member of the team
await prisma.membership.create({
data: {
teamId: team.id,
userId: user.id,
role: "MEMBER",
accepted: true,
},
});
// But user has explicitly disabled the feature
await prisma.userFeatures.create({
data: {
userId: user.id,
featureId: testFeature,
enabled: false,
assignedBy: "test",
},
});
const result = await featuresRepository.checkIfUserHasFeature(user.id, testFeature);
expect(result).toBe(false);
});
it("should return true when user belongs to team with feature", async () => {
const { user, team, testFeature, featuresRepository } = await setup();
await prisma.feature.create({
data: {
slug: testFeature,
enabled: true,
type: "OPERATIONAL",
},
});
await prisma.teamFeatures.create({
data: {
teamId: team.id,
featureId: testFeature,
assignedBy: "test",
enabled: true,
},
});
await prisma.membership.create({
data: {
teamId: team.id,
userId: user.id,
role: "MEMBER",
accepted: true,
},
});
const result = await featuresRepository.checkIfUserHasFeature(user.id, testFeature);
expect(result).toBe(true);
});
it("should return false when user belongs to team with feature disabled (tri-state)", async () => {
const { user, team, testFeature, featuresRepository } = await setup();
await prisma.feature.create({
data: {
slug: testFeature,
enabled: true,
type: "OPERATIONAL",
},
});
// Team has the feature explicitly disabled
await prisma.teamFeatures.create({
data: {
teamId: team.id,
featureId: testFeature,
enabled: false,
assignedBy: "test",
},
});
await prisma.membership.create({
data: {
teamId: team.id,
userId: user.id,
role: "MEMBER",
accepted: true,
},
});
const result = await featuresRepository.checkIfUserHasFeature(user.id, testFeature);
expect(result).toBe(false);
});
it("should return true when user belongs to team where parent team has feature", async () => {
const { user, team, testFeature, featuresRepository, clearCache } = await setup();
// Create an organization
const org = await prisma.team.create({
data: {
name: `Test Org ${uniqueId()}`,
slug: `test-org-${uniqueId()}`,
isOrganization: true,
},
});
// Make the test team part of the organization
await prisma.team.update({
where: { id: team.id },
data: { parentId: org.id },
});
// Create the feature
await prisma.feature.create({
data: {
slug: testFeature,
enabled: true,
type: "OPERATIONAL",
},
});
// Clear the feature flag cache
clearCache();
// Assign feature to the organization
await prisma.teamFeatures.create({
data: {
teamId: org.id,
featureId: testFeature,
assignedBy: "test",
enabled: true,
},
});
// Create membership
await prisma.membership.create({
data: {
teamId: team.id,
userId: user.id,
role: "MEMBER",
accepted: true,
},
});
const result = await featuresRepository.checkIfUserHasFeature(user.id, testFeature);
expect(result).toBe(true);
// Clean up organization
await prisma.teamFeatures.deleteMany({
where: { teamId: org.id },
});
await prisma.team.update({
where: { id: team.id },
data: { parentId: null },
});
await prisma.team.delete({
where: { id: org.id },
});
});
});
describe("checkIfTeamHasFeature", () => {
it("should return false when team does not have feature", async () => {
const { team, testFeature, featuresRepository } = await setup();
const result = await featuresRepository.checkIfTeamHasFeature(team.id, testFeature);
expect(result).toBe(false);
});
it("should return true when team has feature directly assigned", async () => {
const { team, testFeature, featuresRepository } = await setup();
await prisma.feature.create({
data: {
slug: testFeature,
enabled: true,
type: "OPERATIONAL",
},
});
await prisma.teamFeatures.create({
data: {
teamId: team.id,
featureId: testFeature,
assignedBy: "test",
enabled: true,
},
});
const result = await featuresRepository.checkIfTeamHasFeature(team.id, testFeature);
expect(result).toBe(true);
});
it("should return false when team has feature with enabled=false (tri-state)", async () => {
const { team, testFeature, featuresRepository } = await setup();
await prisma.feature.create({
data: {
slug: testFeature,
enabled: true,
type: "OPERATIONAL",
},
});
await prisma.teamFeatures.create({
data: {
teamId: team.id,
featureId: testFeature,
enabled: false,
assignedBy: "test",
},
});
const result = await featuresRepository.checkIfTeamHasFeature(team.id, testFeature);
expect(result).toBe(false);
});
it("should return true when parent team has feature", async () => {
const { team, testFeature, featuresRepository } = await setup();
const parentTeam = await prisma.team.create({
data: {
name: `Parent Team ${uniqueId()}`,
slug: `parent-team-${uniqueId()}`,
},
});
await prisma.team.update({
where: { id: team.id },
data: { parentId: parentTeam.id },
});
await prisma.feature.create({
data: {
slug: testFeature,
enabled: true,
type: "OPERATIONAL",
},
});
await prisma.teamFeatures.create({
data: {
teamId: parentTeam.id,
featureId: testFeature,
assignedBy: "test",
enabled: true,
},
});
const result = await featuresRepository.checkIfTeamHasFeature(team.id, testFeature);
expect(result).toBe(true);
// Clean up parent team
await prisma.teamFeatures.deleteMany({
where: { teamId: parentTeam.id },
});
await prisma.team.update({
where: { id: team.id },
data: { parentId: null },
});
await prisma.team.delete({
where: { id: parentTeam.id },
});
});
it("should return false when parent has feature with enabled=false (tri-state)", async () => {
const { team, testFeature, featuresRepository } = await setup();
const parentTeam = await prisma.team.create({
data: {
name: `Parent Team ${uniqueId()}`,
slug: `parent-team-${uniqueId()}`,
},
});
await prisma.team.update({
where: { id: team.id },
data: { parentId: parentTeam.id },
});
await prisma.feature.create({
data: {
slug: testFeature,
enabled: true,
type: "OPERATIONAL",
},
});
// Parent has the feature explicitly disabled
await prisma.teamFeatures.create({
data: {
teamId: parentTeam.id,
featureId: testFeature,
enabled: false,
assignedBy: "test",
},
});
const result = await featuresRepository.checkIfTeamHasFeature(team.id, testFeature);
expect(result).toBe(false);
// Clean up parent team
await prisma.teamFeatures.deleteMany({
where: { teamId: parentTeam.id },
});
await prisma.team.update({
where: { id: team.id },
data: { parentId: null },
});
await prisma.team.delete({
where: { id: parentTeam.id },
});
});
});
describe("checkIfUserHasFeatureNonHierarchical", () => {
it("should return false when user does not have feature", async () => {
const { user, testFeature, featuresRepository } = await setup();
const result = await featuresRepository.checkIfUserHasFeatureNonHierarchical(user.id, testFeature);
expect(result).toBe(false);
});
it("should return true when user has feature directly assigned", async () => {
const { user, testFeature, featuresRepository } = await setup();
await prisma.feature.create({
data: {
slug: testFeature,
enabled: true,
type: "OPERATIONAL",
},
});
await prisma.userFeatures.create({
data: {
userId: user.id,
featureId: testFeature,
enabled: true,
assignedBy: "test",
},
});
const result = await featuresRepository.checkIfUserHasFeatureNonHierarchical(user.id, testFeature);
expect(result).toBe(true);
});
it("should return false when user has feature with enabled=false (tri-state)", async () => {
const { user, testFeature, featuresRepository } = await setup();
await prisma.feature.create({
data: {
slug: testFeature,
enabled: true,
type: "OPERATIONAL",
},
});
await prisma.userFeatures.create({
data: {
userId: user.id,
featureId: testFeature,
enabled: false,
assignedBy: "test",
},
});
const result = await featuresRepository.checkIfUserHasFeatureNonHierarchical(user.id, testFeature);
expect(result).toBe(false);
});
it("should return true when user belongs to direct team with feature (non-hierarchical)", async () => {
const { user, team, testFeature, featuresRepository } = await setup();
await prisma.feature.create({
data: {
slug: testFeature,
enabled: true,
type: "OPERATIONAL",
},
});
await prisma.teamFeatures.create({
data: {
teamId: team.id,
featureId: testFeature,
enabled: true,
assignedBy: "test",
},
});
await prisma.membership.create({
data: {
teamId: team.id,
userId: user.id,
role: "MEMBER",
accepted: true,
},
});
const result = await featuresRepository.checkIfUserHasFeatureNonHierarchical(user.id, testFeature);
expect(result).toBe(true);
});
it("should return false when user belongs to direct team with feature disabled (tri-state)", async () => {
const { user, team, testFeature, featuresRepository } = await setup();
await prisma.feature.create({
data: {
slug: testFeature,
enabled: true,
type: "OPERATIONAL",
},
});
await prisma.teamFeatures.create({
data: {
teamId: team.id,
featureId: testFeature,
enabled: false,
assignedBy: "test",
},
});
await prisma.membership.create({
data: {
teamId: team.id,
userId: user.id,
role: "MEMBER",
accepted: true,
},
});
const result = await featuresRepository.checkIfUserHasFeatureNonHierarchical(user.id, testFeature);
expect(result).toBe(false);
});
});
describe("setTeamFeatureState", () => {
it("should create a new TeamFeatures row with enabled=true", async () => {
const { team, testFeature, featuresRepository } = await setup();
await prisma.feature.create({
data: {
slug: testFeature,
enabled: true,
type: "OPERATIONAL",
},
});
await featuresRepository.setTeamFeatureState({
teamId: team.id,
featureId: testFeature,
state: "enabled",
assignedBy: "test-assigner",
});
const teamFeature = await prisma.teamFeatures.findUnique({
where: {
teamId_featureId: {
teamId: team.id,
featureId: testFeature,
},
},
});
expect(teamFeature).not.toBeNull();
expect(teamFeature?.enabled).toBe(true);
expect(teamFeature?.assignedBy).toBe("test-assigner");
});
it("should update existing TeamFeatures row to enabled=true", async () => {
const { team, testFeature, featuresRepository } = await setup();
await prisma.feature.create({
data: {
slug: testFeature,
enabled: true,
type: "OPERATIONAL",
},
});
// Create with enabled=false first
await prisma.teamFeatures.create({
data: {
teamId: team.id,
featureId: testFeature,
enabled: false,
assignedBy: "original-assigner",
},
});
// Now enable the feature
await featuresRepository.setTeamFeatureState({
teamId: team.id,
featureId: testFeature,
state: "enabled",
assignedBy: "new-assigner",
});
const teamFeature = await prisma.teamFeatures.findUnique({
where: {
teamId_featureId: {
teamId: team.id,
featureId: testFeature,
},
},
});
expect(teamFeature).not.toBeNull();
expect(teamFeature?.enabled).toBe(true);
expect(teamFeature?.assignedBy).toBe("new-assigner");
});
it("should create a new TeamFeatures row with enabled=false", async () => {
const { team, testFeature, featuresRepository } = await setup();
await prisma.feature.create({
data: {
slug: testFeature,
enabled: true,
type: "OPERATIONAL",
},
});
await featuresRepository.setTeamFeatureState({
teamId: team.id,
featureId: testFeature,
state: "disabled",
assignedBy: "test-assigner",
});
const teamFeature = await prisma.teamFeatures.findUnique({
where: {
teamId_featureId: {
teamId: team.id,
featureId: testFeature,
},
},
});
expect(teamFeature).not.toBeNull();
expect(teamFeature?.enabled).toBe(false);
expect(teamFeature?.assignedBy).toBe("test-assigner");
});
it("should update existing TeamFeatures row to enabled=false", async () => {
const { team, testFeature, featuresRepository } = await setup();
await prisma.feature.create({
data: {
slug: testFeature,
enabled: true,
type: "OPERATIONAL",
},
});
// Create with enabled=true first
await prisma.teamFeatures.create({
data: {
teamId: team.id,
featureId: testFeature,
enabled: true,
assignedBy: "original-assigner",
},
});
// Now disable the feature
await featuresRepository.setTeamFeatureState({
teamId: team.id,
featureId: testFeature,
state: "disabled",
assignedBy: "new-assigner",
});
const teamFeature = await prisma.teamFeatures.findUnique({
where: {
teamId_featureId: {
teamId: team.id,
featureId: testFeature,
},
},
});
expect(teamFeature).not.toBeNull();
expect(teamFeature?.enabled).toBe(false);
expect(teamFeature?.assignedBy).toBe("new-assigner");
});
it("should delete TeamFeatures row when state is inherit", async () => {
const { team, testFeature, featuresRepository } = await setup();
await prisma.feature.create({
data: {
slug: testFeature,
enabled: true,
type: "OPERATIONAL",
},
});
// Create a TeamFeatures row first
await prisma.teamFeatures.create({
data: {
teamId: team.id,
featureId: testFeature,
enabled: true,
assignedBy: "original-assigner",
},
});
// Verify the row exists
const beforeDelete = await prisma.teamFeatures.findUnique({
where: {
teamId_featureId: {
teamId: team.id,
featureId: testFeature,
},
},
});
expect(beforeDelete).not.toBeNull();
// Now set to inherit - should delete the row
await featuresRepository.setTeamFeatureState({
teamId: team.id,
featureId: testFeature,
state: "inherit",
});
const teamFeature = await prisma.teamFeatures.findUnique({
where: {
teamId_featureId: {
teamId: team.id,
featureId: testFeature,
},
},
});
expect(teamFeature).toBeNull();
});
it("should handle inherit state when no TeamFeatures row exists", async () => {
const { team, testFeature, featuresRepository } = await setup();
await prisma.feature.create({
data: {
slug: testFeature,
enabled: true,
type: "OPERATIONAL",
},
});
// Call inherit without any existing row - should not throw
await featuresRepository.setTeamFeatureState({
teamId: team.id,
featureId: testFeature,
state: "inherit",
});
const teamFeature = await prisma.teamFeatures.findUnique({
where: {
teamId_featureId: {
teamId: team.id,
featureId: testFeature,
},
},
});
expect(teamFeature).toBeNull();
});
});
describe("getTeamsWithFeatureEnabled", () => {
it("should return empty array when no teams have the feature", async () => {
const { testFeature, featuresRepository, clearCache } = await setup();
await prisma.feature.create({
data: {
slug: testFeature,
enabled: true,
type: "OPERATIONAL",
},
});
clearCache();
const result = await featuresRepository.getTeamsWithFeatureEnabled(testFeature);
expect(result).toEqual([]);
});
it("should return teams with enabled=true only", async () => {
const { team, testFeature, featuresRepository, clearCache } = await setup();
await prisma.feature.create({
data: {
slug: testFeature,
enabled: true,
type: "OPERATIONAL",
},
});
// Create a second team
const secondTeam = await prisma.team.create({
data: {
name: `Second Team ${uniqueId()}`,
slug: `second-team-${uniqueId()}`,
},
});
// team has feature enabled
await prisma.teamFeatures.create({
data: {
teamId: team.id,
featureId: testFeature,
enabled: true,
assignedBy: "test",
},
});
// secondTeam has feature disabled
await prisma.teamFeatures.create({
data: {
teamId: secondTeam.id,
featureId: testFeature,
enabled: false,
assignedBy: "test",
},
});
clearCache();
const result = await featuresRepository.getTeamsWithFeatureEnabled(testFeature);
expect(result).toContain(team.id);
expect(result).not.toContain(secondTeam.id);
// Clean up second team
await prisma.teamFeatures.deleteMany({
where: { teamId: secondTeam.id },
});
await prisma.team.delete({
where: { id: secondTeam.id },
});
});
it("should return empty array when feature is not globally enabled", async () => {
const { team, testFeature, featuresRepository, clearCache } = await setup();
await prisma.feature.create({
data: {
slug: testFeature,
enabled: false, // Globally disabled
type: "OPERATIONAL",
},
});
await prisma.teamFeatures.create({
data: {
teamId: team.id,
featureId: testFeature,
enabled: true,
assignedBy: "test",
},
});
clearCache();
const result = await featuresRepository.getTeamsWithFeatureEnabled(testFeature);
expect(result).toEqual([]);
});
});
describe("setUserFeatureState", () => {
it("should create a new UserFeatures row with enabled=true when state is 'enabled'", async () => {
const { user, testFeature, featuresRepository } = await setup();
await prisma.feature.create({
data: {
slug: testFeature,
enabled: true,
type: "OPERATIONAL",
},
});
await featuresRepository.setUserFeatureState({
userId: user.id,
featureId: testFeature,
state: "enabled",
assignedBy: "test-assigner",
});
const userFeature = await prisma.userFeatures.findFirst({
where: {
userId: user.id,
featureId: testFeature,
},
});
expect(userFeature).not.toBeNull();
expect(userFeature?.enabled).toBe(true);
expect(userFeature?.assignedBy).toBe("test-assigner");
});
it("should create a new UserFeatures row with enabled=false when state is 'disabled'", async () => {
const { user, testFeature, featuresRepository } = await setup();
await prisma.feature.create({
data: {
slug: testFeature,
enabled: true,
type: "OPERATIONAL",
},
});
await featuresRepository.setUserFeatureState({
userId: user.id,
featureId: testFeature,
state: "disabled",
assignedBy: "test-assigner",
});
const userFeature = await prisma.userFeatures.findFirst({
where: {
userId: user.id,
featureId: testFeature,
},
});
expect(userFeature).not.toBeNull();
expect(userFeature?.enabled).toBe(false);
expect(userFeature?.assignedBy).toBe("test-assigner");
});
it("should update existing UserFeatures row", async () => {
const { user, testFeature, featuresRepository } = await setup();
await prisma.feature.create({
data: {
slug: testFeature,
enabled: true,
type: "OPERATIONAL",
},
});
// Create with enabled=false first
await prisma.userFeatures.create({
data: {
userId: user.id,
featureId: testFeature,
enabled: false,
assignedBy: "original-assigner",
},
});
// Now enable the feature
await featuresRepository.setUserFeatureState({
userId: user.id,
featureId: testFeature,
state: "enabled",
assignedBy: "new-assigner",
});
const userFeature = await prisma.userFeatures.findFirst({
where: {
userId: user.id,
featureId: testFeature,
},
});
expect(userFeature).not.toBeNull();
expect(userFeature?.enabled).toBe(true);
expect(userFeature?.assignedBy).toBe("new-assigner");
});
it("should delete the row when state is 'inherit'", async () => {
const { user, testFeature, featuresRepository } = await setup();
await prisma.feature.create({
data: {
slug: testFeature,
enabled: true,
type: "OPERATIONAL",
},
});
// Create a row first
await prisma.userFeatures.create({
data: {
userId: user.id,
featureId: testFeature,
enabled: true,
assignedBy: "test",
},
});
// Set to inherit (should delete)
await featuresRepository.setUserFeatureState({
userId: user.id,
featureId: testFeature,
state: "inherit",
});
const userFeature = await prisma.userFeatures.findFirst({
where: {
userId: user.id,
featureId: testFeature,
},
});
expect(userFeature).toBeNull();
});
});
describe("getUserFeatureStates", () => {
it("should return 'inherit' when user has no feature row", async () => {
const { user, testFeature, featuresRepository } = await setup();
const result = await featuresRepository.getUserFeatureStates({
userId: user.id,
featureIds: [testFeature],
});
expect(result[testFeature]).toBe("inherit");
});
it("should return 'enabled' when user has feature enabled", async () => {
const { user, testFeature, featuresRepository } = await setup();
await prisma.feature.create({
data: {
slug: testFeature,
enabled: true,
type: "OPERATIONAL",
},
});
await prisma.userFeatures.create({
data: {
userId: user.id,
featureId: testFeature,
enabled: true,
assignedBy: "test",
},
});
const result = await featuresRepository.getUserFeatureStates({
userId: user.id,
featureIds: [testFeature],
});
expect(result[testFeature]).toBe("enabled");
});
it("should return 'disabled' when user has feature disabled", async () => {
const { user, testFeature, featuresRepository } = await setup();
await prisma.feature.create({
data: {
slug: testFeature,
enabled: true,
type: "OPERATIONAL",
},
});
await prisma.userFeatures.create({
data: {
userId: user.id,
featureId: testFeature,
enabled: false,
assignedBy: "test",
},
});
const result = await featuresRepository.getUserFeatureStates({
userId: user.id,
featureIds: [testFeature],
});
expect(result[testFeature]).toBe("disabled");
});
it("should return states for multiple features in a single query", async () => {
const { user, testFeature, featuresRepository } = await setup();
const testFeature2 = `${testFeature}-2` as FeatureId;
await prisma.feature.createMany({
data: [
{ slug: testFeature, enabled: true, type: "OPERATIONAL" },
{ slug: testFeature2, enabled: true, type: "OPERATIONAL" },
],
});
await prisma.userFeatures.createMany({
data: [
{ userId: user.id, featureId: testFeature, enabled: true, assignedBy: "test" },
{ userId: user.id, featureId: testFeature2, enabled: false, assignedBy: "test" },
],
});
const result = await featuresRepository.getUserFeatureStates({
userId: user.id,
featureIds: [testFeature, testFeature2],
});
expect(result[testFeature]).toBe("enabled");
expect(result[testFeature2]).toBe("disabled");
// Clean up extra feature
await prisma.userFeatures.deleteMany({
where: { userId: user.id, featureId: testFeature2 },
});
await prisma.feature.deleteMany({
where: { slug: testFeature2 },
});
});
});
describe("getTeamsFeatureStates", () => {
it("should return empty object for empty teamIds array", async () => {
const { testFeature, featuresRepository } = await setup();
const result = await featuresRepository.getTeamsFeatureStates({
teamIds: [],
featureIds: [testFeature],
});
expect(result).toEqual({});
});
it("should return inherit (missing key) when team has no feature row", async () => {
const { team, testFeature, featuresRepository } = await setup();
const result = await featuresRepository.getTeamsFeatureStates({
teamIds: [team.id],
featureIds: [testFeature],
});
// Teams without explicit state are not in the result (caller should default to 'inherit')
expect(result[testFeature]?.[team.id]).toBeUndefined();
});
it("should return 'enabled' when team has feature enabled", async () => {
const { team, testFeature, featuresRepository } = await setup();
await prisma.feature.create({
data: { slug: testFeature, enabled: true, type: "OPERATIONAL" },
});
await prisma.teamFeatures.create({
data: {
teamId: team.id,
featureId: testFeature,
enabled: true,
assignedBy: "test",
},
});
const result = await featuresRepository.getTeamsFeatureStates({
teamIds: [team.id],
featureIds: [testFeature],
});
expect(result[testFeature]?.[team.id]).toBe("enabled");
});
it("should return 'disabled' when team has feature disabled", async () => {
const { team, testFeature, featuresRepository } = await setup();
await prisma.feature.create({
data: { slug: testFeature, enabled: true, type: "OPERATIONAL" },
});
await prisma.teamFeatures.create({
data: {
teamId: team.id,
featureId: testFeature,
enabled: false,
assignedBy: "test",
},
});
const result = await featuresRepository.getTeamsFeatureStates({
teamIds: [team.id],
featureIds: [testFeature],
});
expect(result[testFeature]?.[team.id]).toBe("disabled");
});
it("should return states for multiple teams in a single query", async () => {
const { team, testFeature, featuresRepository } = await setup();
const secondTeam = await prisma.team.create({
data: { name: `Second Test Team ${uniqueId()}`, slug: `second-test-team-${uniqueId()}` },
});
await prisma.feature.create({
data: { slug: testFeature, enabled: true, type: "OPERATIONAL" },
});
await prisma.teamFeatures.createMany({
data: [
{ teamId: team.id, featureId: testFeature, enabled: true, assignedBy: "test" },
{ teamId: secondTeam.id, featureId: testFeature, enabled: false, assignedBy: "test" },
],
});
const result = await featuresRepository.getTeamsFeatureStates({
teamIds: [team.id, secondTeam.id],
featureIds: [testFeature],
});
expect(result[testFeature]?.[team.id]).toBe("enabled");
expect(result[testFeature]?.[secondTeam.id]).toBe("disabled");
// Clean up
await prisma.teamFeatures.deleteMany({
where: { teamId: secondTeam.id },
});
await prisma.team.delete({
where: { id: secondTeam.id },
});
});
it("should only return teams with explicit state, not all requested teams", async () => {
const { team, testFeature, featuresRepository } = await setup();
const secondTeam = await prisma.team.create({
data: { name: `Second Test Team ${uniqueId()}`, slug: `second-test-team-${uniqueId()}` },
});
await prisma.feature.create({
data: { slug: testFeature, enabled: true, type: "OPERATIONAL" },
});
// Only set feature for team, not secondTeam
await prisma.teamFeatures.create({
data: {
teamId: team.id,
featureId: testFeature,
enabled: true,
assignedBy: "test",
},
});
const result = await featuresRepository.getTeamsFeatureStates({
teamIds: [team.id, secondTeam.id],
featureIds: [testFeature],
});
expect(result[testFeature]?.[team.id]).toBe("enabled");
expect(result[testFeature]?.[secondTeam.id]).toBeUndefined(); // Not in result, caller defaults to 'inherit'
// Clean up
await prisma.team.delete({
where: { id: secondTeam.id },
});
});
it("should return states for multiple features across teams", async () => {
const { team, testFeature, featuresRepository } = await setup();
const secondFeature = `${testFeature}-two` as FeatureId;
await prisma.feature.createMany({
data: [
{ slug: testFeature, enabled: true, type: "OPERATIONAL" },
{ slug: secondFeature, enabled: true, type: "OPERATIONAL" },
],
});
await prisma.teamFeatures.createMany({
data: [
{ teamId: team.id, featureId: testFeature, enabled: true, assignedBy: "test" },
{ teamId: team.id, featureId: secondFeature, enabled: false, assignedBy: "test" },
],
});
const result = await featuresRepository.getTeamsFeatureStates({
teamIds: [team.id],
featureIds: [testFeature, secondFeature],
});
expect(result[testFeature]?.[team.id]).toBe("enabled");
expect(result[secondFeature]?.[team.id]).toBe("disabled");
await prisma.teamFeatures.deleteMany({
where: { teamId: team.id, featureId: { in: [testFeature, secondFeature] } },
});
await prisma.feature.deleteMany({
where: { slug: secondFeature },
});
});
});
describe("getUserAutoOptIn", () => {
it("should return false when user has autoOptInFeatures=false (default)", async () => {
const { user, featuresRepository } = await setup();
const result = await featuresRepository.getUserAutoOptIn(user.id);
expect(result).toBe(false);
});
it("should return true when user has autoOptInFeatures=true", async () => {
const { user, featuresRepository } = await setup();
await prisma.user.update({
where: { id: user.id },
data: { autoOptInFeatures: true },
});
const result = await featuresRepository.getUserAutoOptIn(user.id);
expect(result).toBe(true);
});
it("should return false for non-existent user", async () => {
const { featuresRepository } = await setup();
const result = await featuresRepository.getUserAutoOptIn(999999);
expect(result).toBe(false);
});
});
describe("getTeamsAutoOptIn", () => {
it("should return empty object for empty teamIds array", async () => {
const { featuresRepository } = await setup();
const result = await featuresRepository.getTeamsAutoOptIn([]);
expect(result).toEqual({});
});
it("should return false for team with autoOptInFeatures=false (default)", async () => {
const { team, featuresRepository } = await setup();
const result = await featuresRepository.getTeamsAutoOptIn([team.id]);
expect(result[team.id]).toBe(false);
});
it("should return true for team with autoOptInFeatures=true", async () => {
const { team, featuresRepository } = await setup();
await prisma.team.update({
where: { id: team.id },
data: { autoOptInFeatures: true },
});
const result = await featuresRepository.getTeamsAutoOptIn([team.id]);
expect(result[team.id]).toBe(true);
});
it("should return values for multiple teams in a single query", async () => {
const { team, featuresRepository } = await setup();
const secondTeam = await prisma.team.create({
data: {
name: `Second Team ${uniqueId()}`,
slug: `second-team-auto-opt-in-${uniqueId()}`,
autoOptInFeatures: true,
},
});
// team has autoOptInFeatures=false (default)
// secondTeam has autoOptInFeatures=true
const result = await featuresRepository.getTeamsAutoOptIn([team.id, secondTeam.id]);
expect(result[team.id]).toBe(false);
expect(result[secondTeam.id]).toBe(true);
// Clean up
await prisma.team.delete({
where: { id: secondTeam.id },
});
});
it("should not include non-existent teams in result", async () => {
const { team, featuresRepository } = await setup();
const result = await featuresRepository.getTeamsAutoOptIn([team.id, 999999]);
expect(result[team.id]).toBe(false);
expect(result[999999]).toBeUndefined();
});
});
describe("setUserAutoOptIn", () => {
it("should set autoOptInFeatures to true", async () => {
const { user, featuresRepository } = await setup();
// Ensure user starts with false
const before = await featuresRepository.getUserAutoOptIn(user.id);
expect(before).toBe(false);
await featuresRepository.setUserAutoOptIn(user.id, true);
const after = await featuresRepository.getUserAutoOptIn(user.id);
expect(after).toBe(true);
});
it("should set autoOptInFeatures to false", async () => {
const { user, featuresRepository } = await setup();
// First set to true
await featuresRepository.setUserAutoOptIn(user.id, true);
const before = await featuresRepository.getUserAutoOptIn(user.id);
expect(before).toBe(true);
// Now set to false
await featuresRepository.setUserAutoOptIn(user.id, false);
const after = await featuresRepository.getUserAutoOptIn(user.id);
expect(after).toBe(false);
});
});
describe("setTeamAutoOptIn", () => {
it("should set autoOptInFeatures to true", async () => {
const { team, featuresRepository } = await setup();
// Ensure team starts with false
const before = await featuresRepository.getTeamsAutoOptIn([team.id]);
expect(before[team.id]).toBe(false);
await featuresRepository.setTeamAutoOptIn(team.id, true);
const after = await featuresRepository.getTeamsAutoOptIn([team.id]);
expect(after[team.id]).toBe(true);
});
it("should set autoOptInFeatures to false", async () => {
const { team, featuresRepository } = await setup();
// First set to true
await featuresRepository.setTeamAutoOptIn(team.id, true);
const before = await featuresRepository.getTeamsAutoOptIn([team.id]);
expect(before[team.id]).toBe(true);
// Now set to false
await featuresRepository.setTeamAutoOptIn(team.id, false);
const after = await featuresRepository.getTeamsAutoOptIn([team.id]);
expect(after[team.id]).toBe(false);
});
});
});