Skip to content

Commit

Permalink
chore(EU Covid Certificate): [IAGP-5] Types, actions & store for EU C…
Browse files Browse the repository at this point in the history
…ovid Certificate (#3076)

* add placeholders for foldering

* wip datamodel

* add actions & store

* add networking handlers

* renaming

* fix wrong import

* add test

* revert

* add test

* add selector

* update certificate types

* change selector

* add generator

* Update ts/features/euCovidCert/saga/index.ts

Co-authored-by: Matteo Boschi <[email protected]>
  • Loading branch information
fabriziofff and Undermaken authored Jun 1, 2021
1 parent c8c9267 commit 4a04070
Show file tree
Hide file tree
Showing 19 changed files with 369 additions and 4 deletions.
4 changes: 2 additions & 2 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
codecov:
require_ci_to_pass: yes
require_ci_to_pass: true

coverage:
precision: 2
Expand All @@ -24,7 +24,7 @@ parsers:
comment:
layout: "reach,diff,flags,files,footer"
behavior: default
require_changes: no
require_changes: false

github_checks:
annotations: false
14 changes: 14 additions & 0 deletions ts/features/common/store/reducers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { combineReducers } from "redux";
import { Action } from "../../../../store/actions/types";
import {
euCovidCertReducer,
EuCovidCertState
} from "../../../euCovidCert/store/reducers";

export type FeaturesState = {
euCovidCert: EuCovidCertState;
};

export const featuresReducer = combineReducers<FeaturesState, Action>({
euCovidCert: euCovidCertReducer
});
Empty file.
Empty file.
Empty file.
14 changes: 14 additions & 0 deletions ts/features/euCovidCert/saga/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { SagaIterator } from "redux-saga";
import { takeLatest } from "redux-saga/effects";
import { euCovidCertificateGet } from "../store/actions";
import { handleGetEuCovidCertificate } from "./networking/handleGetEuCovidCertificate";

/**
* Handle the EU Covid Certificate requests
* @param _
*/
export function* watchEUCovidCertificateSaga(_: string): SagaIterator {
// const euCovidCertClient = BackendBpdClient(bpdApiUrlPrefix, bpdBearerToken);

yield takeLatest(euCovidCertificateGet.request, handleGetEuCovidCertificate);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { delay, Effect, put } from "redux-saga/effects";
import { ActionType } from "typesafe-actions";
import { euCovidCertificateGet } from "../../store/actions";

/**
* Handle the remote call to retrieve the certificate data
* @param action
*/
export function* handleGetEuCovidCertificate(
action: ActionType<typeof euCovidCertificateGet.request>
): Generator<Effect, void> {
// TODO: add networking logic
yield delay(500);
yield put(
euCovidCertificateGet.success({
authCode: action.payload,
kind: "notFound"
})
);
}
Empty file.
Empty file.
22 changes: 22 additions & 0 deletions ts/features/euCovidCert/store/actions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ActionType, createAsyncAction } from "typesafe-actions";
import { GenericError } from "../../../../utils/errors";
import { EUCovidCertificateAuthCode } from "../../types/EUCovidCertificate";
import {
EUCovidCertificateResponse,
WithEUCovidCertAuthCode
} from "../../types/EUCovidCertificateResponse";

/**
* The user requests the EU Covid certificate, starting from the auth_code
*/
export const euCovidCertificateGet = createAsyncAction(
"EUCOVIDCERT_REQUEST",
"EUCOVIDCERT_SUCCESS",
"EUCOVIDCERT_FAILURE"
)<
EUCovidCertificateAuthCode,
EUCovidCertificateResponse,
WithEUCovidCertAuthCode<GenericError>
>();

export type EuCovidCertActions = ActionType<typeof euCovidCertificateGet>;
107 changes: 107 additions & 0 deletions ts/features/euCovidCert/store/reducers/__test__/byAuthCode.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { pot } from "italia-ts-commons";
import { createStore } from "redux";
import { applicationChangeState } from "../../../../../store/actions/application";
import { appReducer } from "../../../../../store/reducers";
import { GenericError } from "../../../../../utils/errors";
import { EUCovidCertificateAuthCode } from "../../../types/EUCovidCertificate";
import {
EUCovidCertificateResponse,
WithEUCovidCertAuthCode
} from "../../../types/EUCovidCertificateResponse";
import { euCovidCertificateGet } from "../../actions";
import { euCovidCertificateFromAuthCodeSelector } from "../byAuthCode";

const authCode = "authCode1" as EUCovidCertificateAuthCode;
const mockResponseSuccess: EUCovidCertificateResponse = {
authCode,
kind: "notFound"
};

const mockFailure: WithEUCovidCertAuthCode<GenericError> = {
authCode,
kind: "generic",
value: new Error("A generic error")
};

describe("Test byAuthCode reducer & selector behaviour", () => {
it("Initial state should be pot.none", () => {
const globalState = appReducer(undefined, applicationChangeState("active"));
expect(globalState.features.euCovidCert.byAuthCode).toStrictEqual({});
expect(
euCovidCertificateFromAuthCodeSelector(globalState, authCode)
).toStrictEqual(pot.none);
});
it("Should be pot.noneLoading after the first loading action dispatched", () => {
const globalState = appReducer(undefined, applicationChangeState("active"));
const store = createStore(appReducer, globalState as any);
store.dispatch(euCovidCertificateGet.request(authCode));

const byAuthCode = store.getState().features.euCovidCert.byAuthCode;

expect(byAuthCode[authCode]).toStrictEqual(pot.noneLoading);
expect(
euCovidCertificateFromAuthCodeSelector(store.getState(), authCode)
).toStrictEqual(pot.noneLoading);
});
it("Should be pot.some with the response, after the success action", () => {
const globalState = appReducer(undefined, applicationChangeState("active"));
const store = createStore(appReducer, globalState as any);
store.dispatch(euCovidCertificateGet.request(authCode));
store.dispatch(euCovidCertificateGet.success(mockResponseSuccess));

expect(
store.getState().features.euCovidCert.byAuthCode[authCode]
).toStrictEqual(pot.some(mockResponseSuccess));
expect(
euCovidCertificateFromAuthCodeSelector(store.getState(), authCode)
).toStrictEqual(pot.some(mockResponseSuccess));

store.dispatch(euCovidCertificateGet.request(authCode));

expect(
store.getState().features.euCovidCert.byAuthCode[authCode]
).toStrictEqual(pot.someLoading(mockResponseSuccess));
expect(
euCovidCertificateFromAuthCodeSelector(store.getState(), authCode)
).toStrictEqual(pot.someLoading(mockResponseSuccess));

store.dispatch(euCovidCertificateGet.failure(mockFailure));

expect(
store.getState().features.euCovidCert.byAuthCode[authCode]
).toStrictEqual(pot.someError(mockResponseSuccess, mockFailure.value));
expect(
euCovidCertificateFromAuthCodeSelector(store.getState(), authCode)
).toStrictEqual(pot.someError(mockResponseSuccess, mockFailure.value));
});
it("Should be pot.noneError after the failure action", () => {
const globalState = appReducer(undefined, applicationChangeState("active"));
const store = createStore(appReducer, globalState as any);
store.dispatch(euCovidCertificateGet.request(authCode));
store.dispatch(euCovidCertificateGet.failure(mockFailure));

expect(
store.getState().features.euCovidCert.byAuthCode[authCode]
).toStrictEqual(pot.noneError(mockFailure.value));
expect(
euCovidCertificateFromAuthCodeSelector(store.getState(), authCode)
).toStrictEqual(pot.noneError(mockFailure.value));

store.dispatch(euCovidCertificateGet.request(authCode));
expect(
store.getState().features.euCovidCert.byAuthCode[authCode]
).toStrictEqual(pot.noneLoading);
expect(
euCovidCertificateFromAuthCodeSelector(store.getState(), authCode)
).toStrictEqual(pot.noneLoading);

store.dispatch(euCovidCertificateGet.success(mockResponseSuccess));

expect(
store.getState().features.euCovidCert.byAuthCode[authCode]
).toStrictEqual(pot.some(mockResponseSuccess));
expect(
euCovidCertificateFromAuthCodeSelector(store.getState(), authCode)
).toStrictEqual(pot.some(mockResponseSuccess));
});
});
51 changes: 51 additions & 0 deletions ts/features/euCovidCert/store/reducers/byAuthCode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { pot } from "italia-ts-commons";
import { createSelector } from "reselect";
import { getType } from "typesafe-actions";
import { Action } from "../../../../store/actions/types";
import { IndexedById } from "../../../../store/helpers/indexer";
import {
toError,
toLoading,
toSome
} from "../../../../store/reducers/IndexedByIdPot";
import { GlobalState } from "../../../../store/reducers/types";
import { EUCovidCertificateAuthCode } from "../../types/EUCovidCertificate";
import { EUCovidCertificateResponse } from "../../types/EUCovidCertificateResponse";
import { euCovidCertificateGet } from "../actions";

type EuCovidCertByIdState = IndexedById<
pot.Pot<EUCovidCertificateResponse, Error>
>;

/**
* Store the EU Certificate response status based on the AuthCode used to issue the request
* @param state
* @param action
*/
export const euCovidCertByAuthCodeReducer = (
state: EuCovidCertByIdState = {},
action: Action
): EuCovidCertByIdState => {
switch (action.type) {
case getType(euCovidCertificateGet.request):
return toLoading(action.payload, state);
case getType(euCovidCertificateGet.success):
return toSome(action.payload.authCode, state, action.payload);
case getType(euCovidCertificateGet.failure):
return toError(action.payload.authCode, state, action.payload.value);
}

return state;
};

/**
* From authCode to EUCovidCertificateResponse
*/
export const euCovidCertificateFromAuthCodeSelector = createSelector(
[
(state: GlobalState) => state.features.euCovidCert.byAuthCode,
(_: GlobalState, authCode: EUCovidCertificateAuthCode) => authCode
],
(byAuthCode, authCode): pot.Pot<EUCovidCertificateResponse, Error> =>
byAuthCode[authCode] ?? pot.none
);
15 changes: 15 additions & 0 deletions ts/features/euCovidCert/store/reducers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { pot } from "italia-ts-commons";
import { combineReducers } from "redux";
import { Action } from "../../../../store/actions/types";
import { IndexedById } from "../../../../store/helpers/indexer";
import { EUCovidCertificateResponse } from "../../types/EUCovidCertificateResponse";
import { euCovidCertByAuthCodeReducer } from "./byAuthCode";

export type EuCovidCertState = {
byAuthCode: IndexedById<pot.Pot<EUCovidCertificateResponse, Error>>;
};

export const euCovidCertReducer = combineReducers<EuCovidCertState, Action>({
// save, using the AuthCode as key the the pot.Pot<EUCovidCertificateResponse, Error> response
byAuthCode: euCovidCertByAuthCodeReducer
});
44 changes: 44 additions & 0 deletions ts/features/euCovidCert/types/EUCovidCertificate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { IUnitTag } from "italia-ts-commons/lib/units";

/**
* The unique ID of a EU Covid Certificate
*/
type EUCovidCertificateId = string & IUnitTag<"EUCovidCertificateId">;

/**
* The auth code used to request the EU Covid Certificate, received via message
*/
export type EUCovidCertificateAuthCode = string &
IUnitTag<"EUCovidCertificateAuthCode">;

type WithEUCovidCertificateId<T> = T & {
id: EUCovidCertificateId;
};

type QRCode = {
mimeType: "image/png" | "image/svg";
content: string;
};

type ValidCertificate = {
kind: "valid";
qrCode: QRCode;
markdownPreview: string;
markdownDetails: string;
};

type ExpiredCertificate = {
kind: "expired";
};

type RevokedCertificate = {
kind: "revoked";
revokedOn: Date;
};

/**
* This type represents the EU Covid Certificate with the different states & data
*/
export type EUCovidCertificate = WithEUCovidCertificateId<
ValidCertificate | ExpiredCertificate | RevokedCertificate
>;
65 changes: 65 additions & 0 deletions ts/features/euCovidCert/types/EUCovidCertificateResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {
EUCovidCertificate,
EUCovidCertificateAuthCode
} from "./EUCovidCertificate";

type EUCovidCertificateResponseSuccess = {
kind: "success";
value: EUCovidCertificate;
};

/**
* The required certificated is not found (403)
*/
type EUCovidCertificateResponseNotFound = {
kind: "notFound";
};

/**
* The required certificate have a wrong format (400)
*/
type EUCovidCertificateResponseWrongFormat = {
kind: "wrongFormat";
};

/**
* A generic error response was received (500, others, generic error)
*/
type EUCovidCertificateResponseGenericError = {
kind: "genericError";
};

/**
* The EU Covid certificate service is not operational (410)
*/
type EUCovidCertificateResponseNotOperational = {
kind: "notOperational";
};

/**
* The EU Covid certificate service is not operational (504)
*/
type EUCovidCertificateResponseTemporarilyNotAvailable = {
kind: "temporarilyNotAvailable";
};

type EUCovidCertificateResponseFailure =
| EUCovidCertificateResponseNotFound
| EUCovidCertificateResponseWrongFormat
| EUCovidCertificateResponseGenericError
| EUCovidCertificateResponseNotOperational
| EUCovidCertificateResponseTemporarilyNotAvailable;

/**
* Bind the response with the initial authCode
*/
export type WithEUCovidCertAuthCode<T> = T & {
authCode: EUCovidCertificateAuthCode;
};

/**
* This type represents all the possible remote responses
*/
export type EUCovidCertificateResponse = WithEUCovidCertAuthCode<
EUCovidCertificateResponseSuccess | EUCovidCertificateResponseFailure
>;
Loading

0 comments on commit 4a04070

Please sign in to comment.