Skip to content

Commit

Permalink
[#IOPID-1450] Enable strict mode on repo (#231)
Browse files Browse the repository at this point in the history
* [#IOPID-1450] enable noImplicitThis

* [#IOPID-1450] enable alwaysStrict

* [#IOPID-1450] enable strictBindCallApply

* [#IOPID-1450] enable strictFunctionTypes

* [#IOPID-1450] enable noImplicitAny and useUnknownInCatchVariables

* [#IOPID-1450] enable strictNullChecks

* [#IOPID-1450] enable strictPropertyInitialization

* [#IOPID-1450] enable strict

* Update DeleteUserDataActivity/utils.ts

Co-authored-by: Rodolfo Viti <62432865+rodoviti@users.noreply.github.com>

* [#IOPID-1450] add tests for isCosmosErrors

* [#IOPID-1450] refactor based on comments, part 1

* [#IOPID-1450] refactor based on comments, part 2

---------

Co-authored-by: Rodolfo Viti <62432865+rodoviti@users.noreply.github.com>
  • Loading branch information
gquadrati and rodoviti authored Mar 19, 2024
1 parent e852781 commit 0337e96
Show file tree
Hide file tree
Showing 51 changed files with 461 additions and 224 deletions.
7 changes: 4 additions & 3 deletions CreateDevelopmentProfile/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,10 @@ export function toExtendedProfile(profile: RetrievedProfile): ExtendedProfile {
accepted_tos_version: profile.acceptedTosVersion,
blocked_inbox_or_channels: profile.blockedInboxOrChannels,
email: profile.email,
is_email_already_taken: undefined,
is_email_enabled: profile.isEmailEnabled,
is_email_validated: profile.isEmailValidated,
// NOTE: We do NOT check email uniqueness in this context
is_email_already_taken: false,
is_email_enabled: profile.isEmailEnabled !== false,
is_email_validated: profile.isEmailValidated !== false,
is_inbox_enabled: profile.isInboxEnabled === true,
is_webhook_enabled: profile.isWebhookEnabled === true,
preferred_languages: profile.preferredLanguages,
Expand Down
2 changes: 1 addition & 1 deletion CreateSubscription/__tests__/handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const aFakeApimProductContract: ProductContract = {
const aFakeApimSubscriptionContract: SubscriptionContract = {
allowTracing: false,
createdDate: new Date(),
displayName: null,
displayName: undefined,
endDate: undefined,
expirationDate: undefined,
id: "subscription-id",
Expand Down
3 changes: 3 additions & 0 deletions CreateUser/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ export function CreateUserHandler(
taskResults.apimClient.user.createOrUpdate(
azureApimConfig.apimResourceGroup,
azureApimConfig.apim,
// TODO: Implement a validation step to ensure the existence of `objectId`
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
taskResults.objectId,
{
email: userPayload.email,
Expand Down
52 changes: 40 additions & 12 deletions DeleteUserDataActivity/__tests__/backupAndDelete.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,17 @@ import { IBlobServiceInfo } from "../types";
import { AuthenticationLockServiceMock } from "../../__mocks__/authenticationLockService.mock";
import { IProfileEmailWriter } from "@pagopa/io-functions-commons/dist/src/utils/unique_email_enforcement";

const asyncIteratorOf = <T>(items: T[]): AsyncIterator<T[]> => {
const data = [...items];
return {
next: async () => {
const value = data.shift();
return {
done: typeof value === "undefined",
value: [value]
};
}
};
};
export async function* asyncIteratorOf<T>(items: T[]) {
for (const item of items) {
yield [item];
}
}

export async function* errorMessageIterator(error: any) {
//Sonarcloud requires at least one `yield` before `throw` operation
yield [E.right(aRetrievedMessageWithContent)];
throw error;
}

// MessageContentBlobService
const messageContentBlobService = ({} as unknown) as BlobService;
Expand Down Expand Up @@ -289,6 +288,35 @@ describe(`backupAndDeleteAllUserData`, () => {
expect(E.isRight(result)).toBe(true);
});

it("should stop if an error occurred retrieving messages", async () => {
const cosmosError = { kind: "COSMOS_ERROR_RESPONSE" };
mockFindMessages.mockImplementationOnce(() =>
TE.of(errorMessageIterator(cosmosError))
);

const result = await backupAndDeleteAllUserData({
authenticationLockService,
messageContentBlobService,
messageModel,
messageStatusModel,
messageViewModel,
notificationModel,
notificationStatusModel,
profileEmailsRepository,
profileModel,
servicePreferencesModel,
userDataBackup,
fiscalCode: aFiscalCode
})();

expect(result).toEqual(
E.left({
kind: "QUERY_FAILURE",
reason: `CosmosError: ${JSON.stringify(cosmosError)}`
})
);
});

it("should stop if there is an error while looking for a message View (404)", async () => {
mockFindMessageView.mockImplementationOnce(() =>
TE.left({
Expand Down
40 changes: 40 additions & 0 deletions DeleteUserDataActivity/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { CosmosErrors } from "@pagopa/io-functions-commons/dist/src/utils/cosmosdb_model";

import { isCosmosErrors } from "../utils";

describe("utils", () => {
// --------------
// Just a bunch of types needed for creating a tuple from an union type
// See https://www.hacklewayne.com/typescript-convert-union-to-tuple-array-yes-but-how
type Contra<T> = T extends any ? (arg: T) => void : never;
type InferContra<T> = [T] extends [(arg: infer I) => void] ? I : never;
type PickOne<T> = InferContra<InferContra<Contra<Contra<T>>>>;
type Union2Tuple<T> = PickOne<T> extends infer U // assign PickOne<T> to U
? Exclude<T, U> extends never // T and U are the same
? [T]
: [...Union2Tuple<Exclude<T, U>>, U] // recursion
: never;
// --------------

type CosmosErrorsTypesTuple = Union2Tuple<CosmosErrors["kind"]>;

// NOTE: If a new cosmos error is added, the following initialization will not compile,
// forcing us to update `CosmosErrorsTypes` with the new value
const values: CosmosErrorsTypesTuple = [
"COSMOS_EMPTY_RESPONSE",
"COSMOS_CONFLICT_RESPONSE",
"COSMOS_DECODING_ERROR",
"COSMOS_ERROR_RESPONSE"
];

it.each(values)(
"isCosmosErrors should return true if error is a CosmosError of type %s",
v => {
expect(isCosmosErrors({ kind: v })).toBe(true);
}
);

it("isCosmosErrors should return false if error is not a CosmosError", () => {
expect(isCosmosErrors({ kind: "ANOTHER_ERROR" })).toBe(false);
});
});
36 changes: 21 additions & 15 deletions DeleteUserDataActivity/backupAndDelete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ import * as crypto from "crypto";

import { BlobService } from "azure-storage";
import { sequenceT } from "fp-ts/lib/Apply";
import * as A from "fp-ts/lib/Array";
import * as ROA from "fp-ts/lib/ReadonlyArray";
import * as E from "fp-ts/lib/Either";
import * as O from "fp-ts/lib/Option";
import * as TE from "fp-ts/lib/TaskEither";

import { array, flatten, rights } from "fp-ts/lib/Array";
import { MessageContent } from "@pagopa/io-functions-commons/dist/generated/definitions/MessageContent";
import {
RetrievedMessage,
Expand Down Expand Up @@ -40,6 +39,7 @@ import {
QueryFailure
} from "./types";
import {
isCosmosErrors,
saveDataToBlob,
toDocumentDeleteFailure,
toQueryFailure
Expand Down Expand Up @@ -67,18 +67,18 @@ const executeRecursiveBackupAndDelete = <T>(
TE.mapLeft(toQueryFailure),
TE.chainW(e =>
e.done
? TE.of([])
? TE.of<DataFailure, ReadonlyArray<T>>([])
: e.value.some(E.isLeft)
? TE.left(
? TE.left<DataFailure, ReadonlyArray<T>>(
toQueryFailure(new Error("Some elements are not typed correctly"))
)
: TE.of(rights(e.value))
: TE.of<DataFailure, ReadonlyArray<T>>(ROA.rights(e.value))
),
// executes backup&delete for this set of items
TE.chainW(items =>
pipe(
items,
A.map((item: T) =>
ROA.map((item: T) =>
pipe(
sequenceT(TE.ApplicativeSeq)<
DataFailure,
Expand All @@ -104,8 +104,8 @@ const executeRecursiveBackupAndDelete = <T>(
TE.map(([_, __, nextResults]) => [item, ...nextResults])
)
),
A.sequence(TE.ApplicativePar),
TE.map(flatten)
ROA.sequence(TE.ApplicativePar),
TE.map(ROA.flatten)
)
)
);
Expand Down Expand Up @@ -365,7 +365,7 @@ const backupAndDeleteMessageView = ({
}): TE.TaskEither<DataFailure, O.Option<RetrievedMessageView>> =>
pipe(
messageViewModel.find([message.id, message.fiscalCode]),
TE.chain(TE.fromOption(() => undefined)),
TE.chainW(TE.fromOption(() => undefined)),
TE.foldW(
_ =>
// unfortunately, a document not found is threated like a query error
Expand Down Expand Up @@ -443,7 +443,7 @@ const backupAndDeleteMessageContent = ({
}): TE.TaskEither<DataFailure, O.Option<MessageContent>> =>
pipe(
messageModel.getContentFromBlob(messageContentBlobService, message.id),
TE.chain(TE.fromOption(() => undefined)),
TE.chainW(TE.fromOption(() => undefined)),
TE.foldW(
_ =>
// unfortunately, a document not found is threated like a query error
Expand Down Expand Up @@ -587,19 +587,25 @@ const backupAndDeleteAllMessagesData = ({
pipe(
messageModel.findMessages(fiscalCode),
TE.mapLeft(toQueryFailure),
TE.chain(iter =>
TE.tryCatch(() => asyncIteratorToArray(iter), toQueryFailure)
TE.chainW(iter =>
TE.tryCatch(
() => asyncIteratorToArray(iter),
err =>
err instanceof Error || isCosmosErrors(err)
? toQueryFailure(err)
: toQueryFailure(E.toError(err))
)
),
TE.map(flatten),
TE.map(ROA.flatten),
TE.chainW(results =>
results.some(E.isLeft)
? TE.left(
toQueryFailure(
new Error("Cannot decode some element due to decoding errors")
)
)
: array.sequence(TE.ApplicativeSeq)(
rights(results).map(message => {
: ROA.sequence(TE.ApplicativeSeq)(
ROA.rights(results).map(message => {
// cast needed because findMessages has a wrong signature
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const retrievedMessage = (message as any) as RetrievedMessageWithoutContent;
Expand Down
18 changes: 18 additions & 0 deletions DeleteUserDataActivity/utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import * as t from "io-ts";
import { Context } from "@azure/functions";
import { BlobService } from "azure-storage";
import * as TE from "fp-ts/lib/TaskEither";
import { CosmosErrors } from "@pagopa/io-functions-commons/dist/src/utils/cosmosdb_model";
import { pipe } from "fp-ts/lib/function";

import { enumType } from "@pagopa/ts-commons/lib/types";
import {
ActivityResultFailure,
BlobCreationFailure,
Expand All @@ -11,6 +14,21 @@ import {
QueryFailure
} from "./types";

// Cosmos Errors
export enum CosmosErrorsTypes {
"COSMOS_EMPTY_RESPONSE" = "COSMOS_EMPTY_RESPONSE",
"COSMOS_CONFLICT_RESPONSE" = "COSMOS_CONFLICT_RESPONSE",
"COSMOS_DECODING_ERROR" = "COSMOS_DECODING_ERROR",
"COSMOS_ERROR_RESPONSE" = "COSMOS_ERROR_RESPONSE"
}

const CosmosErrorsTypesC = t.interface({
kind: enumType<CosmosErrorsTypes>(CosmosErrorsTypes, "kind")
});

export const isCosmosErrors = (error: unknown): error is CosmosErrors =>
CosmosErrorsTypesC.is(error);

/**
* To be used for exhaustive checks
*/
Expand Down
17 changes: 5 additions & 12 deletions ExtractUserDataActivity/__tests__/handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,18 +113,11 @@ const messageModelMock = ({
} as any) as MessageModel;

// ServicePreferences Model
const asyncIteratorOf = <T>(items: T[]): AsyncIterator<T[]> => {
const data = [...items];
return {
next: async () => {
const value = data.shift();
return {
done: typeof value === "undefined",
value: [value]
};
}
};
};
export async function* asyncIteratorOf<T>(items: T[]) {
for (const item of items) {
yield [item];
}
}

const mockDeleteServicePreferences = jest.fn<
ReturnType<InstanceType<typeof ServicePreferencesDeletableModel>["delete"]>,
Expand Down
Loading

0 comments on commit 0337e96

Please sign in to comment.