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>
353 lines
9.7 KiB
TypeScript
353 lines
9.7 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
|
|
import { computeEffectiveStateAcrossTeams } from "./computeEffectiveState";
|
|
|
|
describe("computeEffectiveStateAcrossTeams", () => {
|
|
describe("when global is disabled", () => {
|
|
it("returns false regardless of other states", () => {
|
|
expect(
|
|
computeEffectiveStateAcrossTeams({
|
|
globalEnabled: false,
|
|
orgState: "enabled",
|
|
teamStates: ["enabled"],
|
|
userState: "enabled",
|
|
})
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("when org is disabled", () => {
|
|
it("returns false regardless of team and user state", () => {
|
|
expect(
|
|
computeEffectiveStateAcrossTeams({
|
|
globalEnabled: true,
|
|
orgState: "disabled",
|
|
teamStates: ["enabled"],
|
|
userState: "enabled",
|
|
})
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("when org is enabled", () => {
|
|
describe("when all teams are disabled", () => {
|
|
it("returns false regardless of user state", () => {
|
|
expect(
|
|
computeEffectiveStateAcrossTeams({
|
|
globalEnabled: true,
|
|
orgState: "enabled",
|
|
teamStates: ["disabled", "disabled"],
|
|
userState: "enabled",
|
|
})
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("when at least one team is enabled", () => {
|
|
it("returns true when user is enabled", () => {
|
|
expect(
|
|
computeEffectiveStateAcrossTeams({
|
|
globalEnabled: true,
|
|
orgState: "enabled",
|
|
teamStates: ["enabled", "disabled"],
|
|
userState: "enabled",
|
|
})
|
|
).toBe(true);
|
|
});
|
|
|
|
it("returns false when user is disabled", () => {
|
|
expect(
|
|
computeEffectiveStateAcrossTeams({
|
|
globalEnabled: true,
|
|
orgState: "enabled",
|
|
teamStates: ["enabled", "disabled"],
|
|
userState: "disabled",
|
|
})
|
|
).toBe(false);
|
|
});
|
|
|
|
it("returns true when user inherits", () => {
|
|
expect(
|
|
computeEffectiveStateAcrossTeams({
|
|
globalEnabled: true,
|
|
orgState: "enabled",
|
|
teamStates: ["enabled", "disabled"],
|
|
userState: "inherit",
|
|
})
|
|
).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("when teams inherit from enabled org", () => {
|
|
it("returns true when user is enabled or inherits", () => {
|
|
expect(
|
|
computeEffectiveStateAcrossTeams({
|
|
globalEnabled: true,
|
|
orgState: "enabled",
|
|
teamStates: ["inherit"],
|
|
userState: "enabled",
|
|
})
|
|
).toBe(true);
|
|
|
|
expect(
|
|
computeEffectiveStateAcrossTeams({
|
|
globalEnabled: true,
|
|
orgState: "enabled",
|
|
teamStates: ["inherit"],
|
|
userState: "inherit",
|
|
})
|
|
).toBe(true);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("when org inherits (or no org)", () => {
|
|
describe("when all teams are disabled", () => {
|
|
it("returns false regardless of user state", () => {
|
|
expect(
|
|
computeEffectiveStateAcrossTeams({
|
|
globalEnabled: true,
|
|
orgState: "inherit",
|
|
teamStates: ["disabled"],
|
|
userState: "enabled",
|
|
})
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("when at least one team is enabled", () => {
|
|
it("returns true when user is enabled or inherits", () => {
|
|
expect(
|
|
computeEffectiveStateAcrossTeams({
|
|
globalEnabled: true,
|
|
orgState: "inherit",
|
|
teamStates: ["enabled", "disabled"],
|
|
userState: "enabled",
|
|
})
|
|
).toBe(true);
|
|
|
|
expect(
|
|
computeEffectiveStateAcrossTeams({
|
|
globalEnabled: true,
|
|
orgState: "inherit",
|
|
teamStates: ["enabled"],
|
|
userState: "inherit",
|
|
})
|
|
).toBe(true);
|
|
});
|
|
|
|
it("returns false when user is disabled", () => {
|
|
expect(
|
|
computeEffectiveStateAcrossTeams({
|
|
globalEnabled: true,
|
|
orgState: "inherit",
|
|
teamStates: ["enabled"],
|
|
userState: "disabled",
|
|
})
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("when teams only inherit (no org enabled)", () => {
|
|
it("returns true when user explicitly opts in", () => {
|
|
expect(
|
|
computeEffectiveStateAcrossTeams({
|
|
globalEnabled: true,
|
|
orgState: "inherit",
|
|
teamStates: ["inherit"],
|
|
userState: "enabled",
|
|
})
|
|
).toBe(true);
|
|
});
|
|
|
|
it("returns false when user inherits because no explicit enablement above", () => {
|
|
expect(
|
|
computeEffectiveStateAcrossTeams({
|
|
globalEnabled: true,
|
|
orgState: "inherit",
|
|
teamStates: ["inherit"],
|
|
userState: "inherit",
|
|
})
|
|
).toBe(false);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("when user has no teams", () => {
|
|
it("returns true when org is enabled and user is enabled/inherits", () => {
|
|
expect(
|
|
computeEffectiveStateAcrossTeams({
|
|
globalEnabled: true,
|
|
orgState: "enabled",
|
|
teamStates: [],
|
|
userState: "enabled",
|
|
})
|
|
).toBe(true);
|
|
|
|
expect(
|
|
computeEffectiveStateAcrossTeams({
|
|
globalEnabled: true,
|
|
orgState: "enabled",
|
|
teamStates: [],
|
|
userState: "inherit",
|
|
})
|
|
).toBe(true);
|
|
});
|
|
|
|
it("returns false when org inherits and user has no explicit enablement", () => {
|
|
expect(
|
|
computeEffectiveStateAcrossTeams({
|
|
globalEnabled: true,
|
|
orgState: "inherit",
|
|
teamStates: [],
|
|
userState: "inherit",
|
|
})
|
|
).toBe(false); // No explicit enablement in chain, feature should be disabled
|
|
});
|
|
|
|
it("returns true when org inherits but user explicitly enables", () => {
|
|
expect(
|
|
computeEffectiveStateAcrossTeams({
|
|
globalEnabled: true,
|
|
orgState: "inherit",
|
|
teamStates: [],
|
|
userState: "enabled",
|
|
})
|
|
).toBe(true); // User explicit enablement is sufficient
|
|
});
|
|
});
|
|
|
|
describe("user opt-in behavior", () => {
|
|
it("allows user to opt-in regardless of org/team inheritance state", () => {
|
|
// User can opt-in even when org and all teams are just inheriting
|
|
expect(
|
|
computeEffectiveStateAcrossTeams({
|
|
globalEnabled: true,
|
|
orgState: "inherit",
|
|
teamStates: ["inherit", "inherit"],
|
|
userState: "enabled",
|
|
})
|
|
).toBe(true);
|
|
});
|
|
|
|
it("blocks user opt-in when all teams have explicitly disabled", () => {
|
|
expect(
|
|
computeEffectiveStateAcrossTeams({
|
|
globalEnabled: true,
|
|
orgState: "inherit",
|
|
teamStates: ["disabled", "disabled"],
|
|
userState: "enabled",
|
|
})
|
|
).toBe(false);
|
|
});
|
|
|
|
it("blocks user opt-in when org has explicitly disabled", () => {
|
|
expect(
|
|
computeEffectiveStateAcrossTeams({
|
|
globalEnabled: true,
|
|
orgState: "disabled",
|
|
teamStates: ["inherit"],
|
|
userState: "enabled",
|
|
})
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("truth table from design doc", () => {
|
|
it("org disabled, any teams, any user → false", () => {
|
|
expect(
|
|
computeEffectiveStateAcrossTeams({
|
|
globalEnabled: true,
|
|
orgState: "disabled",
|
|
teamStates: ["enabled"],
|
|
userState: "enabled",
|
|
})
|
|
).toBe(false);
|
|
});
|
|
|
|
it("org enabled, all teams disabled, any user → false", () => {
|
|
expect(
|
|
computeEffectiveStateAcrossTeams({
|
|
globalEnabled: true,
|
|
orgState: "enabled",
|
|
teamStates: ["disabled", "disabled"],
|
|
userState: "enabled",
|
|
})
|
|
).toBe(false);
|
|
});
|
|
|
|
it("org enabled, at least one team enabled/inherit, user disabled → false", () => {
|
|
expect(
|
|
computeEffectiveStateAcrossTeams({
|
|
globalEnabled: true,
|
|
orgState: "enabled",
|
|
teamStates: ["enabled"],
|
|
userState: "disabled",
|
|
})
|
|
).toBe(false);
|
|
});
|
|
|
|
it("org enabled, at least one team enabled/inherit, user enabled/inherit → true", () => {
|
|
expect(
|
|
computeEffectiveStateAcrossTeams({
|
|
globalEnabled: true,
|
|
orgState: "enabled",
|
|
teamStates: ["enabled"],
|
|
userState: "enabled",
|
|
})
|
|
).toBe(true);
|
|
|
|
expect(
|
|
computeEffectiveStateAcrossTeams({
|
|
globalEnabled: true,
|
|
orgState: "enabled",
|
|
teamStates: ["inherit"],
|
|
userState: "inherit",
|
|
})
|
|
).toBe(true);
|
|
});
|
|
|
|
it("org inherit/null, all teams disabled, any user → false", () => {
|
|
expect(
|
|
computeEffectiveStateAcrossTeams({
|
|
globalEnabled: true,
|
|
orgState: "inherit",
|
|
teamStates: ["disabled"],
|
|
userState: "enabled",
|
|
})
|
|
).toBe(false);
|
|
});
|
|
|
|
it("org inherit/null, at least one team enabled, user disabled → false", () => {
|
|
expect(
|
|
computeEffectiveStateAcrossTeams({
|
|
globalEnabled: true,
|
|
orgState: "inherit",
|
|
teamStates: ["enabled"],
|
|
userState: "disabled",
|
|
})
|
|
).toBe(false);
|
|
});
|
|
|
|
it("org inherit/null, at least one team enabled, user enabled/inherit → true", () => {
|
|
expect(
|
|
computeEffectiveStateAcrossTeams({
|
|
globalEnabled: true,
|
|
orgState: "inherit",
|
|
teamStates: ["enabled"],
|
|
userState: "enabled",
|
|
})
|
|
).toBe(true);
|
|
|
|
expect(
|
|
computeEffectiveStateAcrossTeams({
|
|
globalEnabled: true,
|
|
orgState: "inherit",
|
|
teamStates: ["enabled"],
|
|
userState: "inherit",
|
|
})
|
|
).toBe(true);
|
|
});
|
|
});
|
|
});
|