Skip to content

Commit

Permalink
chore: add purify-ts in the template module
Browse files Browse the repository at this point in the history
  • Loading branch information
valpinkman committed Jan 25, 2024
1 parent 4138604 commit 443b107
Showing 16 changed files with 372 additions and 84 deletions.
1 change: 1 addition & 0 deletions packages/core/jest.config.ts
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ import type { JestConfigWithTsJest } from "@ledgerhq/jest-config-dsdk";
const config: JestConfigWithTsJest = {
preset: "@ledgerhq/jest-config-dsdk",
setupFiles: ["<rootDir>/jest.setup.ts"],
testPathIgnorePatterns: ["<rootDir>/lib/"],
collectCoverageFrom: [
// TODO: remove internal when the rest of the files are setup
"src/internal/**/*.ts",
5 changes: 4 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -12,13 +12,16 @@
"dev": "tsc --watch",
"lint": "eslint --cache --ext .ts \"src\"",
"lint:fix": "eslint --cache --fix --ext .ts \"src\"",
"test:unit": "jest --coverage src",
"test": "jest src",
"test:watch": "pnpm test -- --watch",
"test:coverage": "pnpm test -- --coverage",
"module:init": "zx scripts/add-module.mjs",
"run:index": "ts-node src/index.ts"
},
"dependencies": {
"inversify": "^6.0.2",
"inversify-logger-middleware": "^3.1.0",
"purify-ts": "^2.0.3",
"reflect-metadata": "^0.2.1"
},
"devDependencies": {
5 changes: 4 additions & 1 deletion packages/core/src/di.ts
Original file line number Diff line number Diff line change
@@ -12,7 +12,10 @@ export const makeContainer = ({
}: Partial<MakeContainerProps> = {}) => {
const container = new Container();
container.applyMiddleware(logger);
container.load(configModuleFactory({ mock }));
container.load(
configModuleFactory({ mock })
// modules go here
);

return container;
};
6 changes: 4 additions & 2 deletions packages/core/src/internal/config/data/ConfigDataSource.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Either } from "purify-ts";
import { LocalConfigFailure, RemoteConfigFailure } from "../di/configTypes";
import { Config } from "../model/Config";

// Describe the different data sources interfaces our application could have

export interface LocalConfigDataSource {
getConfig(): Config;
getConfig(): Either<LocalConfigFailure, Config>;
}

export interface RemoteConfigDataSource {
getConfig(): Promise<Config>;
getConfig(): Promise<Either<RemoteConfigFailure, Config>>;
}
2 changes: 1 addition & 1 deletion packages/core/src/internal/config/data/Dto.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Types used by the data sources (here an example for the remote data source)
// They will usually be the response of an API call that will need to be parsed
// into an object used by the application. (in our example: DTOConfig => Config)
// into an object used by the application. (in our example: ConfigDto => Config)

export type ConfigDto = {
version: string;
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { injectable } from "inversify";
import { Config } from "../model/Config";
import { LocalConfigDataSource } from "./ConfigDataSource";
import { Either } from "purify-ts";

/**
*
@@ -10,10 +11,10 @@ import { LocalConfigDataSource } from "./ConfigDataSource";

@injectable()
export class StubLocalConfigDataSource implements LocalConfigDataSource {
getConfig(): Config {
return {
getConfig(): Either<never, Config> {
return Either.of({
name: "DeviceSDK",
version: "0.0.0-mock.1",
};
});
}
}
Original file line number Diff line number Diff line change
@@ -1,43 +1,64 @@
import fs from "fs";
import { LocalConfigDataSource } from "./ConfigDataSource";
import { FileLocalConfigDataSource } from "./LocalConfigDataSource";
import { StubLocalConfigDataSource } from "./LocalConfigDataSource.stub";
import { Either, Left } from "purify-ts";
import { JSONParseError, ReadFileError } from "../di/configTypes";

const readFileSyncSpy = jest.spyOn(fs, "readFileSync");
const jsonParse = jest.spyOn(JSON, "parse");

let datasource: LocalConfigDataSource;
describe("LocalConfigDataSource", () => {
describe("FileLocalConfigDataSource", () => {
beforeEach(() => {
readFileSyncSpy.mockClear();
jsonParse.mockClear();
datasource = new FileLocalConfigDataSource();
});

it("should return an Either<never, Config>", () => {
readFileSyncSpy.mockReturnValue(
JSON.stringify({ name: "DeviceSDK", version: "0.0.0-spied.1" })
);
});

it("should return the config", () => {
expect(datasource.getConfig()).toEqual({
jsonParse.mockReturnValue({
name: "DeviceSDK",
version: "0.0.0-spied.1",
});

expect(datasource.getConfig()).toStrictEqual(
Either.of({
name: "DeviceSDK",
version: "0.0.0-spied.1",
})
);
});
});

describe("StubLocalConfigDataSource", () => {
beforeEach(() => {
datasource = new StubLocalConfigDataSource();
it("should return an Either<ReadFileError, never> if readFileSync throws", () => {
const err = new Error("readFileSync error");
readFileSyncSpy.mockImplementation(() => {
throw err;
});

expect(datasource.getConfig()).toEqual(Left(new ReadFileError(err)));
});

it("should return the config", () => {
expect(datasource.getConfig()).toEqual({
name: "DeviceSDK",
version: "0.0.0-mock.1",
it("should return an Either<JSONParseError, never> if JSON.parse throws", () => {
const err = new Error("JSON.parse error");
readFileSyncSpy.mockReturnValue(
JSON.stringify({ name: "DeviceSDK", version: "0.0.0-spied.1" })
);

jsonParse.mockImplementation(() => {
throw err;
});

expect(datasource.getConfig()).toEqual(Left(new JSONParseError(err)));
});
});

afterAll(() => {
readFileSyncSpy.mockClear();
readFileSyncSpy.mockRestore();
jsonParse.mockRestore();
});
});
30 changes: 23 additions & 7 deletions packages/core/src/internal/config/data/LocalConfigDataSource.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import fs from "fs";
import { injectable } from "inversify";
import path from "path";
import { Either } from "purify-ts";
import {
ReadFileError,
JSONParseError,
LocalConfigFailure,
} from "../di/configTypes";
import { Config } from "../model/Config";
import { LocalConfigDataSource } from "./ConfigDataSource";
import path from "path";

/**
*
@@ -12,11 +18,21 @@ import path from "path";
*/
@injectable()
export class FileLocalConfigDataSource implements LocalConfigDataSource {
getConfig(): Config {
const version = fs.readFileSync(
path.join(__dirname, "version.json"),
"utf-8"
);
return JSON.parse(version) as Config;
getConfig(): Either<LocalConfigFailure, Config> {
return Either.encase(() =>
fs.readFileSync(path.join(__dirname, "version.json"), "utf-8")
)
.mapLeft((error) => {
console.log("readFileSync error");
return new ReadFileError(error);
})
.chain((str) => {
return Either.encase(() => JSON.parse(str) as Config).mapLeft(
(error) => {
console.log("JSON.parse error");
return new JSONParseError(error);
}
);
});
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import { injectable } from "inversify";
import { RemoteConfigDataSource } from "./ConfigDataSource";
import { Config } from "../model/Config";
import { Either } from "purify-ts";

/**
* class RemoteRestConfigDataSource
* This is a remote data source that reads the config from a remote API (example).
*/
@injectable()
export class StubRemoteConfigDataSource implements RemoteConfigDataSource {
async getConfig(): Promise<Config> {
async getConfig(): Promise<Either<never, Config>> {
return new Promise((res) =>
res({
name: "DeviceSDK",
version: "0.0.0-fake.2",
})
res(
Either.of({
name: "DeviceSDK",
version: "0.0.0-fake.2",
})
)
);
}
}
146 changes: 132 additions & 14 deletions packages/core/src/internal/config/data/RemoteConfigDataSource.test.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,148 @@
import { Either, Left } from "purify-ts";
import { RemoteConfigDataSource } from "./ConfigDataSource";
import { RestRemoteConfigDataSource } from "./RemoteConfigDataSource";
import { StubRemoteConfigDataSource } from "./RemoteConfigDataSource.stub";
import {
ApiCallError,
JSONParseError,
ParseResponseError,
} from "../di/configTypes";

let datasource: RemoteConfigDataSource;
const callApiSpy = jest.spyOn(
RestRemoteConfigDataSource.prototype as any,
"_callApi"
);
const parseResponseSpy = jest.spyOn(
RestRemoteConfigDataSource.prototype as any,
"_parseResponse"
);

describe("RemoteRestConfigDataSource", () => {
describe("RestRemoteConfigDataSource", () => {
beforeAll(() => {
beforeEach(() => {
callApiSpy.mockClear();
parseResponseSpy.mockClear();
datasource = new RestRemoteConfigDataSource();
});

it("should return the config", async () => {
expect(await datasource.getConfig()).toStrictEqual({
name: "DeviceSDK",
version: "0.0.0-fake.1",
it("should return an Either<never, Config>", async () => {
callApiSpy.mockResolvedValue(
Either.of({
ok: true,
json: () =>
Promise.resolve(
Either.of({ name: "DeviceSDK", version: "0.0.0-fake.1" })
),
})
);

parseResponseSpy.mockReturnValue(
Either.of({
name: "DeviceSDK",
version: "0.0.0-fake.1",
})
);

expect(await datasource.getConfig()).toStrictEqual(
Either.of({
name: "DeviceSDK",
version: "0.0.0-fake.1",
})
);
});

it("should return an Either<ApiCallError, never> if _callApi throws", async () => {
const err = new Error("_callApi error");
callApiSpy.mockResolvedValue(Left(err));

expect(await datasource.getConfig()).toStrictEqual(
Left(new ApiCallError(err))
);
});

it("should return an Either<ApiCallError, never> if _callApi returns a non-ok response", async () => {
callApiSpy.mockResolvedValue(
Either.of({
ok: false,
json: () =>
Promise.resolve(
Either.of({ name: "DeviceSDK", version: "0.0.0-fake.1" })
),
})
);

expect(await datasource.getConfig()).toStrictEqual(
Left(new ApiCallError(new Error("response not ok")))
);
});

it("should return an Either<JSONParseError, never> if deserializing json fails", async () => {
const err = new Error("deserializing json failure");
callApiSpy.mockResolvedValue(
Either.of({
ok: true,
json: () => Promise.resolve(Left(err)),
})
);

expect(await datasource.getConfig()).toStrictEqual(
Left(new JSONParseError())
);
});

it("should return an Either<ParseResponseError, never> if _parseResponse throws", async () => {
callApiSpy.mockResolvedValue(
Either.of({
ok: true,
json: () =>
Promise.resolve(
Either.of({ name: "DeviceSDK", version: "0.0.0-fake.1" })
),
})
);

parseResponseSpy.mockImplementation(() => {
return Left(new ParseResponseError());
});

expect(await datasource.getConfig()).toStrictEqual(
Left(new ParseResponseError())
);
});
});
describe("MockRemoteConfigDataSource", () => {
beforeAll(() => {
datasource = new StubRemoteConfigDataSource();

it("should return an Either<ParseResponseError, never> if `name` is missing in Dto", async () => {
parseResponseSpy.mockRestore();
callApiSpy.mockResolvedValue(
Either.of({
ok: true,
json: () =>
Promise.resolve(
Either.of({
version: "0.0.0-fake.1",
yolo: "yolo",
})
),
})
);

expect(await datasource.getConfig()).toStrictEqual(
Left(new ParseResponseError())
);
});

it("should return the config", async () => {
expect(await datasource.getConfig()).toStrictEqual({
name: "DeviceSDK",
version: "0.0.0-fake.2",
describe("without private methods spy", () => {
beforeEach(() => {
callApiSpy.mockRestore();
parseResponseSpy.mockRestore();
});

it("should return an Either<never, Config>", async () => {
expect(await datasource.getConfig()).toStrictEqual(
Either.of({
name: "DeviceSDK",
version: "0.0.0-fake.1",
})
);
});
});
});
Loading

0 comments on commit 443b107

Please sign in to comment.