Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#175441170] Add Organization and Service logo's upload #110

Merged
merged 10 commits into from
Nov 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.demo
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,4 @@ REPLY_URL=https://developer.io.italia.it
TENANT_NAME=your_tenant_name

WEBSITE_NODE_DEFAULT_VERSION=6.11.2
WEBSITE_NPM_DEFAULT_VERSION=6.1.0
WEBSITE_NPM_DEFAULT_VERSION=6.1.0
2 changes: 1 addition & 1 deletion docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ services:
env_file:
- .env
ports:
- "3000:3000"
- "3999:3000"
stdin_open: true
tty: true
19 changes: 19 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"generate:api-models": "npm-run-all -s clean:api-models generate:api-models:*",
"dist:modules": "modclean -r -n default:safe && npm prune --production",
"predeploy": "npm-run-all build dist:modules",
"test": "jest",
"preversion": "auto-changelog --config .auto-changelog.json --unreleased --commit-limit false --stdout --template preview.hbs",
"version": "auto-changelog -p --config .auto-changelog.json --unreleased && git add CHANGELOG.md"
},
Expand Down Expand Up @@ -63,6 +64,7 @@
"@types/dotenv": "^4.0.2",
"@types/express": "^4.0.39",
"@types/helmet": "^0.0.38",
"@types/jest": "^23.3.3",
"@types/method-override": "^0.0.31",
"@types/morgan": "^1.7.35",
"@types/node-fetch": "^2.1.2",
Expand All @@ -74,12 +76,14 @@
"danger-plugin-digitalcitizenship": "^0.3.1",
"io-functions-commons": "^15.0.0",
"italia-utils": "^4.1.0",
"jest": "^23.6.0",
"modclean": "^3.0.0-beta.1",
"npm-run-all": "^4.1.3",
"prettier": "^1.7.4",
"rimraf": "^2.6.3",
"shx": "^0.3.2",
"ts-node": "^6.1.1",
"ts-jest": "^23.10.4",
"tslint": "^5.8.0",
"tslint-config-prettier": "^1.6.0",
"tslint-immutable": "^4.4.0",
Expand All @@ -88,5 +92,20 @@
},
"resolutions": {
"fp-ts": "1.17.4"
},
"jest": {
"testEnvironment": "node",
"moduleFileExtensions": [
"ts",
"js",
"node"
],
"transform": {
"^.+\\.(ts|tsx)$": "ts-jest"
},
"testMatch": [
"**/__tests__/*.ts",
"**/__integrations__/*.ts"
]
}
}
155 changes: 155 additions & 0 deletions src/__integrations__/upload_logo_services.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import * as apim from "../apim_operations";
import * as services from "../controllers/services";

import ApiManagementClient from "azure-arm-apimanagement";
import { OrganizationFiscalCode } from "italia-ts-commons/lib/strings";
import { Logo } from "../../generated/api/Logo";
import { AdUser } from "../bearer_strategy";
import { putOrganizationLogo, putServiceLogo } from "../controllers/services";

import { none, option } from "fp-ts/lib/Option";
import SerializableSet from "json-set-map/build/src/set";
import { IExtendedUserContract } from "../apim_operations";

import { ServiceId } from "../../generated/api/ServiceId";

afterEach(() => {
jest.resetAllMocks();
jest.restoreAllMocks();
});

const serviceId = "s123" as ServiceId;

const organizationFiscalCode = "0123455" as OrganizationFiscalCode;

const userContract: IExtendedUserContract = {
email: "test@test.it",
groupNames: new SerializableSet<string>(["user"]),
id: "124123_id",
name: "name"
};

const adminUserContract: apim.IExtendedUserContract = {
email: "test@test.it",
groupNames: new SerializableSet<string>(["apiadmin"]),
id: "124123_id",
name: "name"
};

const adUser = {
emails: ["test@test.it"],
extension_Department: "deparment",
extension_Organization: "organization",
extension_Service: "service",
family_name: "name",
given_name: "given_name",
oid: "oid"
} as AdUser;

const logo = { logo: "logo_base_64" } as Logo;

const apiManagementClientMock = ({} as unknown) as ApiManagementClient;

describe("putServiceLogo", () => {
it("should respond with IResponseSuccessRedirectToResource if logo upload was successfull", async () => {
jest
.spyOn(apim, "getApimUser")
.mockReturnValueOnce(Promise.resolve(option.of(adminUserContract)));

jest
.spyOn(services.notificationApiClient, "uploadServiceLogo")
.mockReturnValueOnce(
Promise.resolve({
headers: undefined,
status: 201,
value: undefined
} as any)
);

const result = await putServiceLogo(
apiManagementClientMock,
adUser,
serviceId,
logo
);
expect(result.kind).toBe("IResponseSuccessRedirectToResource");
});

it("should respond with IResponseErrorNotFound if cannot find a user in the API management", async () => {
jest.spyOn(apim, "getApimUser").mockReturnValueOnce(Promise.resolve(none));

const result = await putServiceLogo(
apiManagementClientMock,
adUser,
serviceId,
logo
);
expect(result.kind).toBe("IResponseErrorNotFound");
});

it("should respond with ResponseErrorForbiddenNotAuthorized if user is not admin", async () => {
jest
.spyOn(apim, "getApimUser")
.mockReturnValueOnce(Promise.resolve(option.of(userContract)));

const result = await putServiceLogo(
apiManagementClientMock,
adUser,
serviceId,
logo
);
expect(result.kind).toBe("IResponseErrorForbiddenNotAuthorized");
});
});

describe("putOrganizationLogo", () => {
it("should respond with IResponseSuccessRedirectToResource if logo upload was successfull", async () => {
jest
.spyOn(apim, "getApimUser")
.mockReturnValueOnce(Promise.resolve(option.of(adminUserContract)));

jest
.spyOn(services.notificationApiClient, "uploadOrganizationLogo")
.mockReturnValueOnce(
Promise.resolve({
headers: undefined,
status: 201,
value: undefined
} as any)
);

const result = await putOrganizationLogo(
apiManagementClientMock,
adUser,
organizationFiscalCode,
logo
);
expect(result.kind).toBe("IResponseSuccessRedirectToResource");
});

it("should respond with IResponseErrorNotFound if cannot find a user in the API management", async () => {
jest.spyOn(apim, "getApimUser").mockReturnValueOnce(Promise.resolve(none));

const result = await putOrganizationLogo(
apiManagementClientMock,
adUser,
organizationFiscalCode,
logo
);
expect(result.kind).toBe("IResponseErrorNotFound");
});

it("should respond with ResponseErrorForbiddenNotAuthorized if user is not admin", async () => {
jest
.spyOn(apim, "getApimUser")
.mockReturnValueOnce(Promise.resolve(option.of(userContract)));

const result = await putOrganizationLogo(
apiManagementClientMock,
adUser,
organizationFiscalCode,
logo
);
expect(result.kind).toBe("IResponseErrorForbiddenNotAuthorized");
});
});
50 changes: 49 additions & 1 deletion src/api_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as t from "io-ts";

// A basic response type that also include 401
import { Either, left, right } from "fp-ts/lib/Either";

import {
ApiHeaderJson,
basicErrorResponseDecoder,
Expand Down Expand Up @@ -35,6 +36,8 @@ import { NewMessage } from "../generated/api/NewMessage";
import { Service } from "../generated/api/Service";
import { ServicePublic } from "../generated/api/ServicePublic";

import { Logo } from "../generated/api/Logo";

const OcpApimSubscriptionKey = "Ocp-Apim-Subscription-Key";
type OcpApimSubscriptionKey = typeof OcpApimSubscriptionKey;

Expand Down Expand Up @@ -119,6 +122,26 @@ export type UpdateServiceT = IPutApiRequestType<
ApiResponseType<ServicePublic>
>;

export type UploadServiceLogoT = IPutApiRequestType<
{
readonly logo: Logo;
readonly serviceId: string;
},
OcpApimSubscriptionKey | "Content-Type",
never,
ApiResponseType<undefined>
>;

export type UploadOrganizationLogoT = IPutApiRequestType<
{
readonly logo: Logo;
readonly organizationfiscalcode: string;
},
OcpApimSubscriptionKey | "Content-Type",
never,
ApiResponseType<undefined>
>;

export function APIClient(
baseUrl: string,
token: string,
Expand All @@ -130,6 +153,8 @@ export function APIClient(
readonly updateService: TypeofApiCall<UpdateServiceT>;
readonly getService: TypeofApiCall<GetServiceT>;
readonly sendMessage: TypeofApiCall<SendMessageT>;
readonly uploadServiceLogo: TypeofApiCall<UploadServiceLogoT>;
readonly uploadOrganizationLogo: TypeofApiCall<UploadOrganizationLogoT>;
} {
const options = {
baseUrl,
Expand Down Expand Up @@ -182,6 +207,24 @@ export function APIClient(
url: params => `/adm/services/${params.serviceId}`
};

const uploadServiceLogoT: UploadServiceLogoT = {
body: params => JSON.stringify(params.logo),
headers: composeHeaderProducers(tokenHeaderProducer, ApiHeaderJson),
method: "put",
query: _ => ({}),
response_decoder: apiResponseDecoder(t.undefined),
url: params => `/adm/services/${params.serviceId}/logo`
};

const uploadOrganizationLogoT: UploadOrganizationLogoT = {
body: params => JSON.stringify(params.logo),
headers: composeHeaderProducers(tokenHeaderProducer, ApiHeaderJson),
method: "put",
query: _ => ({}),
response_decoder: apiResponseDecoder(t.undefined),
url: params => `/adm/organizations/${params.organizationfiscalcode}/logo`
};

return {
createDevelopmentProfile: createFetchRequestForApi(
createDevelopmentProfileT,
Expand All @@ -190,7 +233,12 @@ export function APIClient(
createService: createFetchRequestForApi(createServiceT, options),
getService: createFetchRequestForApi(getServiceT, options),
sendMessage: createFetchRequestForApi(sendMessageT, options),
updateService: createFetchRequestForApi(updateServiceT, options)
updateService: createFetchRequestForApi(updateServiceT, options),
uploadOrganizationLogo: createFetchRequestForApi(
uploadOrganizationLogoT,
options
),
uploadServiceLogo: createFetchRequestForApi(uploadServiceLogoT, options)
};
}

Expand Down
43 changes: 41 additions & 2 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,22 @@ import {
withRequestMiddlewares,
wrapRequestHandler
} from "italia-ts-commons/lib/request_middleware";
import { EmailString, NonEmptyString } from "italia-ts-commons/lib/strings";
import {
EmailString,
NonEmptyString,
OrganizationFiscalCode
} from "italia-ts-commons/lib/strings";

import { setupBearerStrategy } from "./bearer_strategy";
import { initCacheStats } from "./cache";
import { getConfiguration } from "./controllers/configuration";
import { getService, putService, ServicePayload } from "./controllers/services";
import {
getService,
putOrganizationLogo,
putService,
putServiceLogo,
ServicePayload
} from "./controllers/services";
import {
getSubscriptions,
postSubscriptions,
Expand All @@ -53,6 +63,9 @@ import { SubscriptionData } from "./new_subscription";

import { ExtractFromPayloadMiddleware } from "./middlewares/extract_payload";

import { Logo } from "../generated/api/Logo";
import { ServiceId } from "../generated/api/ServiceId";

process.on("unhandledRejection", e => logger.error(JSON.stringify(e)));

if (process.env.NODE_ENV === "debug") {
Expand Down Expand Up @@ -178,6 +191,32 @@ app.put(
)
);

app.put(
"/services/:serviceId/logo",
ouathVerifier,
wrapRequestHandler(
withRequestMiddlewares(
getApiClientMiddleware(),
getUserFromRequestMiddleware(),
RequiredParamMiddleware("serviceId", ServiceId),
ExtractFromPayloadMiddleware(Logo)
)(putServiceLogo)
)
);

app.put(
"/organizations/:organizationFiscalCode/logo",
ouathVerifier,
wrapRequestHandler(
withRequestMiddlewares(
getApiClientMiddleware(),
getUserFromRequestMiddleware(),
RequiredParamMiddleware("organizationFiscalCode", OrganizationFiscalCode),
ExtractFromPayloadMiddleware(Logo)
)(putOrganizationLogo)
)
);

app.get(
["/user", "/user/:email"],
ouathVerifier,
Expand Down
2 changes: 2 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,5 @@ export const servicePrincipalTenantId = process.env
.SERVICE_PRINCIPAL_TENANT_ID as string;

export const sandboxFiscalCode = process.env.SANDBOX_FISCAL_CODE as FiscalCode;

export const logoUrl = process.env.LOGO_URL;
Loading