Files
calendar/packages/features/feature-opt-in/services/FeatureOptInService.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

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