Skip to content

Commit

Permalink
Add tests (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
timja authored Jun 12, 2023
1 parent 3fc0fd2 commit 73a5c8a
Show file tree
Hide file tree
Showing 14 changed files with 2,148 additions and 65 deletions.
10 changes: 10 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ on:
branches:
- main

permissions:
id-token: write
contents: read

jobs:
build:
runs-on: ubuntu-latest
Expand All @@ -18,4 +22,10 @@ jobs:
node-version-file: ".nvmrc"
- run: yarn install
- run: yarn lint
- name: "Az CLI login"
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
allow-no-subscriptions: true
- run: yarn test
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@
!.yarn/versions
yarn-error.log
*.map
*.js
lib/*.js
coverage/
node_modules/.vitest
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
18.16.0
20.2.0
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,6 @@ IDE Configuration:
- [ ] SSL certificate
- [ ] Possibly allow setting boolean values on per app basic if required
- [ ] README
- [ ] Tests
- [ ] CI/CD
- [ ] Publish to npm registry
- [ ] Blog
- [ ] Visible to users

Expand Down Expand Up @@ -64,5 +61,16 @@ You can run this locally with:
```bash
yarn install
yarn tsc -p .
yarn node dist/index.js --config apps.yaml
yarn node --enable-source-maps dist/index.js --config apps.yaml
```

### Tests

Login to Azure with an account that has the Application Administrator role in the tenant.
It's recommended you use a sandbox tenant for this.

Then run:

```bash
yarn test
```
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"do-it": "./run.sh",
"lint": "prettier --check .",
"lint:fix": "prettier --write .",
"test": "echo TODO"
"test": "vitest",
"test:coverage": "vitest run --coverage"
},
"files": [
"lib/"
Expand All @@ -25,7 +26,9 @@
"@types/node": "^20.2.5",
"@types/prettier": "^2",
"@types/yargs": "^17",
"@vitest/coverage-v8": "^0.32.0",
"prettier": "2.8.8",
"typescript": "^5.1.3"
"typescript": "^5.1.3",
"vitest": "^0.32.0"
}
}
178 changes: 178 additions & 0 deletions src/applicationManager.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import {
ApplicationAndServicePrincipalId,
createApplication,
deleteApplication,
findExistingApplication,
readApplication,
setLogo,
setOnPremisesPublishing,
updateApplicationConfig,
} from "./applicationManager";
import { DefaultAzureCredential } from "@azure/identity";

import { expect, describe, test, beforeAll, afterEach } from "vitest";
import { defaultOnPremisesFlags } from "./configuration";
import {
assignGroups,
readServicePrincipal,
setUserAssignmentRequired,
} from "./servicePrincipalManager";
import * as process from "process";

async function authenticate() {
const credential = new DefaultAzureCredential();

const { token } = await credential.getToken(
"https://graph.microsoft.com/.default"
);

return token;
}

async function cleanup({
token,
appDetails,
}: {
token: string;
appDetails: ApplicationAndServicePrincipalId;
}) {
if (appDetails) {
await deleteApplication({
token,
applicationId: appDetails.applicationId,
});
}
}

function randomString() {
return Math.random().toString(36).slice(2, 7);
}

function applicationName() {
return "azure-app-proxy-manager-" + randomString();
}

function getExternalUrl() {
const suffix =
process.env.EXTERNAL_URL_SUFFIX ||
"app-proxy-poc.sandbox.platform.hmcts.net";
return `https://${randomString()}.${suffix}`;
}

function getInternalUrl() {
const suffix =
process.env.INTERNAL_URL_SUFFIX ||
"app-proxy-poc.sandbox.platform.hmcts.net";
return `https://${randomString()}.${suffix}`;
}

describe("applicationManager", () => {
const logoUrl =
"https://raw.githubusercontent.com/hmcts/azure-app-proxy/e875c42/logos/incident-bot.png";
let token: string;

let appDetails: ApplicationAndServicePrincipalId;
const groupNameForRoleAssignments =
process.env.ROLE_ASSIGNMENT_GROUP || "Test app";

beforeAll(async () => {
token = await authenticate();
});

afterEach(async () => {
await cleanup({ token, appDetails });

appDetails = undefined;
});

test("happy path", async () => {
const displayName = applicationName();

const existingApplicationId = await findExistingApplication({
token,
displayName,
});
expect(existingApplicationId).toBeUndefined();

appDetails = await createApplication({ token, displayName });

expect(appDetails.applicationId).toBeDefined();
expect(appDetails.servicePrincipalObjectId).toBeDefined();

const externalUrl = getExternalUrl();

await updateApplicationConfig({
token,
externalUrl,
appId: appDetails.applicationId,
});

await setLogo({ token, appId: appDetails.applicationId, logoUrl });

const internalUrl = getInternalUrl();
await setOnPremisesPublishing({
token,
appId: appDetails.applicationId,
onPremisesPublishing: {
externalUrl: externalUrl,
internalUrl,
...defaultOnPremisesFlags(),
},
});

await setUserAssignmentRequired({
token,
objectId: appDetails.servicePrincipalObjectId,
assignmentRequired: false,
});
await assignGroups({
token,
objectId: appDetails.servicePrincipalObjectId,
groups: [groupNameForRoleAssignments],
});

const application = await readApplication({
token,
applicationId: appDetails.applicationId,
});

const identifierUri = application.identifierUris[0];
expect(identifierUri).toEqual(externalUrl);

const servicePrincipal = await readServicePrincipal({
token,
servicePrincipalObjectId: appDetails.servicePrincipalObjectId,
});
expect(servicePrincipal.appRoleAssignmentRequired).toEqual(false);
expect(servicePrincipal.info.logoUrl).toBeDefined();
});

test("finds existing application when creating", async () => {
const displayName = applicationName();

appDetails = await createApplication({ token, displayName });
const appDetails2 = await createApplication({ token, displayName });

expect(appDetails.applicationId).toEqual(appDetails2.applicationId);
expect(appDetails.servicePrincipalObjectId).toEqual(
appDetails2.servicePrincipalObjectId
);
});

test("setLogo does nothing if no logo", async () => {
const displayName = applicationName();
appDetails = await createApplication({ token, displayName });

await setLogo({
token,
appId: appDetails.applicationId,
logoUrl: undefined,
});

const servicePrincipal = await readServicePrincipal({
token,
servicePrincipalObjectId: appDetails.servicePrincipalObjectId,
});
expect(servicePrincipal.info.logoUrl).toBeNull();
});
});
58 changes: 52 additions & 6 deletions src/applicationManager.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { OnPremisesPublishing } from "./onPremisesPublishing.js";
import { errorHandler } from "./errorHandler.js";
import { findExistingServicePrincipal } from "./servicePrincipalManager.js";
import * as process from "process";

export type ApplicationAndServicePrincipalId = {
applicationId: string;
Expand All @@ -25,10 +24,9 @@ export async function createApplication({
});

if (!servicePrincipalObjectId) {
console.log(
throw new Error(
`Found application ${displayName} but no service principal, aborting`
);
process.exit(1);
}

return { applicationId, servicePrincipalObjectId };
Expand Down Expand Up @@ -60,6 +58,54 @@ export async function createApplication({
};
}

export function helloWorld() {
console.log("me");
}

export async function readApplication({
token,
applicationId,
}: {
token: string;
applicationId: string;
}) {
console.log("Retrieving application", applicationId);
const result = await fetch(
`https://graph.microsoft.com/v1.0/applications/${applicationId}`,
{
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
},
}
);

await errorHandler("reading application", result);

return await result.json();
}

export async function deleteApplication({
token,
applicationId,
}: {
token: string;
applicationId: string;
}) {
console.log("Deleting application", applicationId);
const result = await fetch(
`https://graph.microsoft.com/v1.0/applications/${applicationId}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${token}`,
},
}
);

await errorHandler("deleting application", result);
}

export async function findExistingApplication({
token,
displayName,
Expand Down Expand Up @@ -129,9 +175,9 @@ async function waitTillApplicationExists({

const maxAttempts = 30;
if (attempt > maxAttempts) {
console.log(`Failed to find application after ${maxAttempts} attempts`);
// @ts-ignore
process.exit(1);
throw new Error(
`Failed to find application after ${maxAttempts} attempts`
);
}
}
}
Expand Down
4 changes: 1 addition & 3 deletions src/configuration.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import yaml from "js-yaml";
import { promises as fsPromises } from "fs";
import { Application } from "./application.js";
import path from "path";
import { fileURLToPath } from "url";

// TODO merge with config
function defaultOnPremisesFlags(): {
export function defaultOnPremisesFlags(): {
externalAuthenticationType: "aadPreAuthentication";
isHttpOnlyCookieEnabled: boolean;
isOnPremPublishingEnabled: boolean;
Expand Down
16 changes: 11 additions & 5 deletions src/errorHandler.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
export async function errorHandler(when: string, result: Response) {
if (!result.ok) {
console.log(`Error ${when}`, result.status);
console.log(result.statusText);
console.log(await result.json());
// @ts-ignore
process.exit(1);
console.log();

let body = null;
try {
body = await result.json();
} catch (err) {}
throw new Error(
`Error ${when}, status: ${result.status}, statusText: ${
result.statusText
}, body: ${JSON.stringify(body)}`
);
}
}
Loading

0 comments on commit 73a5c8a

Please sign in to comment.