f4248bf20d
* 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>
909 lines
28 KiB
TypeScript
909 lines
28 KiB
TypeScript
import { afterEach, describe, expect, it } from "vitest";
|
|
|
|
import type { FeatureId } from "@calcom/features/flags/config";
|
|
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
|
import { prisma } from "@calcom/prisma";
|
|
|
|
import { FeatureOptInService } from "./FeatureOptInService";
|
|
|
|
// 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;
|
|
});
|
|
|
|
// Access private clearCache method through type assertion
|
|
const clearFeaturesCache = (repo: FeaturesRepository) => {
|
|
(repo as unknown as { clearCache: () => void }).clearCache();
|
|
};
|
|
|
|
interface TestEntities {
|
|
user: { id: number };
|
|
org: { id: number };
|
|
team: { id: number };
|
|
team2: { id: number };
|
|
featuresRepository: FeaturesRepository;
|
|
service: FeatureOptInService;
|
|
createdFeatures: string[];
|
|
setupFeature: (enabled?: boolean) => Promise<FeatureId>;
|
|
}
|
|
|
|
/**
|
|
* Creates isolated test entities (user, org, teams) 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 createdFeatures: string[] = [];
|
|
|
|
// Create test user
|
|
const user = await prisma.user.create({
|
|
data: {
|
|
email: `test-opt-in-${id}@example.com`,
|
|
username: `test-opt-in-${id}`,
|
|
},
|
|
});
|
|
|
|
// Create test org
|
|
const org = await prisma.team.create({
|
|
data: {
|
|
name: `Test OptIn Org ${id}`,
|
|
slug: `test-opt-in-org-${id}`,
|
|
isOrganization: true,
|
|
},
|
|
});
|
|
|
|
// Create test teams
|
|
const team = await prisma.team.create({
|
|
data: {
|
|
name: `Test OptIn Team ${id}`,
|
|
slug: `test-opt-in-team-${id}`,
|
|
parentId: org.id,
|
|
},
|
|
});
|
|
|
|
const team2 = await prisma.team.create({
|
|
data: {
|
|
name: `Test OptIn Team 2 ${id}`,
|
|
slug: `test-opt-in-team-2-${id}`,
|
|
parentId: org.id,
|
|
},
|
|
});
|
|
|
|
const featuresRepository = new FeaturesRepository(prisma);
|
|
const service = new FeatureOptInService(featuresRepository);
|
|
|
|
// Helper to create a feature for a test and track it for cleanup
|
|
const setupFeature = async (enabled = true): Promise<FeatureId> => {
|
|
const featureSlug = `test-opt-in-feature-${uniqueId()}` as FeatureId;
|
|
createdFeatures.push(featureSlug);
|
|
await prisma.feature.create({
|
|
data: {
|
|
slug: featureSlug,
|
|
enabled,
|
|
type: "EXPERIMENT",
|
|
},
|
|
});
|
|
// Clear cache after creating feature so getAllFeatures() returns fresh data
|
|
clearFeaturesCache(featuresRepository);
|
|
return featureSlug;
|
|
};
|
|
|
|
// 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: { in: [team.id, team2.id, org.id] } },
|
|
});
|
|
await prisma.feature.deleteMany({
|
|
where: { slug: { in: createdFeatures } },
|
|
});
|
|
await prisma.membership.deleteMany({
|
|
where: { userId: user.id },
|
|
});
|
|
await prisma.team.deleteMany({
|
|
where: { id: { in: [team.id, team2.id] } },
|
|
});
|
|
await prisma.team.deleteMany({
|
|
where: { id: org.id },
|
|
});
|
|
await prisma.user.delete({
|
|
where: { id: user.id },
|
|
});
|
|
};
|
|
|
|
// Register cleanup to run automatically after each test
|
|
cleanupFunctions.push(cleanup);
|
|
|
|
return {
|
|
user,
|
|
org,
|
|
team,
|
|
team2,
|
|
featuresRepository,
|
|
service,
|
|
createdFeatures,
|
|
setupFeature,
|
|
};
|
|
}
|
|
|
|
describe("FeatureOptInService Integration Tests", () => {
|
|
describe("resolveFeatureStatesAcrossTeams", () => {
|
|
describe("global feature disabled", () => {
|
|
it("should return effectiveEnabled=false regardless of other states", async () => {
|
|
const { user, org, team, service, setupFeature } = await setup();
|
|
const testFeature = await setupFeature(false);
|
|
|
|
const statusMap = await service.resolveFeatureStatesAcrossTeams({
|
|
userId: user.id,
|
|
orgId: org.id,
|
|
teamIds: [team.id],
|
|
featureIds: [testFeature],
|
|
});
|
|
const status = statusMap[testFeature];
|
|
|
|
expect(status.effectiveEnabled).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("org disabled", () => {
|
|
it("should return effectiveEnabled=false regardless of team and user states", async () => {
|
|
const { user, org, team, service, setupFeature, featuresRepository } = await setup();
|
|
const testFeature = await setupFeature(true);
|
|
|
|
// Org explicitly disables
|
|
await featuresRepository.setTeamFeatureState({
|
|
teamId: org.id,
|
|
featureId: testFeature,
|
|
state: "disabled",
|
|
assignedBy: "test",
|
|
});
|
|
|
|
// Team enables
|
|
await featuresRepository.setTeamFeatureState({
|
|
teamId: team.id,
|
|
featureId: testFeature,
|
|
state: "enabled",
|
|
assignedBy: "test",
|
|
});
|
|
|
|
// User enables
|
|
await featuresRepository.setUserFeatureState({
|
|
userId: user.id,
|
|
featureId: testFeature,
|
|
state: "enabled",
|
|
assignedBy: "test",
|
|
});
|
|
|
|
const statusMap = await service.resolveFeatureStatesAcrossTeams({
|
|
userId: user.id,
|
|
orgId: org.id,
|
|
teamIds: [team.id],
|
|
featureIds: [testFeature],
|
|
});
|
|
const status = statusMap[testFeature];
|
|
|
|
expect(status.orgState).toBe("disabled");
|
|
expect(status.effectiveEnabled).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("org enabled", () => {
|
|
it("should return effectiveEnabled=false when all teams are disabled", async () => {
|
|
const { user, org, team, team2, service, setupFeature, featuresRepository } = await setup();
|
|
const testFeature = await setupFeature(true);
|
|
|
|
// Org enables
|
|
await featuresRepository.setTeamFeatureState({
|
|
teamId: org.id,
|
|
featureId: testFeature,
|
|
state: "enabled",
|
|
assignedBy: "test",
|
|
});
|
|
|
|
// Both teams disable
|
|
await Promise.all([
|
|
featuresRepository.setTeamFeatureState({
|
|
teamId: team.id,
|
|
featureId: testFeature,
|
|
state: "disabled",
|
|
assignedBy: "test",
|
|
}),
|
|
featuresRepository.setTeamFeatureState({
|
|
teamId: team2.id,
|
|
featureId: testFeature,
|
|
state: "disabled",
|
|
assignedBy: "test",
|
|
}),
|
|
]);
|
|
|
|
// User enables
|
|
await featuresRepository.setUserFeatureState({
|
|
userId: user.id,
|
|
featureId: testFeature,
|
|
state: "enabled",
|
|
assignedBy: "test",
|
|
});
|
|
|
|
const statusMap = await service.resolveFeatureStatesAcrossTeams({
|
|
userId: user.id,
|
|
orgId: org.id,
|
|
teamIds: [team.id, team2.id],
|
|
featureIds: [testFeature],
|
|
});
|
|
const status = statusMap[testFeature];
|
|
|
|
expect(status.orgState).toBe("enabled");
|
|
expect(status.teamStates).toEqual(["disabled", "disabled"]);
|
|
expect(status.effectiveEnabled).toBe(false);
|
|
});
|
|
|
|
it("should return effectiveEnabled=true when at least one team is enabled and user inherits", async () => {
|
|
const { user, org, team, team2, service, setupFeature, featuresRepository } = await setup();
|
|
const testFeature = await setupFeature(true);
|
|
|
|
// Org enables
|
|
await featuresRepository.setTeamFeatureState({
|
|
teamId: org.id,
|
|
featureId: testFeature,
|
|
state: "enabled",
|
|
assignedBy: "test",
|
|
});
|
|
|
|
// One team enables, one disables
|
|
await Promise.all([
|
|
featuresRepository.setTeamFeatureState({
|
|
teamId: team.id,
|
|
featureId: testFeature,
|
|
state: "enabled",
|
|
assignedBy: "test",
|
|
}),
|
|
featuresRepository.setTeamFeatureState({
|
|
teamId: team2.id,
|
|
featureId: testFeature,
|
|
state: "disabled",
|
|
assignedBy: "test",
|
|
}),
|
|
]);
|
|
|
|
const statusMap = await service.resolveFeatureStatesAcrossTeams({
|
|
userId: user.id,
|
|
orgId: org.id,
|
|
teamIds: [team.id, team2.id],
|
|
featureIds: [testFeature],
|
|
});
|
|
const status = statusMap[testFeature];
|
|
|
|
expect(status.orgState).toBe("enabled");
|
|
expect(status.userState).toBe("inherit");
|
|
expect(status.effectiveEnabled).toBe(true);
|
|
});
|
|
|
|
it("should return effectiveEnabled=false when user explicitly disables", async () => {
|
|
const { user, org, team, service, setupFeature, featuresRepository } = await setup();
|
|
const testFeature = await setupFeature(true);
|
|
|
|
// Org enables
|
|
await featuresRepository.setTeamFeatureState({
|
|
teamId: org.id,
|
|
featureId: testFeature,
|
|
state: "enabled",
|
|
assignedBy: "test",
|
|
});
|
|
|
|
// Team enables
|
|
await featuresRepository.setTeamFeatureState({
|
|
teamId: team.id,
|
|
featureId: testFeature,
|
|
state: "enabled",
|
|
assignedBy: "test",
|
|
});
|
|
|
|
// User disables
|
|
await featuresRepository.setUserFeatureState({
|
|
userId: user.id,
|
|
featureId: testFeature,
|
|
state: "disabled",
|
|
assignedBy: "test",
|
|
});
|
|
|
|
const statusMap = await service.resolveFeatureStatesAcrossTeams({
|
|
userId: user.id,
|
|
orgId: org.id,
|
|
teamIds: [team.id],
|
|
featureIds: [testFeature],
|
|
});
|
|
const status = statusMap[testFeature];
|
|
|
|
expect(status.userState).toBe("disabled");
|
|
expect(status.effectiveEnabled).toBe(false);
|
|
});
|
|
|
|
it("should return effectiveEnabled=true when teams inherit from enabled org", async () => {
|
|
const { user, org, team, service, setupFeature, featuresRepository } = await setup();
|
|
const testFeature = await setupFeature(true);
|
|
|
|
// Org enables
|
|
await featuresRepository.setTeamFeatureState({
|
|
teamId: org.id,
|
|
featureId: testFeature,
|
|
state: "enabled",
|
|
assignedBy: "test",
|
|
});
|
|
|
|
// Teams inherit (no rows)
|
|
|
|
const statusMap = await service.resolveFeatureStatesAcrossTeams({
|
|
userId: user.id,
|
|
orgId: org.id,
|
|
teamIds: [team.id],
|
|
featureIds: [testFeature],
|
|
});
|
|
const status = statusMap[testFeature];
|
|
|
|
expect(status.orgState).toBe("enabled");
|
|
expect(status.teamStates).toEqual(["inherit"]);
|
|
expect(status.effectiveEnabled).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("org inherits (or no org)", () => {
|
|
it("should return effectiveEnabled=false when all teams are disabled", async () => {
|
|
const { user, team, service, setupFeature, featuresRepository } = await setup();
|
|
const testFeature = await setupFeature(true);
|
|
|
|
// Team disables
|
|
await featuresRepository.setTeamFeatureState({
|
|
teamId: team.id,
|
|
featureId: testFeature,
|
|
state: "disabled",
|
|
assignedBy: "test",
|
|
});
|
|
|
|
const statusMap = await service.resolveFeatureStatesAcrossTeams({
|
|
userId: user.id,
|
|
orgId: null,
|
|
teamIds: [team.id],
|
|
featureIds: [testFeature],
|
|
});
|
|
const status = statusMap[testFeature];
|
|
|
|
expect(status.orgState).toBe("inherit");
|
|
expect(status.teamStates).toEqual(["disabled"]);
|
|
expect(status.effectiveEnabled).toBe(false);
|
|
});
|
|
|
|
it("should return effectiveEnabled=true when at least one team is enabled", async () => {
|
|
const { user, team, service, setupFeature, featuresRepository } = await setup();
|
|
const testFeature = await setupFeature(true);
|
|
|
|
// One team enables
|
|
await featuresRepository.setTeamFeatureState({
|
|
teamId: team.id,
|
|
featureId: testFeature,
|
|
state: "enabled",
|
|
assignedBy: "test",
|
|
});
|
|
|
|
const statusMap = await service.resolveFeatureStatesAcrossTeams({
|
|
userId: user.id,
|
|
orgId: null,
|
|
teamIds: [team.id],
|
|
featureIds: [testFeature],
|
|
});
|
|
const status = statusMap[testFeature];
|
|
|
|
expect(status.teamStates).toEqual(["enabled"]);
|
|
expect(status.effectiveEnabled).toBe(true);
|
|
});
|
|
|
|
it("should return effectiveEnabled=false when teams only inherit (no explicit enablement)", async () => {
|
|
const { user, team, service, setupFeature } = await setup();
|
|
const testFeature = await setupFeature(true);
|
|
|
|
// No org, team inherits (no row)
|
|
const statusMap = await service.resolveFeatureStatesAcrossTeams({
|
|
userId: user.id,
|
|
orgId: null,
|
|
teamIds: [team.id],
|
|
featureIds: [testFeature],
|
|
});
|
|
const status = statusMap[testFeature];
|
|
|
|
expect(status.orgState).toBe("inherit");
|
|
expect(status.teamStates).toEqual(["inherit"]);
|
|
expect(status.effectiveEnabled).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("no teams", () => {
|
|
it("should return effectiveEnabled=true when org is enabled and user inherits", async () => {
|
|
const { user, org, service, setupFeature, featuresRepository } = await setup();
|
|
const testFeature = await setupFeature(true);
|
|
|
|
// Org enables
|
|
await featuresRepository.setTeamFeatureState({
|
|
teamId: org.id,
|
|
featureId: testFeature,
|
|
state: "enabled",
|
|
assignedBy: "test",
|
|
});
|
|
|
|
const statusMap = await service.resolveFeatureStatesAcrossTeams({
|
|
userId: user.id,
|
|
orgId: org.id,
|
|
teamIds: [],
|
|
featureIds: [testFeature],
|
|
});
|
|
const status = statusMap[testFeature];
|
|
|
|
expect(status.orgState).toBe("enabled");
|
|
expect(status.teamStates).toEqual([]);
|
|
expect(status.effectiveEnabled).toBe(true);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("setUserFeatureState", () => {
|
|
it("should delete the row when state is 'inherit'", async () => {
|
|
const { user, service, setupFeature, featuresRepository } = await setup();
|
|
const testFeature = await setupFeature(true);
|
|
|
|
// First create a row
|
|
await featuresRepository.setUserFeatureState({
|
|
userId: user.id,
|
|
featureId: testFeature,
|
|
state: "enabled",
|
|
assignedBy: "test",
|
|
});
|
|
|
|
// Set to inherit (should delete)
|
|
await service.setUserFeatureState({
|
|
userId: user.id,
|
|
featureId: testFeature,
|
|
state: "inherit",
|
|
});
|
|
|
|
const row = await prisma.userFeatures.findFirst({
|
|
where: {
|
|
userId: user.id,
|
|
featureId: testFeature,
|
|
},
|
|
});
|
|
|
|
expect(row).toBeNull();
|
|
});
|
|
|
|
it("should upsert with enabled=true when state is 'enabled'", async () => {
|
|
const { user, service, setupFeature } = await setup();
|
|
const testFeature = await setupFeature(true);
|
|
|
|
await service.setUserFeatureState({
|
|
userId: user.id,
|
|
featureId: testFeature,
|
|
state: "enabled",
|
|
assignedBy: user.id,
|
|
});
|
|
|
|
const row = await prisma.userFeatures.findFirst({
|
|
where: {
|
|
userId: user.id,
|
|
featureId: testFeature,
|
|
},
|
|
});
|
|
|
|
expect(row).not.toBeNull();
|
|
expect(row?.enabled).toBe(true);
|
|
});
|
|
|
|
it("should upsert with enabled=false when state is 'disabled'", async () => {
|
|
const { user, service, setupFeature } = await setup();
|
|
const testFeature = await setupFeature(true);
|
|
|
|
await service.setUserFeatureState({
|
|
userId: user.id,
|
|
featureId: testFeature,
|
|
state: "disabled",
|
|
assignedBy: user.id,
|
|
});
|
|
|
|
const row = await prisma.userFeatures.findFirst({
|
|
where: {
|
|
userId: user.id,
|
|
featureId: testFeature,
|
|
},
|
|
});
|
|
|
|
expect(row).not.toBeNull();
|
|
expect(row?.enabled).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("setTeamFeatureState", () => {
|
|
it("should delete the row when state is 'inherit'", async () => {
|
|
const { team, service, setupFeature, featuresRepository } = await setup();
|
|
const testFeature = await setupFeature(true);
|
|
|
|
// First create a row
|
|
await featuresRepository.setTeamFeatureState({
|
|
teamId: team.id,
|
|
featureId: testFeature,
|
|
state: "enabled",
|
|
assignedBy: "test",
|
|
});
|
|
|
|
// Set to inherit (should delete)
|
|
await service.setTeamFeatureState({
|
|
teamId: team.id,
|
|
featureId: testFeature,
|
|
state: "inherit",
|
|
});
|
|
|
|
const row = await prisma.teamFeatures.findUnique({
|
|
where: {
|
|
teamId_featureId: {
|
|
teamId: team.id,
|
|
featureId: testFeature,
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(row).toBeNull();
|
|
});
|
|
|
|
it("should upsert with enabled=true when state is 'enabled'", async () => {
|
|
const { user, team, service, setupFeature } = await setup();
|
|
const testFeature = await setupFeature(true);
|
|
|
|
await service.setTeamFeatureState({
|
|
teamId: team.id,
|
|
featureId: testFeature,
|
|
state: "enabled",
|
|
assignedBy: user.id,
|
|
});
|
|
|
|
const row = await prisma.teamFeatures.findUnique({
|
|
where: {
|
|
teamId_featureId: {
|
|
teamId: team.id,
|
|
featureId: testFeature,
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(row).not.toBeNull();
|
|
expect(row?.enabled).toBe(true);
|
|
});
|
|
|
|
it("should upsert with enabled=false when state is 'disabled'", async () => {
|
|
const { user, team, service, setupFeature } = await setup();
|
|
const testFeature = await setupFeature(true);
|
|
|
|
await service.setTeamFeatureState({
|
|
teamId: team.id,
|
|
featureId: testFeature,
|
|
state: "disabled",
|
|
assignedBy: user.id,
|
|
});
|
|
|
|
const row = await prisma.teamFeatures.findUnique({
|
|
where: {
|
|
teamId_featureId: {
|
|
teamId: team.id,
|
|
featureId: testFeature,
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(row).not.toBeNull();
|
|
expect(row?.enabled).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("auto-opt-in scenarios", () => {
|
|
describe("user autoOptInFeatures", () => {
|
|
it("should transform user state from 'inherit' to 'enabled' when user has autoOptInFeatures=true", async () => {
|
|
const { user, org, team, service, setupFeature, featuresRepository } = await setup();
|
|
const testFeature = await setupFeature(true);
|
|
|
|
// Enable auto-opt-in for user
|
|
await prisma.user.update({
|
|
where: { id: user.id },
|
|
data: { autoOptInFeatures: true },
|
|
});
|
|
|
|
// Org enables (to allow team level)
|
|
await featuresRepository.setTeamFeatureState({
|
|
teamId: org.id,
|
|
featureId: testFeature,
|
|
state: "enabled",
|
|
assignedBy: "test",
|
|
});
|
|
|
|
// Team enables (to allow user level)
|
|
await featuresRepository.setTeamFeatureState({
|
|
teamId: team.id,
|
|
featureId: testFeature,
|
|
state: "enabled",
|
|
assignedBy: "test",
|
|
});
|
|
|
|
// User has no explicit state (inherit), but autoOptInFeatures=true should enable
|
|
const statusMap = await service.resolveFeatureStatesAcrossTeams({
|
|
userId: user.id,
|
|
orgId: org.id,
|
|
teamIds: [team.id],
|
|
featureIds: [testFeature],
|
|
});
|
|
const status = statusMap[testFeature];
|
|
|
|
expect(status.userState).toBe("inherit"); // Raw state is still inherit
|
|
expect(status.userAutoOptIn).toBe(true);
|
|
expect(status.effectiveEnabled).toBe(true); // But effective is true due to auto-opt-in
|
|
});
|
|
|
|
it("should NOT transform explicit 'disabled' state when user has autoOptInFeatures=true", async () => {
|
|
const { user, org, team, service, setupFeature, featuresRepository } = await setup();
|
|
const testFeature = await setupFeature(true);
|
|
|
|
// Enable auto-opt-in for user
|
|
await prisma.user.update({
|
|
where: { id: user.id },
|
|
data: { autoOptInFeatures: true },
|
|
});
|
|
|
|
// Org enables
|
|
await featuresRepository.setTeamFeatureState({
|
|
teamId: org.id,
|
|
featureId: testFeature,
|
|
state: "enabled",
|
|
assignedBy: "test",
|
|
});
|
|
|
|
// Team enables
|
|
await featuresRepository.setTeamFeatureState({
|
|
teamId: team.id,
|
|
featureId: testFeature,
|
|
state: "enabled",
|
|
assignedBy: "test",
|
|
});
|
|
|
|
// User explicitly disables
|
|
await featuresRepository.setUserFeatureState({
|
|
userId: user.id,
|
|
featureId: testFeature,
|
|
state: "disabled",
|
|
assignedBy: "test",
|
|
});
|
|
|
|
const statusMap = await service.resolveFeatureStatesAcrossTeams({
|
|
userId: user.id,
|
|
orgId: org.id,
|
|
teamIds: [team.id],
|
|
featureIds: [testFeature],
|
|
});
|
|
const status = statusMap[testFeature];
|
|
|
|
expect(status.userState).toBe("disabled");
|
|
expect(status.userAutoOptIn).toBe(true);
|
|
expect(status.effectiveEnabled).toBe(false); // Explicit disabled takes precedence
|
|
});
|
|
});
|
|
|
|
describe("team autoOptInFeatures", () => {
|
|
it("should transform team state from 'inherit' to 'enabled' when team has autoOptInFeatures=true", async () => {
|
|
const { user, org, team, service, setupFeature, featuresRepository } = await setup();
|
|
const testFeature = await setupFeature(true);
|
|
|
|
// Enable auto-opt-in for team
|
|
await prisma.team.update({
|
|
where: { id: team.id },
|
|
data: { autoOptInFeatures: true },
|
|
});
|
|
|
|
// Org enables (to allow team level)
|
|
await featuresRepository.setTeamFeatureState({
|
|
teamId: org.id,
|
|
featureId: testFeature,
|
|
state: "enabled",
|
|
assignedBy: "test",
|
|
});
|
|
|
|
// Team has no explicit state (inherit), but autoOptInFeatures=true should enable
|
|
const statusMap = await service.resolveFeatureStatesAcrossTeams({
|
|
userId: user.id,
|
|
orgId: org.id,
|
|
teamIds: [team.id],
|
|
featureIds: [testFeature],
|
|
});
|
|
const status = statusMap[testFeature];
|
|
|
|
expect(status.teamStates).toEqual(["inherit"]); // Raw state is still inherit
|
|
expect(status.teamAutoOptIns).toEqual([true]);
|
|
expect(status.effectiveEnabled).toBe(true); // Effective is true due to team auto-opt-in
|
|
});
|
|
|
|
it("should NOT transform explicit 'disabled' state when team has autoOptInFeatures=true", async () => {
|
|
const { user, org, team, service, setupFeature, featuresRepository } = await setup();
|
|
const testFeature = await setupFeature(true);
|
|
|
|
// Enable auto-opt-in for team
|
|
await prisma.team.update({
|
|
where: { id: team.id },
|
|
data: { autoOptInFeatures: true },
|
|
});
|
|
|
|
// Org enables
|
|
await featuresRepository.setTeamFeatureState({
|
|
teamId: org.id,
|
|
featureId: testFeature,
|
|
state: "enabled",
|
|
assignedBy: "test",
|
|
});
|
|
|
|
// Team explicitly disables
|
|
await featuresRepository.setTeamFeatureState({
|
|
teamId: team.id,
|
|
featureId: testFeature,
|
|
state: "disabled",
|
|
assignedBy: "test",
|
|
});
|
|
|
|
const statusMap = await service.resolveFeatureStatesAcrossTeams({
|
|
userId: user.id,
|
|
orgId: org.id,
|
|
teamIds: [team.id],
|
|
featureIds: [testFeature],
|
|
});
|
|
const status = statusMap[testFeature];
|
|
|
|
expect(status.teamStates).toEqual(["disabled"]);
|
|
expect(status.teamAutoOptIns).toEqual([true]);
|
|
expect(status.effectiveEnabled).toBe(false); // Explicit disabled takes precedence
|
|
});
|
|
});
|
|
|
|
describe("org autoOptInFeatures", () => {
|
|
it("should transform org state from 'inherit' to 'enabled' when org has autoOptInFeatures=true", async () => {
|
|
const { user, org, team, service, setupFeature } = await setup();
|
|
const testFeature = await setupFeature(true);
|
|
|
|
// Enable auto-opt-in for org
|
|
await prisma.team.update({
|
|
where: { id: org.id },
|
|
data: { autoOptInFeatures: true },
|
|
});
|
|
|
|
// Org has no explicit state (inherit), but autoOptInFeatures=true should enable
|
|
// Team inherits from org
|
|
const statusMap = await service.resolveFeatureStatesAcrossTeams({
|
|
userId: user.id,
|
|
orgId: org.id,
|
|
teamIds: [team.id],
|
|
featureIds: [testFeature],
|
|
});
|
|
const status = statusMap[testFeature];
|
|
|
|
expect(status.orgState).toBe("inherit"); // Raw state is still inherit
|
|
expect(status.orgAutoOptIn).toBe(true);
|
|
expect(status.effectiveEnabled).toBe(true); // Effective is true due to org auto-opt-in
|
|
});
|
|
|
|
it("should NOT transform explicit 'disabled' state when org has autoOptInFeatures=true", async () => {
|
|
const { user, org, team, service, setupFeature, featuresRepository } = await setup();
|
|
const testFeature = await setupFeature(true);
|
|
|
|
// Enable auto-opt-in for org
|
|
await prisma.team.update({
|
|
where: { id: org.id },
|
|
data: { autoOptInFeatures: true },
|
|
});
|
|
|
|
// Org explicitly disables
|
|
await featuresRepository.setTeamFeatureState({
|
|
teamId: org.id,
|
|
featureId: testFeature,
|
|
state: "disabled",
|
|
assignedBy: "test",
|
|
});
|
|
|
|
const statusMap = await service.resolveFeatureStatesAcrossTeams({
|
|
userId: user.id,
|
|
orgId: org.id,
|
|
teamIds: [team.id],
|
|
featureIds: [testFeature],
|
|
});
|
|
const status = statusMap[testFeature];
|
|
|
|
expect(status.orgState).toBe("disabled");
|
|
expect(status.orgAutoOptIn).toBe(true);
|
|
expect(status.effectiveEnabled).toBe(false); // Explicit disabled takes precedence
|
|
});
|
|
});
|
|
|
|
describe("auto-opt-in with org disabled blocking", () => {
|
|
it("should return effectiveEnabled=false when user auto-opts-in but org is disabled", async () => {
|
|
const { user, org, team, service, setupFeature, featuresRepository } = await setup();
|
|
const testFeature = await setupFeature(true);
|
|
|
|
// Enable auto-opt-in for user
|
|
await prisma.user.update({
|
|
where: { id: user.id },
|
|
data: { autoOptInFeatures: true },
|
|
});
|
|
|
|
// Org explicitly disables
|
|
await featuresRepository.setTeamFeatureState({
|
|
teamId: org.id,
|
|
featureId: testFeature,
|
|
state: "disabled",
|
|
assignedBy: "test",
|
|
});
|
|
|
|
const statusMap = await service.resolveFeatureStatesAcrossTeams({
|
|
userId: user.id,
|
|
orgId: org.id,
|
|
teamIds: [team.id],
|
|
featureIds: [testFeature],
|
|
});
|
|
const status = statusMap[testFeature];
|
|
|
|
expect(status.orgState).toBe("disabled");
|
|
expect(status.userState).toBe("inherit");
|
|
expect(status.userAutoOptIn).toBe(true);
|
|
expect(status.effectiveEnabled).toBe(false); // Org disabled blocks everything
|
|
});
|
|
});
|
|
|
|
describe("auto-opt-in flags in response", () => {
|
|
it("should return correct auto-opt-in flags for all levels", async () => {
|
|
const { user, org, team, team2, service, setupFeature } = await setup();
|
|
const testFeature = await setupFeature(true);
|
|
|
|
// Set up different auto-opt-in states
|
|
await prisma.user.update({
|
|
where: { id: user.id },
|
|
data: { autoOptInFeatures: true },
|
|
});
|
|
|
|
await prisma.team.update({
|
|
where: { id: org.id },
|
|
data: { autoOptInFeatures: false },
|
|
});
|
|
|
|
await prisma.team.update({
|
|
where: { id: team.id },
|
|
data: { autoOptInFeatures: true },
|
|
});
|
|
|
|
await prisma.team.update({
|
|
where: { id: team2.id },
|
|
data: { autoOptInFeatures: false },
|
|
});
|
|
|
|
const statusMap = await service.resolveFeatureStatesAcrossTeams({
|
|
userId: user.id,
|
|
orgId: org.id,
|
|
teamIds: [team.id, team2.id],
|
|
featureIds: [testFeature],
|
|
});
|
|
const status = statusMap[testFeature];
|
|
|
|
expect(status.userAutoOptIn).toBe(true);
|
|
expect(status.orgAutoOptIn).toBe(false);
|
|
expect(status.teamAutoOptIns).toEqual([true, false]);
|
|
});
|
|
});
|
|
});
|
|
});
|