"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const database_init_1 = __importDefault(require("../../../test/e2e/helpers/database-init"));
const no_logger_1 = __importDefault(require("../../../test/fixtures/no-logger"));
const test_helper_1 = require("../../../test/e2e/helpers/test-helper");
const random_id_1 = require("../../util/random-id");
const user_1 = __importDefault(require("../../types/user"));
const segments_1 = require("../../util/segments");
const collect_ids_1 = require("../../util/collect-ids");
const arraysHaveSameItems_1 = require("../../util/arraysHaveSameItems");
const util_1 = require("../../util");
const types_1 = require("../../types");
let db;
let app;
const FEATURES_CLIENT_BASE_PATH = '/api/client/features';
const fetchSegments = () => {
    return app.services.segmentService.getAll();
};
const fetchFeatures = () => {
    return app.request
        .get(`/api/admin/features`)
        .expect(200)
        .then((res) => res.body.features);
};
const fetchClientFeatures = () => {
    return app.request
        .get(FEATURES_CLIENT_BASE_PATH)
        .expect(200)
        .then((res) => res.body.features);
};
const createSegment = (postData) => {
    return app.services.segmentService.create(postData, types_1.TEST_AUDIT_USER);
};
const updateSegment = (id, postData) => {
    return app.services.segmentService.update(id, postData, {
        email: 'test@example.com',
        id: 1,
    }, types_1.TEST_AUDIT_USER);
};
const mockStrategy = (segments = []) => {
    return {
        name: 'flexibleRollout',
        parameters: {},
        constraints: [],
        segments,
    };
};
const createProjects = async (projects = [types_1.DEFAULT_PROJECT]) => {
    for (const project of projects) {
        await db.stores.projectStore.create({
            name: project,
            description: '',
            id: project,
            mode: 'open',
        });
        await app.request
            .post(`/api/admin/projects/${project}/environments`)
            .send({
            environment: util_1.DEFAULT_ENV,
        })
            .expect(200);
    }
};
const createFeatureToggle = async (feature, strategies = [mockStrategy()], project = types_1.DEFAULT_PROJECT, environment = util_1.DEFAULT_ENV, expectStatusCode = 201, expectSegmentStatusCodes = [
    { status: 200 },
]) => {
    await app.createFeature(feature, project, expectStatusCode);
    let processed = 0;
    for (const strategy of strategies) {
        const { body, status } = await app.request
            .post(`/api/admin/projects/${project}/features/${feature.name}/environments/${environment}/strategies`)
            .send(strategy);
        const expectation = expectSegmentStatusCodes[processed++];
        expect(status).toBe(expectation.status);
        if (expectation.message) {
            expect(JSON.stringify(body)).toContain(expectation.message);
        }
    }
};
const updateFeatureStrategy = async (featureName, strategy, project = types_1.DEFAULT_PROJECT, environment = util_1.DEFAULT_ENV, expectedStatus = 200) => {
    const { status } = await app.request
        .put(`/api/admin/projects/${project}/features/${featureName}/environments/${environment}/strategies/${strategy.id}`)
        .send(strategy);
    expect(status).toBe(expectedStatus);
};
const mockFeatureToggle = () => {
    return {
        name: (0, random_id_1.randomId)(),
    };
};
const mockConstraints = () => {
    return Array.from({ length: 5 }).map(() => ({
        values: ['x', 'y', 'z'],
        operator: 'IN',
        contextName: 'a',
    }));
};
const mockConstraintValues = (length) => {
    return Array.from({ length }).map(() => {
        return (0, random_id_1.randomId)();
    });
};
const fetchClientResponse = () => {
    return app.request
        .get(FEATURES_CLIENT_BASE_PATH)
        .set('Unleash-Client-Spec', '4.2.0')
        .expect(200)
        .then((res) => res.body);
};
const createTestSegments = async () => {
    const constraints = mockConstraints();
    const segment1 = await createSegment({ name: 'S1', constraints });
    const segment2 = await createSegment({ name: 'S2', constraints });
    const segment3 = await createSegment({ name: 'S3', constraints });
    await createFeatureToggle(mockFeatureToggle(), [
        mockStrategy([segment1.id, segment2.id]),
    ]);
    await createFeatureToggle(mockFeatureToggle(), [
        mockStrategy([segment2.id]),
    ]);
    await createFeatureToggle(mockFeatureToggle());
    return [segment1, segment2, segment3];
};
beforeAll(async () => {
    db = await (0, database_init_1.default)('segments', no_logger_1.default);
    app = await (0, test_helper_1.setupAppWithCustomConfig)(db.stores, {
        experimental: {
            flags: {},
        },
    }, db.rawDatabase);
});
afterAll(async () => {
    await app.destroy();
    await db.destroy();
});
afterEach(async () => {
    await db.stores.segmentStore.deleteAll();
    await db.stores.featureToggleStore.deleteAll();
    await db.stores.eventStore.deleteAll();
});
test('should validate segment constraint values limit', async () => {
    const constraints = [
        {
            contextName: (0, random_id_1.randomId)(),
            operator: 'IN',
            values: mockConstraintValues(segments_1.DEFAULT_SEGMENT_VALUES_LIMIT + 1),
        },
    ];
    await expect(createSegment({ name: (0, random_id_1.randomId)(), constraints })).rejects.toThrow(`Segments may not have more than ${segments_1.DEFAULT_SEGMENT_VALUES_LIMIT} values`);
});
test('should validate segment constraint values limit with multiple constraints', async () => {
    const constraints = [
        {
            contextName: (0, random_id_1.randomId)(),
            operator: 'IN',
            values: mockConstraintValues(segments_1.DEFAULT_SEGMENT_VALUES_LIMIT),
        },
        {
            contextName: (0, random_id_1.randomId)(),
            operator: 'IN',
            values: mockConstraintValues(1),
        },
    ];
    await expect(createSegment({ name: (0, random_id_1.randomId)(), constraints })).rejects.toThrow(`Segments may not have more than ${segments_1.DEFAULT_SEGMENT_VALUES_LIMIT} values`);
});
test('should validate feature strategy segment limit', async () => {
    const segments = [];
    for (const id of [1, 2, 3, 4, 5, 6]) {
        segments.push(await createSegment({ name: `S${id}`, constraints: [] }));
    }
    await createFeatureToggle(mockFeatureToggle(), [mockStrategy(segments.map((s) => s.id))], types_1.DEFAULT_PROJECT, util_1.DEFAULT_ENV, 201, [
        {
            status: 400,
            message: `Strategies may not have more than ${segments_1.DEFAULT_STRATEGY_SEGMENTS_LIMIT} segments`,
        },
    ]);
});
test('should clone feature strategy segments', async () => {
    const constraints = mockConstraints();
    const segment1 = await createSegment({ name: 'S1', constraints });
    await createFeatureToggle(mockFeatureToggle(), [
        mockStrategy([segment1.id]),
    ]);
    await createFeatureToggle(mockFeatureToggle());
    const [feature1, feature2] = await fetchFeatures();
    const strategy1 = feature1.strategies[0].id;
    const strategy2 = feature2.strategies[0].id;
    let segments1 = await app.services.segmentService.getByStrategy(strategy1);
    let segments2 = await app.services.segmentService.getByStrategy(strategy2);
    expect((0, collect_ids_1.collectIds)(segments1)).toEqual([segment1.id]);
    expect((0, collect_ids_1.collectIds)(segments2)).toEqual([]);
    await app.services.segmentService.cloneStrategySegments(strategy1, strategy2);
    segments1 = await app.services.segmentService.getByStrategy(strategy1);
    segments2 = await app.services.segmentService.getByStrategy(strategy2);
    expect((0, collect_ids_1.collectIds)(segments1)).toEqual([segment1.id]);
    expect((0, collect_ids_1.collectIds)(segments2)).toEqual([segment1.id]);
});
test('should store segment-created and segment-deleted events', async () => {
    const constraints = mockConstraints();
    const user = new user_1.default({ id: 1, email: 'test@example.com' });
    await createSegment({ name: 'S1', constraints });
    const [segment1] = await fetchSegments();
    await app.services.segmentService.delete(segment1.id, user, (0, util_1.extractAuditInfoFromUser)(user));
    const events = await db.stores.eventStore.getEvents();
    expect(events[0].type).toEqual('segment-deleted');
    expect(events[0].preData.id).toEqual(segment1.id);
    expect(events[1].type).toEqual('segment-created');
    expect(events[1].data.id).toEqual(segment1.id);
});
test('should inline segment constraints into features by default', async () => {
    await createTestSegments();
    const [feature1, feature2, feature3] = await fetchFeatures();
    const [, , segment3] = await fetchSegments();
    // add segment3 to all features
    for (const feature of [feature1, feature2, feature3]) {
        const strategy = {
            ...feature.strategies[0],
            segments: feature.strategies[0].segments ?? [],
        };
        await updateFeatureStrategy(feature.name, {
            ...strategy,
            segments: [...strategy.segments, segment3.id],
        });
    }
    const clientFeatures = await fetchClientFeatures();
    const clientStrategies = clientFeatures.flatMap((f) => f.strategies);
    const clientConstraints = clientStrategies.flatMap((s) => s.constraints);
    const clientValues = clientConstraints.flatMap((c) => c.values);
    const uniqueValues = [...new Set(clientValues)];
    expect(clientFeatures.length).toEqual(3);
    expect(clientStrategies.length).toEqual(3);
    expect(clientConstraints.length).toEqual(5 * 6);
    expect(clientValues.length).toEqual(5 * 6 * 3);
    expect(uniqueValues.length).toEqual(3);
});
test('should only return segments to clients that support the spec', async () => {
    await createTestSegments();
    const [segment1, segment2] = await fetchSegments();
    const segmentIds = (0, collect_ids_1.collectIds)([segment1, segment2]);
    const unknownClientResponse = await app.request
        .get(FEATURES_CLIENT_BASE_PATH)
        .expect(200)
        .then((res) => res.body);
    const unknownClientConstraints = unknownClientResponse.features
        .flatMap((f) => f.strategies)
        .flatMap((s) => s.constraints);
    expect(unknownClientResponse.segments).toEqual(undefined);
    expect(unknownClientConstraints.length).toEqual(15);
    const supportedClientResponse = await app.request
        .get(FEATURES_CLIENT_BASE_PATH)
        .set('Unleash-Client-Spec', '4.2.0')
        .expect(200)
        .then((res) => res.body);
    const supportedClientConstraints = supportedClientResponse.features
        .flatMap((f) => f.strategies)
        .flatMap((s) => s.constraints);
    expect((0, collect_ids_1.collectIds)(supportedClientResponse.segments)).toEqual(segmentIds);
    expect(supportedClientConstraints.length).toEqual(0);
});
test('should return segments in base of toggle response if inline is disabled', async () => {
    await createTestSegments();
    const clientFeatures = await fetchClientResponse();
    expect(clientFeatures.segments.length).toBeDefined();
});
test('should only send segments that are in use', async () => {
    await createTestSegments();
    const clientFeatures = await fetchClientResponse();
    expect(clientFeatures.segments.length).toEqual(2);
});
test('should send all segments that are in use by feature', async () => {
    await createTestSegments();
    const clientFeatures = await fetchClientResponse();
    const globalSegments = clientFeatures.segments;
    expect(globalSegments).toHaveLength(2);
    const globalSegmentIds = globalSegments.map((segment) => segment.id);
    const allSegmentIds = clientFeatures.features
        .flatMap((feat) => feat.strategies.map((strategy) => strategy.segments))
        .flat()
        .filter((x) => !!x);
    const toggleSegmentIds = [...new Set(allSegmentIds)];
    expect((0, arraysHaveSameItems_1.arraysHaveSameItems)(globalSegmentIds, toggleSegmentIds)).toEqual(true);
});
describe('project-specific segments', () => {
    test(`can create a toggle with a project-specific segment`, async () => {
        const segmentName = 'my-segment';
        const project = (0, random_id_1.randomId)();
        await createProjects([project]);
        const segment = await createSegment({
            name: segmentName,
            project,
            constraints: [],
        });
        const strategy = {
            name: 'default',
            parameters: {},
            constraints: [],
            segments: [segment.id],
        };
        await createFeatureToggle({
            name: 'first_feature',
            description: 'the #1 feature',
        }, [strategy], project);
    });
    test(`can't create a toggle with a segment from a different project`, async () => {
        const segmentName = 'my-segment';
        const project1 = (0, random_id_1.randomId)();
        const project2 = (0, random_id_1.randomId)();
        await createProjects([project1, project2]);
        const segment = await createSegment({
            name: segmentName,
            project: project1,
            constraints: [],
        });
        const strategy = {
            name: 'default',
            parameters: {},
            constraints: [],
            segments: [segment.id],
        };
        await createFeatureToggle({
            name: 'first_feature',
            description: 'the #1 feature',
        }, [strategy], project2, util_1.DEFAULT_ENV, 201, [{ status: 400 }]);
    });
    test(`can't set a different segment project when being used by another project`, async () => {
        const segmentName = 'my-segment';
        const project1 = (0, random_id_1.randomId)();
        const project2 = (0, random_id_1.randomId)();
        await createProjects([project1, project2]);
        const segment = await createSegment({
            name: segmentName,
            project: project1,
            constraints: [],
        });
        const strategy = {
            name: 'default',
            parameters: {},
            constraints: [],
            segments: [segment.id],
        };
        await createFeatureToggle({
            name: 'first_feature',
            description: 'the #1 feature',
        }, [strategy], project1);
        await expect(() => updateSegment(segment.id, {
            ...segment,
            project: project2,
        })).rejects.toThrow(`Invalid project. Segment is being used by strategies in other projects: ${project1}`);
    });
    test('can promote a segment project to global even when being used by a specific project', async () => {
        const segmentName = 'my-segment';
        const project1 = (0, random_id_1.randomId)();
        const project2 = (0, random_id_1.randomId)();
        await createProjects([project1, project2]);
        const segment = await createSegment({
            name: segmentName,
            project: project1,
            constraints: [],
        });
        const strategy = {
            name: 'default',
            parameters: {},
            constraints: [],
            segments: [segment.id],
        };
        await createFeatureToggle({
            name: 'first_feature',
            description: 'the #1 feature',
        }, [strategy], project1);
        await expect(() => updateSegment(segment.id, {
            ...segment,
            project: '',
        })).resolves;
    });
    test(`can't set a specific segment project when being used by multiple projects (global)`, async () => {
        const segmentName = 'my-segment';
        const project1 = (0, random_id_1.randomId)();
        const project2 = (0, random_id_1.randomId)();
        await createProjects([project1, project2]);
        const segment = await createSegment({
            name: segmentName,
            project: '',
            constraints: [],
        });
        const strategy = {
            name: 'default',
            parameters: {},
            constraints: [],
            segments: [segment.id],
        };
        const strategy2 = {
            name: 'default',
            parameters: {},
            constraints: [],
            segments: [segment.id],
        };
        await createFeatureToggle({
            name: 'first_feature',
            description: 'the #1 feature',
        }, [strategy], project1);
        await createFeatureToggle({
            name: 'second_feature',
            description: 'the #2 feature',
        }, [strategy2], project2);
        await expect(() => updateSegment(segment.id, {
            ...segment,
            project: project1,
        })).rejects.toThrow(`Invalid project. Segment is being used by strategies in other projects: ${project1}, ${project2}`);
    });
});
//# sourceMappingURL=client-segment.e2e.test.js.map