From 1bd52d58a124a7fc62db930b6158bf350f628606 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Burkard?= <22095555+JeromeBu@users.noreply.github.com> Date: Fri, 31 May 2024 17:00:07 +0200 Subject: [PATCH 1/3] move test helpers --- api/src/core/adapters/hal/getHalSoftware.test.ts | 7 ++----- api/src/rpc/routes.e2e.test.ts | 10 ++++++++++ api/src/tools/test.helpers.ts | 5 +++++ 3 files changed, 17 insertions(+), 5 deletions(-) create mode 100644 api/src/rpc/routes.e2e.test.ts create mode 100644 api/src/tools/test.helpers.ts diff --git a/api/src/core/adapters/hal/getHalSoftware.test.ts b/api/src/core/adapters/hal/getHalSoftware.test.ts index ef17ee3c..17c7fbb0 100644 --- a/api/src/core/adapters/hal/getHalSoftware.test.ts +++ b/api/src/core/adapters/hal/getHalSoftware.test.ts @@ -1,11 +1,8 @@ -import { describe, expect, it } from "vitest"; +import { describe, it } from "vitest"; +import { expectToEqual } from "../../../tools/test.helpers"; import { getHalSoftware } from "./getHalSoftware"; import { getHalSoftwareOptions } from "./getHalSoftwareOptions"; -const expectToEqual = (actual: T, expected: T) => { - expect(actual).toEqual(expected); -}; - describe("HAL", () => { describe("getHalSoftware", () => { it("gets data from Hal and converts it to ExternalSoftware", async () => { diff --git a/api/src/rpc/routes.e2e.test.ts b/api/src/rpc/routes.e2e.test.ts new file mode 100644 index 00000000..5b43c45f --- /dev/null +++ b/api/src/rpc/routes.e2e.test.ts @@ -0,0 +1,10 @@ +import { expectToEqual } from "../tools/test.helpers"; +import { describe, it } from "vitest"; + +describe("RPC e2e tests", () => { + describe("route something", () => { + it("works", () => { + expectToEqual({ not: "bad" }, { not: "good" }); + }); + }); +}); diff --git a/api/src/tools/test.helpers.ts b/api/src/tools/test.helpers.ts new file mode 100644 index 00000000..5a709b08 --- /dev/null +++ b/api/src/tools/test.helpers.ts @@ -0,0 +1,5 @@ +import { expect } from "vitest"; + +export const expectToEqual = (actual: T, expected: T) => { + expect(actual).toEqual(expected); +}; From e1b611ebf3a5aa5cdad867acbda5f80af2ad8993 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Burkard?= <22095555+JeromeBu@users.noreply.github.com> Date: Fri, 31 May 2024 19:00:46 +0200 Subject: [PATCH 2/3] adding api tests --- api/src/core/adapters/dbApi/InMemoryDbApi.ts | 60 ++++++ .../{dbApi.ts => dbApi/createGitDbApi.ts} | 8 +- api/src/core/bootstrap.ts | 27 ++- api/src/core/ports/DbApi.ts | 2 + api/src/rpc/createTestCaller.ts | 49 +++++ api/src/rpc/router.ts | 6 +- api/src/rpc/routes.e2e.test.ts | 181 +++++++++++++++++- api/src/rpc/start.ts | 3 +- api/src/tools/test.helpers.ts | 60 ++++++ web/README.md | 2 - 10 files changed, 378 insertions(+), 20 deletions(-) create mode 100644 api/src/core/adapters/dbApi/InMemoryDbApi.ts rename api/src/core/adapters/{dbApi.ts => dbApi/createGitDbApi.ts} (96%) create mode 100644 api/src/rpc/createTestCaller.ts diff --git a/api/src/core/adapters/dbApi/InMemoryDbApi.ts b/api/src/core/adapters/dbApi/InMemoryDbApi.ts new file mode 100644 index 00000000..e0861b53 --- /dev/null +++ b/api/src/core/adapters/dbApi/InMemoryDbApi.ts @@ -0,0 +1,60 @@ +import { CompiledData } from "../../ports/CompileData"; +import type { Db, DbApi } from "../../ports/DbApi"; + +export class InMemoryDbApi implements DbApi { + #softwareRows: Db.SoftwareRow[] = []; + #agentRows: Db.AgentRow[] = []; + #softwareReferentRows: Db.SoftwareReferentRow[] = []; + #softwareUserRows: Db.SoftwareUserRow[] = []; + #instanceRows: Db.InstanceRow[] = []; + + #compiledData: CompiledData<"private"> = []; + + async fetchDb() { + return { + softwareRows: this.#softwareRows, + agentRows: this.#agentRows, + softwareReferentRows: this.#softwareReferentRows, + softwareUserRows: this.#softwareUserRows, + instanceRows: this.#instanceRows + }; + } + + async updateDb({ newDb }: { newDb: Db }) { + this.#softwareRows = newDb.softwareRows; + this.#agentRows = newDb.agentRows; + this.#softwareReferentRows = newDb.softwareReferentRows; + this.#softwareUserRows = newDb.softwareUserRows; + this.#instanceRows = newDb.instanceRows; + } + + async fetchCompiledData() { + return this.#compiledData; + } + + async updateCompiledData({ newCompiledData }: { newCompiledData: CompiledData<"private"> }) { + this.#compiledData = newCompiledData; + } + + // test helpers + + get softwareRows() { + return this.#softwareRows; + } + + get agentRows() { + return this.#agentRows; + } + + get softwareReferentRows() { + return this.#softwareReferentRows; + } + + get softwareUserRows() { + return this.#softwareUserRows; + } + + get instanceRows() { + return this.#instanceRows; + } +} diff --git a/api/src/core/adapters/dbApi.ts b/api/src/core/adapters/dbApi/createGitDbApi.ts similarity index 96% rename from api/src/core/adapters/dbApi.ts rename to api/src/core/adapters/dbApi/createGitDbApi.ts index c2024c4f..a2566a65 100644 --- a/api/src/core/adapters/dbApi.ts +++ b/api/src/core/adapters/dbApi/createGitDbApi.ts @@ -1,7 +1,7 @@ -import type { DbApi, Db } from "../ports/DbApi"; -import { gitSsh } from "../../tools/gitSsh"; +import type { DbApi, Db } from "../../ports/DbApi"; +import { gitSsh } from "../../../tools/gitSsh"; import { Deferred } from "evt/tools/Deferred"; -import { type CompiledData, compiledDataPrivateToPublic } from "../ports/CompileData"; +import { type CompiledData, compiledDataPrivateToPublic } from "../../ports/CompileData"; import * as fs from "fs"; import { join as pathJoin } from "path"; import type { ReturnType } from "tsafe"; @@ -24,7 +24,7 @@ export type GitDbApiParams = { sshPrivateKey: string; }; -export function createGitDbApi(params: GitDbApiParams): { dbApi: DbApi; initializeDbApiCache: () => Promise } { +export function createGitDbApi(params: GitDbApiParams): Db.DbApiAndInitializeCache { const { dataRepoSshUrl, sshPrivateKeyName, sshPrivateKey } = params; const dbApi: DbApi = { diff --git a/api/src/core/bootstrap.ts b/api/src/core/bootstrap.ts index f7595763..0e40229e 100644 --- a/api/src/core/bootstrap.ts +++ b/api/src/core/bootstrap.ts @@ -1,7 +1,8 @@ import { createCore, createObjectThatThrowsIfAccessed, type GenericCore } from "redux-clean-architecture"; import { createCompileData } from "./adapters/compileData"; import { comptoirDuLibreApi } from "./adapters/comptoirDuLibreApi"; -import { createGitDbApi, type GitDbApiParams } from "./adapters/dbApi"; +import { createGitDbApi, type GitDbApiParams } from "./adapters/dbApi/createGitDbApi"; +import { InMemoryDbApi } from "./adapters/dbApi/InMemoryDbApi"; import { getCnllPrestatairesSill } from "./adapters/getCnllPrestatairesSill"; import { getServiceProviders } from "./adapters/getServiceProviders"; import { createGetSoftwareLatestVersion } from "./adapters/getSoftwareLatestVersion"; @@ -12,15 +13,20 @@ import { getHalSoftwareOptions } from "./adapters/hal/getHalSoftwareOptions"; import { createKeycloakUserApi, type KeycloakUserApiParams } from "./adapters/userApi"; import type { CompileData } from "./ports/CompileData"; import type { ComptoirDuLibreApi } from "./ports/ComptoirDuLibreApi"; -import type { DbApi } from "./ports/DbApi"; +import { DbApi, Db } from "./ports/DbApi"; import type { ExternalDataOrigin, GetSoftwareExternalData } from "./ports/GetSoftwareExternalData"; import type { GetSoftwareExternalDataOptions } from "./ports/GetSoftwareExternalDataOptions"; import type { GetSoftwareLatestVersion } from "./ports/GetSoftwareLatestVersion"; import type { UserApi } from "./ports/UserApi"; import { usecases } from "./usecases"; +type GitDbConfig = { dbKind: "git" } & GitDbApiParams; +type InMemoryDbConfig = { dbKind: "inMemory" }; + +type DbConfig = GitDbConfig | InMemoryDbConfig; + type ParamsOfBootstrapCore = { - gitDbApiParams: GitDbApiParams; + dbConfig: DbConfig; keycloakUserApiParams: KeycloakUserApiParams | undefined; githubPersonalAccessTokenForApiRateLimit: string; doPerPerformPeriodicalCompilation: boolean; @@ -45,9 +51,20 @@ export type State = Core["types"]["State"]; export type Thunks = Core["types"]["Thunks"]; export type CreateEvt = Core["types"]["CreateEvt"]; +const getDbApiAndInitializeCache = (dbConfig: DbConfig): Db.DbApiAndInitializeCache => { + if (dbConfig.dbKind === "git") return createGitDbApi(dbConfig); + if (dbConfig.dbKind === "inMemory") + return { + dbApi: new InMemoryDbApi(), + initializeDbApiCache: async () => {} + }; + const shouldNotBeReached: never = dbConfig; + throw new Error(`Unsupported case: ${shouldNotBeReached}`); +}; + export async function bootstrapCore(params: ParamsOfBootstrapCore): Promise<{ core: Core; context: Context }> { const { - gitDbApiParams, + dbConfig, keycloakUserApiParams, githubPersonalAccessTokenForApiRateLimit, doPerPerformPeriodicalCompilation, @@ -70,7 +87,7 @@ export async function bootstrapCore(params: ParamsOfBootstrapCore): Promise<{ co getServiceProviders }); - const { dbApi, initializeDbApiCache } = createGitDbApi(gitDbApiParams); + const { dbApi, initializeDbApiCache } = getDbApiAndInitializeCache(dbConfig); const { userApi, initializeUserApiCache } = keycloakUserApiParams === undefined diff --git a/api/src/core/ports/DbApi.ts b/api/src/core/ports/DbApi.ts index bb1b048b..e1297672 100644 --- a/api/src/core/ports/DbApi.ts +++ b/api/src/core/ports/DbApi.ts @@ -95,6 +95,8 @@ export namespace Db { referencedSinceTime: number; updateTime: number; }; + + export type DbApiAndInitializeCache = { dbApi: DbApi; initializeDbApiCache: () => Promise }; } export type Os = "windows" | "linux" | "mac" | "android" | "ios"; diff --git a/api/src/rpc/createTestCaller.ts b/api/src/rpc/createTestCaller.ts new file mode 100644 index 00000000..73e32c64 --- /dev/null +++ b/api/src/rpc/createTestCaller.ts @@ -0,0 +1,49 @@ +import { bootstrapCore } from "../core"; +import { InMemoryDbApi } from "../core/adapters/dbApi/InMemoryDbApi"; +import { ExternalDataOrigin } from "../core/ports/GetSoftwareExternalData"; +import { createRouter } from "./router"; +import { User } from "./user"; + +type TestCallerConfig = { + user: User | undefined; +}; + +export const defaultUser: User = { + id: "1", + email: "default.user@mail.com", + organization: "Default Organization" +}; + +export type ApiCaller = Awaited>["apiCaller"]; + +export const createTestCaller = async ({ user }: TestCallerConfig = { user: defaultUser }) => { + const externalSoftwareDataOrigin: ExternalDataOrigin = "wikidata"; + + const { core, context } = await bootstrapCore({ + "dbConfig": { dbKind: "inMemory" }, + "keycloakUserApiParams": undefined, + "githubPersonalAccessTokenForApiRateLimit": "fake-token", + "doPerPerformPeriodicalCompilation": false, + "doPerformCacheInitialization": false, + "externalSoftwareDataOrigin": externalSoftwareDataOrigin + }); + + const jwtClaimByUserKey = { + "id": "sub", + "email": "email", + "organization": "organization" + }; + + const { router } = createRouter({ + core, + coreContext: context, + keycloakParams: undefined, + redirectUrl: undefined, + externalSoftwareDataOrigin, + readmeUrl: "http://readme.url", + termsOfServiceUrl: "http://terms.url", + jwtClaimByUserKey + }); + + return { apiCaller: router.createCaller({ user }), inMemoryDb: context.dbApi as InMemoryDbApi }; +}; diff --git a/api/src/rpc/router.ts b/api/src/rpc/router.ts index 594517ba..d9c2dab1 100644 --- a/api/src/rpc/router.ts +++ b/api/src/rpc/router.ts @@ -10,7 +10,7 @@ import { assert } from "tsafe/assert"; import { z } from "zod"; import type { Context as CoreContext, Core } from "../core"; import { ExternalDataOrigin, Language, languages, type LocalizedString } from "../core/ports/GetSoftwareExternalData"; -import type { +import { DeclarationFormData, InstanceFormData, Os, @@ -373,8 +373,8 @@ export function createRouter(params: { return { agent }; }), - "getAllowedEmailRegexp": loggedProcedure.query(coreContext.userApi.getAllowedEmailRegexp), - "getAllOrganizations": loggedProcedure.query(coreContext.userApi.getAllOrganizations), + "getAllowedEmailRegexp": loggedProcedure.query(() => coreContext.userApi.getAllowedEmailRegexp()), + "getAllOrganizations": loggedProcedure.query(() => coreContext.userApi.getAllOrganizations()), "changeAgentOrganization": loggedProcedure .input( z.object({ diff --git a/api/src/rpc/routes.e2e.test.ts b/api/src/rpc/routes.e2e.test.ts index 5b43c45f..f603c775 100644 --- a/api/src/rpc/routes.e2e.test.ts +++ b/api/src/rpc/routes.e2e.test.ts @@ -1,10 +1,181 @@ -import { expectToEqual } from "../tools/test.helpers"; -import { describe, it } from "vitest"; +import { beforeAll, describe, expect, it } from "vitest"; +import { InMemoryDbApi } from "../core/adapters/dbApi/InMemoryDbApi"; +import { CompiledData } from "../core/ports/CompileData"; +import { + createDeclarationFormData, + createInstanceFormData, + createSoftwareFormData, + expectToEqual, + expectToMatchObject +} from "../tools/test.helpers"; +import { ApiCaller, createTestCaller, defaultUser } from "./createTestCaller"; + +const expectedSoftwareId = 1; + +const softwareFormData = createSoftwareFormData(); +const declarationFormData = createDeclarationFormData(); +const instanceFormData = createInstanceFormData({ mainSoftwareSillId: expectedSoftwareId }); describe("RPC e2e tests", () => { - describe("route something", () => { - it("works", () => { - expectToEqual({ not: "bad" }, { not: "good" }); + let apiCaller: ApiCaller; + let inMemoryDb: InMemoryDbApi; + + describe("getAgents - wrong paths", () => { + it("fails with UNAUTHORIZED if user is not logged in", async () => { + ({ apiCaller, inMemoryDb } = await createTestCaller({ user: undefined })); + expect(apiCaller.getAgents()).rejects.toThrow("UNAUTHORIZED"); + }); + }); + + describe("createUserOrReferent - Wrong paths", () => { + it("fails with UNAUTHORIZED if user is not logged in", async () => { + ({ apiCaller, inMemoryDb } = await createTestCaller({ user: undefined })); + expect( + apiCaller.createUserOrReferent({ + formData: declarationFormData, + softwareName: "Some software" + }) + ).rejects.toThrow("UNAUTHORIZED"); + }); + + it("fails when software is not found in SILL", async () => { + ({ apiCaller, inMemoryDb } = await createTestCaller()); + expect( + apiCaller.createUserOrReferent({ + formData: declarationFormData, + softwareName: "Some software" + }) + ).rejects.toThrow("Software not in SILL"); + }); + }); + + describe("createSoftware - Wrong paths", () => { + it("fails with UNAUTHORIZED if user is not logged in", async () => { + ({ apiCaller, inMemoryDb } = await createTestCaller({ user: undefined })); + expect( + apiCaller.createSoftware({ + formData: softwareFormData + }) + ).rejects.toThrow("UNAUTHORIZED"); + }); + }); + + // ⚠️ reminder : you need to run the whole scenarios + // because those tests are not isolated + // (the order is important)⚠️ + describe("Scenario - Add a new software then mark an agent as user of this software", () => { + beforeAll(async () => { + ({ apiCaller, inMemoryDb } = await createTestCaller()); + }); + + it("gets the list of agents, which is initially empty", async () => { + const { agents } = await apiCaller.getAgents(); + expect(agents).toHaveLength(0); + }); + + it("adds a new software", async () => { + expect(inMemoryDb.softwareRows).toHaveLength(0); + const initialSoftwares = await apiCaller.getSoftwares(); + expectToEqual(initialSoftwares, []); + + await apiCaller.createSoftware({ + formData: softwareFormData + }); + expect(inMemoryDb.softwareRows).toHaveLength(1); + const expectedSoftware: Partial> = { + "description": softwareFormData.softwareDescription, + "externalId": softwareFormData.externalId, + "doRespectRgaa": softwareFormData.doRespectRgaa, + "isFromFrenchPublicService": softwareFormData.isFromFrenchPublicService, + "isPresentInSupportContract": softwareFormData.isPresentInSupportContract, + "keywords": softwareFormData.softwareKeywords, + "license": softwareFormData.softwareLicense, + "logoUrl": softwareFormData.softwareLogoUrl, + "name": softwareFormData.softwareName, + "softwareType": softwareFormData.softwareType, + "versionMin": softwareFormData.softwareMinimalVersion, + "testUrls": [], + "workshopUrls": [], + "categories": [], + "isStillInObservation": false, + "id": expectedSoftwareId + }; + + expectToMatchObject(inMemoryDb.softwareRows[0], { + ...expectedSoftware, + "addedByAgentEmail": defaultUser.email, + "similarSoftwareExternalDataIds": softwareFormData.similarSoftwareExternalDataIds + }); + }); + + it("gets the list of agents, which now has the user which added the software", async () => { + const { agents } = await apiCaller.getAgents(); + expect(agents).toHaveLength(1); + expectToMatchObject(agents[0], { + "email": defaultUser.email, + "organization": defaultUser.organization + }); + }); + + it("gets the new software in the list", async () => { + const softwares = await apiCaller.getSoftwares(); + expect(softwares).toHaveLength(1); + expectToMatchObject(softwares[0], { softwareName: softwareFormData.softwareName }); + }); + + it("adds an agent as user of the software", async () => { + expect(inMemoryDb.agentRows).toHaveLength(1); + expect(inMemoryDb.softwareRows).toHaveLength(1); + expect(inMemoryDb.softwareUserRows).toHaveLength(0); + await apiCaller.createUserOrReferent({ + formData: declarationFormData, + softwareName: "Some software" + }); + + if (declarationFormData.declarationType !== "user") + throw new Error("This test is only for user declaration"); + + expect(inMemoryDb.softwareUserRows).toHaveLength(1); + + expectToEqual(inMemoryDb.softwareUserRows[0], { + "agentEmail": defaultUser.email, + "softwareId": inMemoryDb.softwareRows[0].id, + "os": declarationFormData.os, + "serviceUrl": declarationFormData.serviceUrl, + "useCaseDescription": declarationFormData.usecaseDescription, + "version": declarationFormData.version + }); + }); + + it("adds an instance of the software", async () => { + expect(inMemoryDb.softwareRows).toHaveLength(1); + expect(inMemoryDb.instanceRows).toHaveLength(0); + await apiCaller.createInstance({ + formData: instanceFormData + }); + expect(inMemoryDb.instanceRows).toHaveLength(1); + expectToMatchObject(inMemoryDb.instanceRows[0], { + "id": 1, + "addedByAgentEmail": defaultUser.email, + "mainSoftwareSillId": expectedSoftwareId, + "organization": instanceFormData.organization, + "otherSoftwareWikidataIds": instanceFormData.otherSoftwareWikidataIds, + "publicUrl": instanceFormData.publicUrl, + "targetAudience": instanceFormData.targetAudience + }); + }); + + it("gets the new instances in the list", async () => { + const instances = await apiCaller.getInstances(); + expect(instances).toHaveLength(1); + expectToMatchObject(instances[0], { + "id": 1, + "mainSoftwareSillId": expectedSoftwareId, + "organization": instanceFormData.organization, + "otherWikidataSoftwares": [], + "publicUrl": instanceFormData.publicUrl, + "targetAudience": instanceFormData.targetAudience + }); }); }); }); diff --git a/api/src/rpc/start.ts b/api/src/rpc/start.ts index 45b02228..b7c15752 100644 --- a/api/src/rpc/start.ts +++ b/api/src/rpc/start.ts @@ -55,7 +55,8 @@ export async function startRpcService(params: { console.log({ isDevEnvironnement }); const { core, context: coreContext } = await bootstrapCore({ - "gitDbApiParams": { + "dbConfig": { + "dbKind": "git", dataRepoSshUrl, "sshPrivateKeyName": sshPrivateKeyForGitName, "sshPrivateKey": sshPrivateKeyForGit diff --git a/api/src/tools/test.helpers.ts b/api/src/tools/test.helpers.ts index 5a709b08..2df9aec2 100644 --- a/api/src/tools/test.helpers.ts +++ b/api/src/tools/test.helpers.ts @@ -1,5 +1,65 @@ import { expect } from "vitest"; +import { Db } from "../core/ports/DbApi"; +import { DeclarationFormData, InstanceFormData, SoftwareFormData } from "../core/usecases/readWriteSillData"; export const expectToEqual = (actual: T, expected: T) => { expect(actual).toEqual(expected); }; + +export const expectToMatchObject = (actual: T, expected: Partial) => { + expect(actual).toMatchObject(expected); +}; + +const makeObjectFactory = + (defaultValue: T) => + (overloads: Partial = {}): T => ({ + ...defaultValue, + ...overloads + }); + +export const createAgent = makeObjectFactory({ + about: "About the default agent", + email: "default.agent@mail.com", + organization: "Default Organization", + isPublic: true +}); + +export const createDeclarationFormData = makeObjectFactory({ + declarationType: "user", + os: "mac", + serviceUrl: "https://example.com", + usecaseDescription: "My description", + version: "1" +}); + +export const createSoftwareFormData = makeObjectFactory({ + softwareType: { + type: "desktop/mobile", + os: { + windows: true, + linux: true, + mac: true, + android: false, + ios: false + } + }, + externalId: "Q171985", + comptoirDuLibreId: undefined, + softwareName: "Some software", + softwareDescription: "Some software description", + softwareLicense: "Some software license", + softwareMinimalVersion: "1.0.0", + isPresentInSupportContract: true, + isFromFrenchPublicService: true, + similarSoftwareExternalDataIds: ["some-external-id"], + softwareLogoUrl: "https://example.com/logo.png", + softwareKeywords: ["some", "keywords"], + doRespectRgaa: true +}); +export const createInstanceFormData = makeObjectFactory({ + organization: "Default organization", + targetAudience: "Default audience", + mainSoftwareSillId: 1, + otherSoftwareWikidataIds: [], + publicUrl: "https://example.com" +}); diff --git a/web/README.md b/web/README.md index 562d3129..b4374022 100644 --- a/web/README.md +++ b/web/README.md @@ -23,5 +23,3 @@ Ce dépôt et l'ensemble des dépôts liés à l'application [SILL](https://code # [Contribuer](CONTRIBUTING.md) 🧢 # Licence - - From a521225502dca10e2475cd7cd841b2ff7a6ed6d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Burkard?= <22095555+JeromeBu@users.noreply.github.com> Date: Fri, 7 Jun 2024 16:41:21 +0200 Subject: [PATCH 3/3] bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d5e8f46d..76e9e03c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sill", - "version": "1.42.35", + "version": "1.42.36", "license": "MIT", "private": true, "scripts": {