Skip to content

Commit

Permalink
Merge branch 'main' into sort-neurons-by-rewards-timestamp
Browse files Browse the repository at this point in the history
  • Loading branch information
mstrasinskis authored Nov 22, 2024
2 parents 8f7accf + 9979071 commit 3168e60
Show file tree
Hide file tree
Showing 9 changed files with 139 additions and 47 deletions.
4 changes: 2 additions & 2 deletions dfx.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"dfx": "0.24.1",
"dfx": "0.24.2",
"canisters": {
"nns-governance": {
"type": "custom",
Expand Down Expand Up @@ -432,7 +432,7 @@
"DIDC_VERSION": "didc 0.4.0",
"POCKETIC_VERSION": "3.0.1",
"CARGO_SORT_VERSION": "1.0.9",
"SNSDEMO_RELEASE": "release-2024-11-13",
"SNSDEMO_RELEASE": "release-2024-11-20",
"IC_COMMIT_FOR_PROPOSALS": "release-2024-11-14_03-07-base",
"IC_COMMIT_FOR_SNS_AGGREGATOR": "release-2024-11-14_03-07-base"
},
Expand Down
26 changes: 22 additions & 4 deletions frontend/src/lib/constants/environment.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,36 @@ export interface FeatureFlags<T> {
TEST_FLAG_EDITABLE: T;
TEST_FLAG_NOT_EDITABLE: T;
}
export const defaultFeatureFlagValues: FeatureFlags<boolean> = {
ENABLE_CKTESTBTC: false,
DISABLE_IMPORT_TOKEN_VALIDATION_FOR_TESTING: false,
ENABLE_PERIODIC_FOLLOWING_CONFIRMATION: false,
ENABLE_EXPORT_NEURONS_REPORT: false,
TEST_FLAG_EDITABLE: false,
TEST_FLAG_NOT_EDITABLE: false,
};

export type FeatureKey = keyof FeatureFlags<boolean>;

const getFeatureFlagsFromEnv = (): FeatureFlags<boolean> => {
let featureFlags = {};
try {
featureFlags = JSON.parse(envVars?.featureFlags);
} catch (e) {
console.error("Error parsing featureFlags", e);
}
// Complement the default flags with the ones from the environment to avoid missing flags.
return { ...defaultFeatureFlagValues, ...featureFlags };
};

/**
* DO NOT USE DIRECTLY
*
* @see feature-flags.store.ts to use feature flags
*/
export const FEATURE_FLAG_ENVIRONMENT: FeatureFlags<boolean> = JSON.parse(
envVars?.featureFlags ??
'{"ENABLE_CKTESTBTC": false, "ENABLE_SNS_TYPES_FILTER": false, "ENABLE_EXPORT_NEURONS_REPORT": false}'
);

export const FEATURE_FLAG_ENVIRONMENT: FeatureFlags<boolean> =
getFeatureFlagsFromEnv();

export const IS_TESTNET: boolean =
DFX_NETWORK !== "mainnet" &&
Expand Down
32 changes: 18 additions & 14 deletions frontend/src/lib/utils/export-to-csv.utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
import { isNullish } from "@dfinity/utils";

export type CsvHeader<T> = {
id: keyof T;
label: string;
};

interface CsvBaseConfig<T> {
data: T[];
headers: CsvHeader<T>[];
}

interface CsvFileConfig<T> extends CsvBaseConfig<T> {
fileName?: string;
description?: string;
}

const escapeCsvValue = (value: unknown): string => {
if (isNullish(value)) return "";

Expand All @@ -24,17 +39,11 @@ const escapeCsvValue = (value: unknown): string => {
return stringValue;
};

export const convertToCsv = <T>({
data,
headers,
}: {
data: T[];
headers: { id: keyof T }[];
}) => {
export const convertToCsv = <T>({ data, headers }: CsvBaseConfig<T>) => {
if (headers.length === 0) return "";

const sanitizedHeaders = headers
.map(({ id }) => id)
.map(({ label }) => label)
.map((header) => escapeCsvValue(header));
const csvRows = [sanitizedHeaders.join(",")];

Expand Down Expand Up @@ -153,12 +162,7 @@ export const generateCsvFileToSave = async <T>({
headers,
fileName = "data",
description = "Csv file",
}: {
data: T[];
headers: { id: keyof T }[];
fileName?: string;
description?: string;
}): Promise<void> => {
}: CsvFileConfig<T>): Promise<void> => {
try {
const csvContent = convertToCsv<T>({ data, headers });

Expand Down
1 change: 1 addition & 0 deletions frontend/src/tests/lib/api/canisters.api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,7 @@ describe("canisters-api", () => {
});

it("should not notify if transfer fails", async () => {
vi.spyOn(console, "error").mockImplementation(() => undefined);
mockLedgerCanister.transfer.mockRejectedValue(new Error());

const call = () =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
import { LinkToCanistersPo } from "$tests/page-objects/LinkToCanisters.page-object";
import { JestPageObjectElement } from "$tests/page-objects/jest.page-object";
import { render } from "@testing-library/svelte";
import { vi } from "vitest";

describe("LinkToCanisters", () => {
beforeEach(() => {
Expand Down
53 changes: 53 additions & 0 deletions frontend/src/tests/lib/constants/environment.constants.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { defaultFeatureFlagValues } from "$lib/constants/environment.constants";
import * as envVarsUtils from "$lib/utils/env-vars.utils";

describe("FEATURE_FLAG_ENVIRONMENT", () => {
const environmentVars = envVarsUtils.getEnvVars();

beforeEach(() => {
// The FEATURE_FLAG_ENVIRONMENT is a constant that is set once when the module
// `environment.constants` is imported. To test different states of it,
// we need to reset the imported modules and reimport `environment.constants` for each test.
vi.resetModules();
});

it("should equal the environment values", async () => {
const { FEATURE_FLAG_ENVIRONMENT } = await import(
"$lib/constants/environment.constants"
);
const expectedFlags = JSON.parse(environmentVars.featureFlags);
expect(FEATURE_FLAG_ENVIRONMENT).toEqual(expectedFlags);
});

it("should contain missing entries substituted with default values", async () => {
vi.spyOn(envVarsUtils, "getEnvVars").mockReturnValue({
...environmentVars,
featureFlags: JSON.stringify({}),
});

const { FEATURE_FLAG_ENVIRONMENT } = await import(
"$lib/constants/environment.constants"
);
expect(FEATURE_FLAG_ENVIRONMENT).toEqual(defaultFeatureFlagValues);
});

it("should fallback to default on error", async () => {
const spyConsoleError = vi
.spyOn(console, "error")
.mockImplementation(() => undefined);
vi.spyOn(envVarsUtils, "getEnvVars").mockReturnValue({
...environmentVars,
featureFlags: `{"TEST_FLAG_NOT_EDITABLE": TRUE}`,
});

const { FEATURE_FLAG_ENVIRONMENT } = await import(
"$lib/constants/environment.constants"
);
expect(FEATURE_FLAG_ENVIRONMENT).toEqual(defaultFeatureFlagValues);
expect(spyConsoleError).toBeCalledTimes(1);
expect(spyConsoleError).toBeCalledWith(
"Error parsing featureFlags",
new SyntaxError("Unexpected token T in JSON at position 27")
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { runResolvedPromises } from "$tests/utils/timers.test-utils";
import { NeuronType, NeuronVisibility, type NeuronInfo } from "@dfinity/nns";
import { nonNullish } from "@dfinity/utils";
import { render } from "@testing-library/svelte";
import { vi } from "vitest";

describe("ChangeBulkNeuronVisibilityForm", () => {
const createMockNeuron = ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { NeuronVisibilityRowPo } from "$tests/page-objects/NeuronVisibilityRow.p
import { JestPageObjectElement } from "$tests/page-objects/jest.page-object";
import { ICPToken, TokenAmountV2 } from "@dfinity/utils";
import { render } from "@testing-library/svelte";
import { vi } from "vitest";

describe("NeuronVisibilityRow", () => {
const renderComponent = ({
Expand Down
67 changes: 43 additions & 24 deletions frontend/src/tests/lib/utils/export-to-csv.utils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import {
convertToCsv,
FileSystemAccessError,
convertToCsv,
generateCsvFileToSave,
type CsvHeader,
} from "$lib/utils/export-to-csv.utils";

type TestPersonData = { name: string; age: number };
type TestFormulaData = { formula: string; value: number };

describe("Export to Csv", () => {
beforeEach(() => {
vi.unstubAllGlobals();
Expand All @@ -19,90 +23,105 @@ describe("Export to Csv", () => {

it("should return a string with headers and no content when empty data is provided", () => {
const data = [];
const headers = [{ id: "name" }];
const headers = [{ id: "name", label: "name" }];
const expected = "name";
expect(convertToCsv({ data, headers })).toBe(expected);
});

it("should apply order defined by the headers argument", () => {
const data = [
const data: TestPersonData[] = [
{ name: "Peter", age: 25 },
{ name: "John", age: 30 },
];
const headers: { id: "age" | "name" }[] = [{ id: "age" }, { id: "name" }];
const expected = "age,name\n25,Peter\n30,John";
const headers: CsvHeader<TestPersonData>[] = [
{ id: "age", label: "Age" },
{ id: "name", label: "Name" },
];
const expected = "Age,Name\n25,Peter\n30,John";
expect(convertToCsv({ data, headers })).toBe(expected);
});

it("should handle null, undefined and empty strings ", () => {
const data = [
const data: TestPersonData[] = [
{ name: "Peter", age: undefined },
{ name: null, age: 25 },
{ name: "", age: 22 },
];
const headers: { id: "age" | "name" }[] = [{ id: "name" }, { id: "age" }];
const headers: CsvHeader<TestPersonData>[] = [
{ id: "name", label: "name" },
{ id: "age", label: "age" },
];
const expected = "name,age\nPeter,\n,25\n,22";
expect(convertToCsv({ data, headers })).toBe(expected);
});

it("should handle values containing commas by wrapping them in quotes", () => {
const data = [
const data: TestPersonData[] = [
{ name: "John, Jr.", age: 30 },
{ name: "Jane", age: 25 },
];
const headers: { id: "age" | "name" }[] = [{ id: "name" }, { id: "age" }];
const headers: CsvHeader<TestPersonData>[] = [
{ id: "name", label: "name" },
{ id: "age", label: "age" },
];
const expected = 'name,age\n"John, Jr.",30\nJane,25';
expect(convertToCsv({ data, headers })).toBe(expected);
});

it("should escape double quotes by doubling them", () => {
const data = [
const data: TestPersonData[] = [
{ name: 'John "Johnny" Doe', age: 30 },
{ name: "Jane", age: 25 },
];
const headers: { id: "age" | "name" }[] = [{ id: "name" }, { id: "age" }];
const headers: CsvHeader<TestPersonData>[] = [
{ id: "name", label: "name" },
{ id: "age", label: "age" },
];
const expected = 'name,age\n"John ""Johnny"" Doe",30\nJane,25';
expect(convertToCsv({ data, headers })).toBe(expected);
});

it("should prevent formula injection by prefixing with single quote", () => {
const data = [
const data: TestFormulaData[] = [
{ formula: "=SUM(A1:A10)", value: 100 },
{ formula: "+1234567", value: 200 },
{ formula: "-1234567", value: 300 },
{ formula: "@SUM(A1)", value: 400 },
{ formula: "|MACRO", value: 500 },
];
const headers: { id: "formula" | "value" }[] = [
{ id: "formula" },
{ id: "value" },
const headers: CsvHeader<TestFormulaData>[] = [
{ id: "formula", label: "formula" },
{ id: "value", label: "value" },
];
const expected =
"formula,value\n'=SUM(A1:A10),100\n'+1234567,200\n'-1234567,300\n'@SUM(A1),400\n'|MACRO,500";
expect(convertToCsv({ data, headers })).toBe(expected);
});

it("should handle formula injection and special characters in values", () => {
const data = [
const data: TestFormulaData[] = [
{ formula: "=SUM(A1:A10)", value: 100 },
{ formula: "+1234567,12", value: 200 },
];
const headers: { id: "formula" | "value" }[] = [
{ id: "formula" },
{ id: "value" },
const headers: CsvHeader<TestFormulaData>[] = [
{ id: "formula", label: "formula" },
{ id: "value", label: "value" },
];
const expected = "formula,value\n'=SUM(A1:A10),100\n\"'+1234567,12\",200";
expect(convertToCsv({ data, headers })).toBe(expected);
});

it("should handle values containing newlines by wrapping them in quotes", () => {
const data = [
{ note: "Line 1\nLine 2", id: 1 },
{ note: "Single Line", id: 2 },
const data: TestPersonData[] = [
{ name: "Peter\nParker", age: 24 },
{ name: "Jane Doe", age: 25 },
];
``;
const headers: { id: "note" | "id" }[] = [{ id: "note" }, { id: "id" }];
const expected = 'note,id\n"Line 1\nLine 2",1\nSingle Line,2';
const headers: CsvHeader<TestPersonData>[] = [
{ id: "name", label: "Full Name" },
{ id: "age", label: "Age" },
];
const expected = 'Full Name,Age\n"Peter\nParker",24\nJane Doe,25';
expect(convertToCsv({ data, headers })).toBe(expected);
});
});
Expand Down

0 comments on commit 3168e60

Please sign in to comment.