From 1aa0616c3c4bff7fda7dcf42a2379b2d5e033237 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Mon, 24 Jul 2023 10:00:26 +0200 Subject: [PATCH 01/15] Playwright tests --- .github/workflows/test.yml | 11 +- .gitignore | 3 +- package-lock.json | 94 +++++++++++ package.json | 5 +- playwright.config.ts | 9 ++ src/config.validate.json | 1 + src/services/usersService.ts | 2 +- tests/constants.ts | 10 -- tests/e2e/maps/apis.ts | 16 +- tests/e2e/maps/products.ts | 28 +++- tests/e2e/maps/profile.ts | 81 +++++++++- tests/e2e/maps/signin-basic.ts | 13 +- tests/e2e/maps/signup-basic.ts | 21 ++- tests/e2e/playwright-test.ts | 82 ++++++++++ tests/e2e/runtime/apis.spec.ts | 77 ++++----- tests/e2e/runtime/products.spec.ts | 57 +++---- tests/e2e/runtime/profile.spec.ts | 43 ----- tests/e2e/runtime/signin.spec.ts | 41 ----- tests/e2e/runtime/signup.spec.ts | 41 ----- tests/e2e/runtime/user-subscriptions.spec.ts | 62 +++++++ .../user-resources-win32.jpeg | Bin 0 -> 40989 bytes tests/e2e/runtime/user.spec.ts | 73 +++++++++ tests/mapiClient.ts | 113 +++++++++++++ tests/mocks/collection/api.ts | 72 ++++----- tests/mocks/collection/apis.ts | 37 ----- tests/mocks/collection/product.ts | 69 ++++---- tests/mocks/collection/products.ts | 37 ----- tests/mocks/collection/resource.ts | 7 + tests/mocks/collection/subscription.ts | 71 ++++++++ tests/mocks/collection/user.ts | 152 +++++++----------- tests/mocks/mockServerData.json | 133 +++++++++++++++ tests/services/IApiService.ts | 9 ++ tests/services/IProductService.ts | 9 ++ tests/services/ITestRunner.ts | 3 + tests/services/IUserService.ts | 7 + tests/services/apiService.ts | 43 +++++ tests/services/productService.ts | 45 ++++++ tests/services/testRunner.ts | 16 ++ tests/services/testRunnerMock.ts | 38 +++++ tests/services/userService.ts | 30 ++++ tests/templating.ts | 20 +++ tests/tsconfig.json | 26 +++ tests/utils.ts | 106 ++++++------ tsconfig.json | 1 + 44 files changed, 1291 insertions(+), 523 deletions(-) create mode 100644 playwright.config.ts delete mode 100644 tests/constants.ts create mode 100644 tests/e2e/playwright-test.ts delete mode 100644 tests/e2e/runtime/profile.spec.ts delete mode 100644 tests/e2e/runtime/signin.spec.ts delete mode 100644 tests/e2e/runtime/signup.spec.ts create mode 100644 tests/e2e/runtime/user-subscriptions.spec.ts create mode 100644 tests/e2e/runtime/user-subscriptions.spec.ts-snapshots/user-resources-win32.jpeg create mode 100644 tests/e2e/runtime/user.spec.ts create mode 100644 tests/mapiClient.ts delete mode 100644 tests/mocks/collection/apis.ts delete mode 100644 tests/mocks/collection/products.ts create mode 100644 tests/mocks/collection/resource.ts create mode 100644 tests/mocks/collection/subscription.ts create mode 100644 tests/mocks/mockServerData.json create mode 100644 tests/services/IApiService.ts create mode 100644 tests/services/IProductService.ts create mode 100644 tests/services/ITestRunner.ts create mode 100644 tests/services/IUserService.ts create mode 100644 tests/services/apiService.ts create mode 100644 tests/services/productService.ts create mode 100644 tests/services/testRunner.ts create mode 100644 tests/services/testRunnerMock.ts create mode 100644 tests/services/userService.ts create mode 100644 tests/templating.ts create mode 100644 tests/tsconfig.json diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3f42edae5..23ea58c71 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,7 @@ jobs: run: npm run test end2end-tests: - runs-on: ubuntu-latest + runs-on: windows-latest steps: - name: Checkout code @@ -32,6 +32,12 @@ jobs: - name: Install run: npm install + - name: Compile + run: npx tsc -p tests\tsconfig.json + + - name: Install Playwright Browsers + run: npx playwright install --with-deps + - name: Build static data run: npm run build-mock-static-data @@ -43,5 +49,4 @@ jobs: shell: pwsh - name: Run tests - run: npm run test-e2e - \ No newline at end of file + run: npx playwright test /tests --workers 1 \ No newline at end of file diff --git a/.gitignore b/.gitignore index b6a98625f..fe62fd755 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ node_modules/ src/config.design.json src/config.publish.json src/config.runtime.json - +test-results/ +tsconfig.tsbuildinfo \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 0839bcb5f..a6398cdda 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,6 +58,7 @@ }, "devDependencies": { "@azure/storage-blob": "12.13.0", + "@playwright/test": "1.35.1", "@types/chai": "^4.3.4", "@types/google-maps": "^3.2.3", "@types/knockout": "^3.4.72", @@ -81,6 +82,7 @@ "mini-css-extract-plugin": "^2.7.3", "mocha": "^10.2.0", "path": "^0.12.7", + "playwright": "1.35.1", "postcss-loader": "^7.0.2", "puppeteer": "19.7.5", "querystring-es3": "^0.2.1", @@ -1247,6 +1249,25 @@ "node": ">=10.12.0" } }, + "node_modules/@playwright/test": { + "version": "1.35.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.35.1.tgz", + "integrity": "sha512-b5YoFe6J9exsMYg0pQAobNDR85T1nLumUYgUTtKm4d21iX2L7WqKq9dW8NGJ+2vX0etZd+Y7UeuqsxDXm9+5ZA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "playwright-core": "1.35.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, "node_modules/@rollup/plugin-node-resolve": { "version": "13.3.0", "license": "MIT", @@ -5068,6 +5089,19 @@ "version": "1.0.0", "license": "ISC" }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.1", "license": "MIT" @@ -8495,6 +8529,34 @@ "node": ">=8" } }, + "node_modules/playwright": { + "version": "1.35.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.35.1.tgz", + "integrity": "sha512-NbwBeGJLu5m7VGM0+xtlmLAH9VUfWwYOhUi/lSEDyGg46r1CA9RWlvoc5yywxR9AzQb0mOCm7bWtOXV7/w43ZA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "playwright-core": "1.35.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/playwright-core": { + "version": "1.35.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.35.1.tgz", + "integrity": "sha512-pNXb6CQ7OqmGDRspEjlxE49w+4YtR6a3X6mT1hZXeJHWmsEz7SunmvZeiG/+y1yyMZdHnnn73WKYdtV1er0Xyg==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/plumb": { "version": "0.1.0", "license": "MIT" @@ -12305,6 +12367,17 @@ "webcrypto-core": "^1.7.4" } }, + "@playwright/test": { + "version": "1.35.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.35.1.tgz", + "integrity": "sha512-b5YoFe6J9exsMYg0pQAobNDR85T1nLumUYgUTtKm4d21iX2L7WqKq9dW8NGJ+2vX0etZd+Y7UeuqsxDXm9+5ZA==", + "dev": true, + "requires": { + "@types/node": "*", + "fsevents": "2.3.2", + "playwright-core": "1.35.1" + } + }, "@rollup/plugin-node-resolve": { "version": "13.3.0", "requires": { @@ -14792,6 +14865,12 @@ "fs.realpath": { "version": "1.0.0" }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "optional": true + }, "function-bind": { "version": "1.1.1" }, @@ -16903,6 +16982,21 @@ } } }, + "playwright": { + "version": "1.35.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.35.1.tgz", + "integrity": "sha512-NbwBeGJLu5m7VGM0+xtlmLAH9VUfWwYOhUi/lSEDyGg46r1CA9RWlvoc5yywxR9AzQb0mOCm7bWtOXV7/w43ZA==", + "dev": true, + "requires": { + "playwright-core": "1.35.1" + } + }, + "playwright-core": { + "version": "1.35.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.35.1.tgz", + "integrity": "sha512-pNXb6CQ7OqmGDRspEjlxE49w+4YtR6a3X6mT1hZXeJHWmsEz7SunmvZeiG/+y1yyMZdHnnn73WKYdtV1er0Xyg==", + "dev": true + }, "plumb": { "version": "0.1.0" }, diff --git a/package.json b/package.json index 27c3fd729..393c733e4 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,6 @@ "build-publisher": "webpack --config webpack.publisher.js", "build-runtime": "webpack --config webpack.runtime.js", "build-function": "webpack --config webpack.function.js", - "test-e2e": "node node_modules/mocha/bin/_mocha -r mocha.js tests/e2e/**/*.spec.ts --timeout 3000000", "test": "node node_modules/mocha/bin/_mocha -r mocha.js src/**/*.spec.ts", "deploy-function": "npm run build-function && cd dist/function && func azure functionapp publish < function app name >", "publish": "webpack --config webpack.publisher.js && node dist/publisher/index.js && npm run serve-website", @@ -69,7 +68,9 @@ "webpack": "5.76.1", "webpack-cli": "5.0.1", "webpack-dev-server": "4.12.0", - "webpack-merge": "5.8.0" + "webpack-merge": "5.8.0", + "playwright": "1.35.1", + "@playwright/test": "1.35.1" }, "dependencies": { "@azure/api-management-custom-widgets-scaffolder": "^1.0.0-beta.2", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 000000000..2113ff4d1 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + testIgnore: 'playwright/*', + //timeout: 30_000, + use: { + video: 'retain-on-failure' + } +}); \ No newline at end of file diff --git a/src/config.validate.json b/src/config.validate.json index c92daf9f1..3cae59bb4 100644 --- a/src/config.validate.json +++ b/src/config.validate.json @@ -1,5 +1,6 @@ { "environment": "validation", + "isLocalRun": true, "root": "http://localhost:8080", "urls": { "home": "/", diff --git a/src/services/usersService.ts b/src/services/usersService.ts index bb18faf8c..e5971e576 100644 --- a/src/services/usersService.ts +++ b/src/services/usersService.ts @@ -255,7 +255,7 @@ export class UsersService { */ public async ensureSignedIn(): Promise { const userId = await this.getCurrentUserId(); - + if (!userId) { this.navigateToSignin(); return; // intentionally exiting without resolving the promise. diff --git a/tests/constants.ts b/tests/constants.ts deleted file mode 100644 index 2ebf3f5f2..000000000 --- a/tests/constants.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { LaunchOptions, BrowserLaunchArgumentOptions, BrowserConnectOptions } from "puppeteer"; - -export const BrowserLaunchOptions: LaunchOptions & BrowserLaunchArgumentOptions & BrowserConnectOptions = { - headless: true, - ignoreHTTPSErrors: true, - product: "chrome", - devtools: true, - userDataDir: "/puppeteer-data-dir", // necessary for persistent user preferences - args: [ '--incognito' ] -}; \ No newline at end of file diff --git a/tests/e2e/maps/apis.ts b/tests/e2e/maps/apis.ts index 6f253f5d6..1ccd42578 100644 --- a/tests/e2e/maps/apis.ts +++ b/tests/e2e/maps/apis.ts @@ -1,13 +1,25 @@ -import { Page } from "puppeteer"; +import { Page } from "playwright"; export class ApisWidget { constructor(private readonly page: Page) { } - public async apis(): Promise { + public async waitRuntimeInit(): Promise { await this.page.waitForSelector("api-list.block"); await this.page.waitForSelector("api-list div.table div.table-body div.table-row"); } + public async getApiByName(apiName: string): Promise { + const apis = await this.page.$$('api-list div.table div.table-body div.table-row a'); + + for (let i = 0; i < apis.length; i++) { + const productNameHtml = await (await apis[i].getProperty('innerText')).jsonValue(); + if (productNameHtml == apiName){ + return apis[i]; + } + } + return null; + } + public async getApisCount(): Promise { return await this.page.evaluate(() => document.querySelector("api-list div.table div.table-body div.table-row")?.parentElement?.childElementCount diff --git a/tests/e2e/maps/products.ts b/tests/e2e/maps/products.ts index b493bf41e..e96e7df98 100644 --- a/tests/e2e/maps/products.ts +++ b/tests/e2e/maps/products.ts @@ -1,9 +1,9 @@ -import { Page } from "puppeteer"; +import { Page } from "playwright"; export class ProductseWidget { constructor(private readonly page: Page) { } - public async products(): Promise { + public async waitRuntimeInit(): Promise { await this.page.waitForSelector("product-list-runtime.block"); await this.page.waitForSelector("product-list-runtime div.table div.table-body div.table-row"); } @@ -13,4 +13,28 @@ export class ProductseWidget { document.querySelector("product-list-runtime div.table div.table-body div.table-row")?.parentElement?.childElementCount ); } + + public async getProductByName(productName: string): Promise { + const products = await this.page.$$('product-list-runtime div.table div.table-body div.table-row a'); + + for (let i = 0; i < products.length; i++) { + const productNameHtml = await (await products[i].getProperty('innerText')).jsonValue(); + if (productNameHtml == productName){ + return products[i]; + } + } + return null; + } + + public async goToProductPage(baseUrl, productId: string): Promise{ + await this.page.goto(`${baseUrl}/product#product=${productId}`); + } + + public async subscribeToProduct(baseUrl, productId: string, subscriptionName: string): Promise { + await this.goToProductPage(baseUrl, productId); + await this.page.waitForSelector("product-subscribe-runtime form button"); + await this.page.type("product-subscribe-runtime form input", subscriptionName); + await this.page.click("product-subscribe-runtime form button"); + await this.page.waitForNavigation({ waitUntil: "domcontentloaded" }); + } } \ No newline at end of file diff --git a/tests/e2e/maps/profile.ts b/tests/e2e/maps/profile.ts index 5f01a092c..1ffb27bcb 100644 --- a/tests/e2e/maps/profile.ts +++ b/tests/e2e/maps/profile.ts @@ -1,15 +1,86 @@ -import { Page } from "puppeteer"; +import { Locator, Page } from "playwright"; export class ProfileWidget { constructor(private readonly page: Page) { } - public async profile(): Promise { + public async waitRuntimeInit(): Promise { await this.page.waitForSelector("profile-runtime .row"); await this.page.waitForSelector("subscriptions-runtime .table-row"); } - public async getUserEmail(): Promise { - await this.page.waitForSelector("[data-bind='text: user().email']"); - return await this.page.evaluate(() =>document.querySelector("[data-bind='text: user().email']")?.textContent); + public async getUserEmailLocator(): Promise { + return this.page.locator("profile-runtime [data-bind='text: user().email']").first(); + } + + public async getUserEmail(): Promise { + return (await this.getUserEmailLocator()).innerText(); + } + + public async getUserFirstNameLocator(): Promise { + return this.page.locator("profile-runtime [data-bind='text: user().firstName']").first(); + } + + public async getUserFirstName(): Promise { + return (await this.getUserFirstNameLocator()).innerText(); + } + + public async getUserLastNameLocator(): Promise { + return this.page.locator("profile-runtime [data-bind='text: user().lastName']").first(); + } + + public async getUserLastName(): Promise { + return (await this.getUserLastNameLocator()).innerText(); + } + + public async getUserRegistrationDataLocator(): Promise { + return this.page.locator("profile-runtime [data-bind='text: registrationDate']").first(); + } + + public async getUserRegistrationDate(): Promise { + return (await this.getUserRegistrationDataLocator()).innerText(); + } + + public async getSubscriptionRow(subscriptionName: string): Promise { + return this.page.locator("subscriptions-runtime div.table div.table-body div.table-row", { has: this.page.locator("div.row span[data-bind='text: model.name']").filter({ hasText: subscriptionName })}); + } + + public async getSubscriptioPrimarynKey(subscriptionName: string): Promise { + var subscriptionRow = await this.getSubscriptionRow(subscriptionName); + const primaryKeyElement = subscriptionRow.locator('code[data-bind="text: primaryKey"]').first(); + return await primaryKeyElement.textContent(); + } + + public async getSubscriptioSecondarynKey(subscriptionName: string): Promise { + var subscriptionRow = await this.getSubscriptionRow(subscriptionName); + const primaryKeyElement = subscriptionRow.locator('code[data-bind="text: secondaryKey"]').first(); + return await primaryKeyElement.textContent(); + } + + public async togglePrimarySubscriptionKey(subscriptionName: string): Promise { + var subscriptionRow = await this.getSubscriptionRow(subscriptionName); + await subscriptionRow.locator("a.btn-link[aria-label='Show primary key']", { hasText: "Show" }).click(); + } + + public async toggleSecondarySubscriptionKey(subscriptionName: string): Promise { + var subscriptionRow = await this.getSubscriptionRow(subscriptionName); + await subscriptionRow.locator("a.btn-link[aria-label='Show Secondary key']", { hasText: "Show" }).click(); + } + + public async getListOfLocatorsToHide(): Promise { + const primaryKeyElements = await this.page.locator('code[data-bind="text: primaryKey"]').all(); + const secondaryKeyElements = await this.page.locator('code[data-bind="text: secondaryKey"]').all(); + const productNames = this.page.locator('span[data-bind="text: model.productName"]'); + const subscriptionNames = this.page.locator('span[data-bind="text: model.name"]'); + const subscriptionStartDates = this.page.locator('span[data-bind="text: $parent.timeToString(model.startDate)"]'); + return primaryKeyElements.concat(secondaryKeyElements).concat(productNames).concat(await this.getUserProfileData()).concat(subscriptionNames).concat(subscriptionStartDates); + } + + public async getUserProfileData(): Promise { + return [ + await this.getUserEmailLocator(), + await this.getUserFirstNameLocator(), + await this.getUserLastNameLocator(), + await this.getUserRegistrationDataLocator() + ]; } } \ No newline at end of file diff --git a/tests/e2e/maps/signin-basic.ts b/tests/e2e/maps/signin-basic.ts index d661c3ddb..315233b0b 100644 --- a/tests/e2e/maps/signin-basic.ts +++ b/tests/e2e/maps/signin-basic.ts @@ -1,11 +1,14 @@ -import { Page } from "puppeteer"; +import { Page } from "playwright"; +import { User } from "../../mocks/collection/user"; export class SignInBasicWidget { - constructor(private readonly page: Page) { } + constructor(private readonly page: Page, private readonly configuration: object) { } - public async signInWithBasic(): Promise { - await this.page.type("#email", "foo@bar.com"); - await this.page.type("#password", "password"); + public async signInWithBasic(userInfo: User): Promise { + await this.page.goto(this.configuration['urls']['signin']); + + await this.page.type("#email", userInfo.email); + await this.page.type("#password", userInfo.password); await this.page.click("#signin"); await this.page.waitForNavigation({ waitUntil: "domcontentloaded" }); } diff --git a/tests/e2e/maps/signup-basic.ts b/tests/e2e/maps/signup-basic.ts index 5d3153688..c559a23d5 100644 --- a/tests/e2e/maps/signup-basic.ts +++ b/tests/e2e/maps/signup-basic.ts @@ -1,14 +1,21 @@ -import { Page } from "puppeteer"; +import { Page } from "playwright"; +import { User } from "../../mocks/collection/user"; export class SignupBasicWidget { constructor(private readonly page: Page) { } - public async signUpWithBasic(): Promise { - await this.page.type("#email", "foo@bar.com"); - await this.page.type("#password", "password"); - await this.page.type("#confirmPassword", "password"); - await this.page.type("#firstName", "Foo"); - await this.page.type("#lastName", "Bar"); + public async signUpWithBasic(user: User): Promise { + await this.page.type("#email", user.email); + await this.page.type("#password", user.password); + await this.page.type("#confirmPassword", user.password); + await this.page.type("#firstName", user.firstName); + await this.page.type("#lastName", user.lastName); + + var captchaTextBox = await this.page.evaluate(() => document.getElementById("captchaValue")); + if (captchaTextBox) { + console.log("Captcha is enabled and should be passed with the sign up request."); + } + await this.page.click("#signup"); } diff --git a/tests/e2e/playwright-test.ts b/tests/e2e/playwright-test.ts new file mode 100644 index 000000000..1291f58c0 --- /dev/null +++ b/tests/e2e/playwright-test.ts @@ -0,0 +1,82 @@ +import { test as base } from '@playwright/test'; +import { Utils } from '../utils'; +import { ApiService } from '../services/apiService'; +import { UserService } from '../services/userService'; +import { ProductService } from '../services/productService'; +import { ITestRunner } from '../services/ITestRunner'; +import { TestRunnerMock } from '../services/testRunnerMock'; +import { TestRunner } from '../services/testRunner'; + +let configurationTest = base.extend<{}, { configuration: Object, cleanUp: Array, apiService: ApiService, userService: UserService, productService: ProductService, testRunner: ITestRunner }>({ + configuration: [async ({}, use) => { + let configuration = {}; + configuration = await Utils.getConfigAsync(); + await use(configuration); + }, { scope: 'worker' }], + + testRunner: [async ({}, use) => { + let testRunner: ITestRunner; + if (!(await Utils.IsLocalEnv())){ + testRunner = new TestRunner(); + }else{ + testRunner = new TestRunnerMock(); + } + await use(testRunner); + }, { scope: 'worker' }], + + apiService: [async ({}, use) => { + let apiService = new ApiService(); + await use(apiService); + }, { scope: 'worker' }], + + productService: [async ({}, use) => { + let productService = new ProductService(); + await use(productService); + }, { scope: 'worker' }], + + userService: [async ({}, use) => { + let userService = new UserService(); + await use(userService); + }, { scope: 'worker' }], + + cleanUp: [async ({}, use) => { + let cleanUp: Array = []; + await use(cleanUp); + }, { scope: 'worker' }], + + page: async ({ page }, use) => { + page.on("console", (message) => { + if(message.type() === "error"){ + console.error(message.text()); + } + }); + await use(page); + }, +}); + + +export const test = configurationTest.extend({ + mockedData: async ({ }, use, testInfo) => { + let testTitle = `${testInfo.titlePath[1]}-${testInfo.titlePath[2]}`; + var dataToUse = Utils.getTestData(testTitle); + let mockedData = {}; + mockedData["data"] = dataToUse; + mockedData["testName"] = testTitle; + await use(mockedData); + }, +}); + +test.beforeEach(async ( { cleanUp } ) => { + console.log("initializing clean up functions"); + cleanUp = []; +}); + +test.afterEach(async ( { cleanUp } ) => { + console.log("amount of clean up functions: " + cleanUp.length); + for (const cleanUpFunction of cleanUp) { + await cleanUpFunction(); + } +}); + + +export { expect } from '@playwright/test'; \ No newline at end of file diff --git a/tests/e2e/runtime/apis.spec.ts b/tests/e2e/runtime/apis.spec.ts index e56becc3e..a67af983d 100644 --- a/tests/e2e/runtime/apis.spec.ts +++ b/tests/e2e/runtime/apis.spec.ts @@ -1,43 +1,36 @@ -import * as puppeteer from "puppeteer"; -import { expect } from "chai"; -import { Utils } from "../../utils"; -import { BrowserLaunchOptions } from "../../constants"; -import { Server } from "http"; -import { Apis } from "../../mocks/collection/apis"; -import { Api } from "../../mocks/collection/api"; -import { ApisWidget } from "../maps/apis"; - -describe("Apis page", async () => { - let config; - let browser: puppeteer.Browser; - let server: Server; - - before(async () => { - config = await Utils.getConfig(); - browser = await puppeteer.launch(BrowserLaunchOptions); - }); - after(async () => { - await browser.close(); - Utils.closeServer(server); - }); - - it("User can see apis on the page", async () =>{ - var apis = new Apis(); - apis.addApi(Api.getRandomApi()); - apis.addApi(Api.getRandomApi()); - server = Utils.createMockServer([apis.getApisListResponse()]); - - async function validate(){ - const page = await Utils.getBrowserNewPage(browser); - - await page.goto(config.urls.apis); - - const apiWidget = new ApisWidget(page); - await apiWidget.apis(); - - expect(await apiWidget.getApisCount()).to.equal(apis.apiList.length); - } - - await Utils.startTest(server, validate); - }); +import { Product } from "../../mocks/collection/product"; +import { ApisWidget } from "../maps/apis"; +import { test, expect } from '../playwright-test'; +import { Api } from "../../mocks/collection/api"; +import { Templating } from "../../templating"; + +test.describe("apis-page", async () => { + test("published-apis-visible-to-guests", async function ({page, configuration, cleanUp, mockedData, productService, apiService, testRunner}) { + var product1: Product = Product.getRandomProduct("product1"); + var api: Api = Api.getRandomApi("api1"); + + mockedData.data = Templating.updateTemplate(JSON.stringify(mockedData.data), api); + + async function populateData(): Promise{ + await productService.putProduct("products/"+product1.productId, product1.getContract()); + await productService.putProductGroup("products/"+product1.productId, "groups/guests"); + cleanUp.push(async () => productService.deleteProduct("products/"+product1.productId, true)); + + await apiService.putApi("apis/"+api.apiId, api.getContract()); + await apiService.putApiProduct("products/"+product1.productId, "apis/"+api.apiId); + cleanUp.push(async () => apiService.deleteApi("apis/"+api.apiId)); + } + + async function validate(){ + await page.goto(configuration['urls']['apis']); + + const apiWidget = new ApisWidget(page); + await apiWidget.waitRuntimeInit(); + + var apiHtml = await apiWidget.getApiByName(api.apiName); + expect(apiHtml).not.toBe(null); + } + + await testRunner.runTest(validate, populateData, mockedData.data); + }); }); \ No newline at end of file diff --git a/tests/e2e/runtime/products.spec.ts b/tests/e2e/runtime/products.spec.ts index bff2b0f66..5fdcaa787 100644 --- a/tests/e2e/runtime/products.spec.ts +++ b/tests/e2e/runtime/products.spec.ts @@ -1,42 +1,35 @@ -import * as puppeteer from "puppeteer"; -import { expect } from "chai"; -import { Utils } from "../../utils"; -import { BrowserLaunchOptions } from "../../constants"; -import { Server } from "http"; -import { Products } from "../../mocks/collection/products"; import { Product } from "../../mocks/collection/product"; import { ProductseWidget } from "../maps/products"; +import { test, expect } from '../playwright-test'; +import { Templating } from "../../templating"; -describe("Products page", async () => { - let config; - let browser: puppeteer.Browser; - let server: Server; - - before(async () => { - config = await Utils.getConfig(); - browser = await puppeteer.launch(BrowserLaunchOptions); - }); - after(async () => { - await browser.close(); - Utils.closeServer(server); - }); - - it("User can see producst on the page", async () => { - var products = new Products(); - products.addProduct(Product.getStartedProduct()); - products.addProduct(Product.getUnlimitedProduct()); +test.describe("products-page", async () => { + test("published-products-visible-to-guests", async function ({page, configuration, cleanUp, mockedData, productService, testRunner}) { + var product1: Product = Product.getRandomProduct("product1"); + var product2: Product = Product.getRandomProduct("product2"); - server = Utils.createMockServer([products.getProductListResponse()]); + mockedData.data = Templating.updateTemplate(JSON.stringify(mockedData.data), product1, product2); - async function validate(){ - const page = await Utils.getBrowserNewPage(browser); - await page.goto(config.urls.products); + async function populateData(): Promise{ + await productService.putProduct("products/"+product1.productId, product1.getContract()); + await productService.putProductGroup("products/"+product1.productId, "groups/guests"); + await productService.putProduct("products/"+product2.productId, product2.getContract()); + await productService.putProductGroup("products/"+product2.productId, "groups/guests"); + cleanUp.push(async () => productService.deleteProduct("products/"+product1.productId, true)); + cleanUp.push(async () => productService.deleteProduct("products/"+product2.productId, true)); + } + + async function validate(){ + await page.goto(configuration['urls']['products']); const productWidget = new ProductseWidget(page); - await productWidget.products(); - - expect(await productWidget.getProductsCount()).to.equal(products.productList.length); + await productWidget.waitRuntimeInit(); + var product1Html = await productWidget.getProductByName(product1.productName); + var product2Html = await productWidget.getProductByName(product2.productName); + expect(product1Html).not.toBe(null); + expect(product2Html).not.toBe(null); } - await Utils.startTest(server, validate); + + await testRunner.runTest(validate, populateData, mockedData.data); }); }); \ No newline at end of file diff --git a/tests/e2e/runtime/profile.spec.ts b/tests/e2e/runtime/profile.spec.ts deleted file mode 100644 index d52a4e646..000000000 --- a/tests/e2e/runtime/profile.spec.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as puppeteer from "puppeteer"; -import { expect } from "chai"; -import { Utils } from "../../utils"; -import { BrowserLaunchOptions } from "../../constants"; -import { ProfileWidget } from "../maps/profile"; -import { signIn } from "./signin.spec"; -import { Server } from "http"; -import { UserMockData } from "../../mocks/collection/user"; - -describe("User profile", async () => { - let config; - let browser: puppeteer.Browser; - let server: Server; - - before(async () => { - config = await Utils.getConfig(); - browser = await puppeteer.launch(BrowserLaunchOptions); - }); - after(async () => { - await browser.close(); - Utils.closeServer(server); - }); - - it("User can visit his profile page", async () => { - var userInfo = new UserMockData(); - server = Utils.createMockServer([ userInfo.getSignInResponse(), userInfo.getUserInfoResponse()]); - - async function validate(){ - const page = await Utils.getBrowserNewPage(browser); - - await signIn(page, config); - expect(page.url()).to.equal(config.urls.home); - - await page.goto(config.urls.profile); - - const profileWidget = new ProfileWidget(page); - await profileWidget.profile(); - - expect(await profileWidget.getUserEmail()).to.equal(userInfo.email); - } - await Utils.startTest(server, validate); - }); -}); \ No newline at end of file diff --git a/tests/e2e/runtime/signin.spec.ts b/tests/e2e/runtime/signin.spec.ts deleted file mode 100644 index b8c77f2eb..000000000 --- a/tests/e2e/runtime/signin.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -import * as puppeteer from "puppeteer"; -import { expect } from "chai"; -import { Utils } from "../../utils"; -import { BrowserLaunchOptions } from "../../constants"; -import { SignInBasicWidget } from "../maps/signin-basic"; -import { Server } from "http"; -import { UserMockData } from "../../mocks/collection/user"; - -export async function signIn(page: puppeteer.Page, config: any): Promise { - await page.goto(config.urls.signin); - - const signInWidget = new SignInBasicWidget(page); - await signInWidget.signInWithBasic(); -} - -describe("User sign-in flow", async () => { - let config; - let browser: puppeteer.Browser; - let server: Server; - - before(async () => { - config = await Utils.getConfig(); - browser = await puppeteer.launch(BrowserLaunchOptions); - }); - after(async () => { - Utils.closeServer(server); - await browser.close(); - }); - - it("User can sign-in with basic credentials", async () => { - var userInfo = new UserMockData(); - server = Utils.createMockServer([userInfo.getSignInResponse()]); - async function validate(){ - const page = await Utils.getBrowserNewPage(browser); - await signIn(page, config); - expect(page.url()).to.equal(config.urls.home); - } - - await Utils.startTest(server, validate); - }); -}); \ No newline at end of file diff --git a/tests/e2e/runtime/signup.spec.ts b/tests/e2e/runtime/signup.spec.ts deleted file mode 100644 index ff892e5f7..000000000 --- a/tests/e2e/runtime/signup.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -import * as puppeteer from "puppeteer"; -import { expect } from "chai"; -import { BrowserLaunchOptions } from "../../constants"; -import { Utils } from "../../utils"; -import { SignupBasicWidget } from "../maps/signup-basic"; -import { Server } from "http"; -import { UserMockData } from "../../mocks/collection/user"; - -describe("User sign-up flow", async () => { - let config; - let browser: puppeteer.Browser; - let server: Server - - before(async () => { - config = await Utils.getConfig(); - browser = await puppeteer.launch(BrowserLaunchOptions); - }); - after(async () => { - await browser.close(); - Utils.closeServer(server); - }); - - it("User can sign-up with basic credentials", async () => { - var userInfo = new UserMockData(); - server = Utils.createMockServer([userInfo.getUserRegisterResponse("email", "name", "lastname")]); - - async function validate(){ - const page = await Utils.getBrowserNewPage(browser); - await page.goto(config.urls.signup); - - const signUpWidget = new SignupBasicWidget(page); - await signUpWidget.signUpWithBasic(); - - expect(await signUpWidget.getConfirmationMessageValue()) - .to.equal("Follow the instructions from the email to verify your account."); - } - - await Utils.startTest(server, validate); - }); - -}); \ No newline at end of file diff --git a/tests/e2e/runtime/user-subscriptions.spec.ts b/tests/e2e/runtime/user-subscriptions.spec.ts new file mode 100644 index 000000000..9a1b00dae --- /dev/null +++ b/tests/e2e/runtime/user-subscriptions.spec.ts @@ -0,0 +1,62 @@ +import { test, expect } from "../playwright-test"; +import { SignInBasicWidget } from "../maps/signin-basic"; +import { ProfileWidget } from "../maps/profile"; +import { ProductseWidget } from "../maps/products"; +import { User } from "../../mocks/collection/user"; +import { Subscription } from "../../mocks/collection/subscription"; +import { Templating } from "../../templating"; +import { Product } from "../../mocks/collection/product"; + +test.describe("user-resources", async () => { + test("user-can-subscribe-to-product-and-see-subscription-key", async function ({page, configuration, cleanUp, mockedData, productService, userService, testRunner}) { + // data init + var userInfo: User = User.getRandomUser("user1"); + var product1: Product = Product.getRandomProduct("product1"); + var subscription: Subscription = Subscription.getRandomSubscription("subscription1", userInfo, product1); + + //mocked data for local runtime + mockedData.data = Templating.updateTemplate(JSON.stringify(mockedData.data), userInfo, product1, subscription); + + async function populateData(): Promise{ + await userService.putUser("users/"+userInfo.publicId, userInfo.getRequestContract()); + cleanUp.push(async () => userService.deleteUser("users/"+userInfo.publicId, true)); + + await productService.putProduct("products/"+product1.productId, product1.getContract()); + await productService.putProductGroup("products/"+product1.productId, "groups/developers"); + cleanUp.push(async () => productService.deleteProduct("products/"+product1.productId, true)); + } + + async function validate(){ + // widgets init + const signInWidget = new SignInBasicWidget(page, configuration); + const profileWidget = new ProfileWidget(page); + const productsWidget = new ProductseWidget(page); + + //sign in + await signInWidget.signInWithBasic(userInfo); + expect(page.url()).toBe(configuration['urls']['home']); + + // subscribe to product + await page.goto(configuration['urls']['products']+"/"+product1.productId); + await productsWidget.subscribeToProduct(configuration['root'], product1.productId, subscription.displayName); + await profileWidget.waitRuntimeInit(); + + // check subscription primary key + var subscriptionPrimaryKeyHidden = await profileWidget.getSubscriptioPrimarynKey(subscription.displayName); + await profileWidget.togglePrimarySubscriptionKey(subscription.displayName); + var subscriptionPrimaryKeyShown = await profileWidget.getSubscriptioPrimarynKey(subscription.displayName); + expect(subscriptionPrimaryKeyHidden).not.toBe(subscriptionPrimaryKeyShown); + + // check subscription secondary key + var subscriptionSecondaryKeyHidden = await profileWidget.getSubscriptioSecondarynKey(subscription.displayName); + await profileWidget.toggleSecondarySubscriptionKey(subscription.displayName); + var subscriptionSecondaryKeyShown = await profileWidget.getSubscriptioSecondarynKey(subscription.displayName); + expect(subscriptionSecondaryKeyHidden).not.toBe(subscriptionSecondaryKeyShown); + + // check profile page screenshot with mocked data for profile page + expect(await page.screenshot({ type: "jpeg", fullPage: true, mask: await profileWidget.getListOfLocatorsToHide(), maskColor: '#ffffff'})).toMatchSnapshot({name: 'user-resources.jpeg', maxDiffPixels: 20}); + } + + await testRunner.runTest(validate, populateData, mockedData.data); + }); +}); \ No newline at end of file diff --git a/tests/e2e/runtime/user-subscriptions.spec.ts-snapshots/user-resources-win32.jpeg b/tests/e2e/runtime/user-subscriptions.spec.ts-snapshots/user-resources-win32.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..2af066a4c1b6090ac60fd54d27b8c81c9358981d GIT binary patch literal 40989 zcmeFZcT`(RlQ=5pY!AkO4H(8mn`poSlN}J4Yylz&BnCtGC$*E+hlZg=0R>gu{3s;m2`?`RD0 zi;kwYCg9jH0N@zw2RNEI)~EgWvGr3ULrrZx&_5Cy0jzNHE&$-_=8ZDadi1Ng1@zZ5 zWB+*L&omo^m&c#?|01yPGJF2S4ghpX{ug=vyXbRv_Ff29h85Od2*o1KV)hyfzUKH3 z`1YT$?LT0ZKVd&_4{uhUr+>mI6Qjp0*p3C?cKi>p?SFs~9;iR_zh~u9adq+elh&W% zPmV9zyFrXu_p_`&FTe|61keIJ`m_A3XI5}e2LSGG0stpI{1s=L005M}0RXNK{1qqk zI{O6`&1p10VoWEJzk`7a#+WKN5>h!6Tr%#_cedf&Rv*&*~ zfBxLL^Vcq4x%dm$wd>cpu5og5-xTKO<`LxOr_MO{* zA~|;E%$f6N&$FLD&wiVmll%7n>u~f9z;^b;@23DKjtKycvmHCZcI>E@Re)oxh(E*s zQ(6Fv*)u1OA3MuR1^)s#cJlbK<0np?I(h0Gs}QFEtcR1Q*iK))#ePlZjKD*%3B(4M z(aj<2`QohLBO@P9IiNfW`?;uC$kv-&D11-hzOmP1#LFLDzOmms##tz@{~NKtsIkcY ziISB9WMh2`$B&;qef;?GV<%7i$!xX7lqM1dwSd>sOBeCx=u z#Q)ZQO|M}xKux5r|7HiTL?O*SRAEl-6m2nl^@23-_iUG@tq)+yC=fkoqY%oiaDwZ~_uI7bIOc zQX<_1jVwO2kLRU-HPE$S-aj;r(-e_Po$mr3Tv$VidLRB~`6|pq-C4r=nbi^CMq9vT z*(eEDdO;MXW%AXKRx_rPgloxkU=of1>;=Z{6K>j|r^O;q;E%%QA*X`WsiV^o2>|0lB z+Yo8{?6lAiT)*W{?!RJd%p+D@Hb~Vh|A#E1tYa2BLZ&3X{qNI?!%*hj1nLOjqQc)l zm6sKH!3om{!zhRcdAo<@(yYhXV~+r|>X$c--W65xge7KGgx;v_A!`RcaeP~3DF|Wj z&0|Ma*O}Drd5l}^CBMR~W7w|wdTk4vmS@n|TYumQoe|{>hAOalK+$KIJ!Bs}kR#Y) z{|vE+mZ5)y-D{0#i@a$1{R9Nedq_e02#EBKR~~zJ7(L|0vn1OqJd@m+d;=XPdxZeM z;Hqs~zys}dx?Wky*R7vp=RT5CAXMAUv^e0-C^j5MitOcbz+1kKuz%P?F$sHUw#P!a zrFydPR~|7LNT>S~I?P@BaI$lnGy3@9#~`Sb4D8eHjb_zNj;gREXD;nk z!-_7nz~qjLHRTB4RL1*OtqrNLSU+FXSf{BMHYZQCj>d;pcpa+8Q4enUpN3~Ewf2?! zOu0tt+2(tSWhQD#*U;2QPOJCxU;T!PcIN8|ZI%G3#K$)u0gR^+S{pvv@|)GEu?Mx$ zF8A@Jm7hJX*njOyikg(?*dm$|4lM1Eipz6Lhc!olq|%kg%lD@EnuGJbE~dB%;^^j5 zAaPe&D+H%j{XyA)X}T1(vT4-%)FXH0A1IQeaTNN|x8Fo-*YbrJJt7Mo{I=Dm9SVaw zdDT8X{q2S(`fsW!47+4daFYE@&G_w!!t3SHWu~+zF?ZNHfYv#R%G77WexsxV14uJz zMlrhRr~N)vGJ?|R@pY?tsPc*Lj81;+XCag*X`qfVM}aLkjK+JZh5p=L!brYiqCeGG zHp;aOQn5oGk9nx-SABjeabd?&8`_GY;xCr+6E;lUlxB4W9gr`jqA5h^!Zdm@19sK2(py9 z(U}4En}6=V)(TR$PaWCTZ@+W(X#>iF^Uc67%Y9xt%gbPQ;5j; zKzzhBh;r^hKZHA9w2L_c6w+YS9X`LZ%Ut|!N~LSt^Cg*B0XsPh1m6r^AOno2LORnI z7>{`p5%bhflS`})t|$>j6n;yX!e|nGW%9jqBl^nWy&=04FXwf$=EC{SF zKyHYe6YY$|Kd%hn!!u2`aO}&lAe~Xiz7mByQNe`$&%LX~an&Ci5w->JlPI~os5qrX zmqb$qm^8|_pMXeNzp1#l9#O#6o|{^cA_35R4mh6LE0AFsYyAC5VZb0*$y7|hTfwr+ zin=%>XlJ7D_hfjD;ekrpma_QZVEDnLNax&5&r-tEq?-Z*v!qIy+L-O!s8w#vfCMsc zQt{MkgK20rsN}k{vusdPv8)C6+^be7ktIGVrSlBvsyN#bpu8{>(*@xOG_P(gdi7y) zs?7HaR}Ci1+)djSq**uK)Z1C9Q++ZD;#cqjm)Ef97;r|A>~M&DD>nZPlifJ^CeEDF zhne@vSOo_|E3ddwNG(MS@o-XEh`}I@Q5R_ye7a6p=_hV>mY!f-;|!hhO02w)>0HI5 ze@B3sG^2dg^HY({yHJAAX?~R z0uQt;!Z7FF;+z|8Rh114_Dz~YN>K&4<%}Kxe&yp@lEv}JPiaYbXwF$hZJHp(NL7ao zWw6G{pds`fUOg{*um!Y-B2R+Y&atV9A?ol>>3KP@#B>2<7|wv_lU4|lJX%X%S#T3h zEGqC9Yf6lohbi6Ri!yN!R@Tk5j)@CzyxYRj#4Z_aK1U%HGGyBxVDu?)>K53z+`YTa zX1kcOy@^)j#TefS;YDW@+$^abl_G_+wK)q!g%JiVQHo8&3d4RkWKfgap=Z5r|D5u4 ze~G3DylIbx!|@8nb(sWhDZ36jTYhL?`vF07_D3HN0 zE-aNm@vX*~PC0YM3nviYQ}@L5QpBCXn<>0qB|?T*txq*ZkL+?~fqAR0*uk>*QNWxQ z=u`l&o7mp3MVIBD?L(tq>R!0`(`%z4T{(|CEYSqCm{OcqF?ucux%)H25~1w1JU3#? zFE_RSY`;M(eB4LW%wUsN0CO*CojMGz%j@x8NUhhax4>lV^LQ%PV3#%1l}DCH^VAIu z$o7qj(bOp}x%L`xwiB;h-|JM zvLBGshix}acp)$jicLI_@Ck1UVL+{bihigmpHRaGHLWI_3^3c-U@+#LzFNbnOvmyRXSoa{ zNYG-%jA=S&(d`ZRoz)6oJamsyzxgSW$N1dG=Dr|4n-P85Kwm$ti?V9(u(!`sL{l_n z`>NEBP7o`;-3O1;_BH*^SOS|@A`?+!V$RcVxI}(Tty$G|PRRDIUUs`<-tx=CYuGA7 zHVF3-;HrYJAq)sD-%e<`Mtrb%iJ2q#y`w-A8B=-_RO;~dM>8!@vEZdyWq}Dqn|oLa z3-Qi%2u}#B+Sy6%;6oX%B`;#u7w0TgrbRi98^$(+bIuplu1c8Oxy}uUnhxbA4y3y? z9?vtKxuWY81Ve>&NUDtyaoH*F!@(s?4rpbY$kw7bwmb*^MZ9;X7OMb(; zlsdMpnnPDo6YX@;_d2#Os!1G#hS~JTQ%qCtw z!KqZZy@bD0t?nP}C9|W*Eo$?2uEJ~Sf=@iX~ga4@_oD&+c@JNdr`yd)}ckb#;$3!0)WjC;)Eg?vD>-fHm7yufU41C)lU_|I z58g8p-4VyW>svy8`OUTH*NV>~L3-Kr`0u(Tlq#4#`0U7RRpMTxI?=oSYuZ7&n#f}7 zVXSgUw#%abW$Vj^lFfLFw@OccbCstqYAbm+MzY(PkviDIQlOjG`iqsdQ=a{aPVQ_W zS-W_AcVd1UE+TI}MIZ9`m6w`mqipd(hAc*Ht;Fg;IV$Q)_Ocn~{_egIR!OKcdxHux zoeR+%?Hl3aF7?_mkH=<~_FPMl-R_$eOs+)<6w1qlS4!cARrmaiz?;tIUG<(98 zfSpuKGJs{b5=$4rld!6K%UmBr`b^u*uEaXXn)jH?%5F=(b#Q)`n<^R>mN!d6=wIl~ z!k4pV725MT_v;EYjl}j1co*-N3N;1dcD-ugocV61`QmbHe!ZPxjWmlaKfBI52?@o6 zUSKv!y9&Z3Lk>+QLKIWi;KtcnOF@aGr>9Fh`gl_<4VEB=6&t0^cdsqT)>TYPoo6`n z_1l;7K&Pvp)qxpB9_r|-FKSx-{vn)kHv~>}N_k{;>OR3dQ5a;hDrzQ&OsgclK?Ux*%$XqM`{jCz zt|1KK&(655FAml-n|Q2w5}Ar@ZC+;2a;vWyomNl|Z&vG)NzmcC2^p5ZTKV?Lmx88U z2uru;9c29TUyn4fvs=WNG^tEktH%K6k9Lo%*Ah`4g~x@`jiC&d0INALgNa7F9~J5C8SmP-yr zaD8i1iZ9|Ndc|7y`XJB?!Ql`78O~$+v6JRQxCZE#LC^N-3?U!qA~S8UD%%{-Ky+_T zWmR+DY!(r^(oN(DB+FhGK+4|l>vlHp))x>EO&8#Mj{Nq|kp3&~UlRD21pZ42$PEd` z{V{#Ng^P@ZMqHPuBE-}{d&Luoc<(Z_A9ww0{%Dj82DK~T(!L^P{W$5jVO-r~B=?-$ z*j!-`d`dmIe>@HXd>pq|^^8 zJ&{vcSM`ea;7xTaW)mKuR5;YA4b}R&wPb;;tzUjte@CBg^Hh;H>O$B@7LSt!5g^Ts z)LPV^-DOENkh8ajxW^;lH-jJgPi+33566Np>mTr_cej>q*QK#6c|9id(2hdWoa;Zk z{iwE84nLmMZMt<-9|5wC0H}!2k2^lE2_^v>@fPGmTSKPV5kL?dswtWo(lUb`EgxJj z3yrzApfF{3%|%}hi`C*uosmu#5TO~E9bC8d3XTUd+!BEOBW)}~RE;A*+coeZbC_Qk znzuf8+sg6I(uo00)ty!m^v(L!D+#N#7sZ1PW)uRkv0y;p2yiL%O4_5b%`$s)NlaFK3AvM#kKm0B3SU6;mJ#x zrsoCUGzD5IX#t_mgG@%^Vbp6?{gZZ89S8C9ni)xZI-t(|4qFqYojX5Lq~tn~y=*o4 z?*wO52C);WMB~Z6+ZpBYt6>y;bCiqAmqO*;e!lSD&K@8H%vDtt-2T;bzm}|ZAnM_Y z1K$+kSd0!Gna75%frJnyT4}Yh^4X3p(--&oXxmdair)X6SO@na+teg)XX?*t48|x3 z_$X^vLqLb(eZ!t4<1@F%u&9ej32mTb^=EXx5s-tIwlmjQU0dC#&3jLxNjb64f<1ZD zFCEC{R}_5&(DH2@wcX;~Fo{}+dRrbYQwDd^6Y%aJ&vGd;Cq7|R{=ry9b?V;3iH{SL zJxF6RjO3;zhdNzgQmNa;3(g3HfGfoaM8BO~M!*?i%Hm#si!5(y7pmVME1;!O8`-8e z1&>P?5D;+g)t-&W(%Nv2de3|uZVF_x7FtH-)CHNb#*;%~W}#0G3gfSk6_8J7Zh6n= z^|M#|>=j4&tP0Sc)%Rn(iiX?6Yjk`y)(%rQ$*H}vxEnF?F*d85LdYkrmd`S=th#d^ z$t8hy=1eM>&TY~t(W#ZP`7eLax8H61+D5yD&sI|f(TuDR%N;+)V36m~@yyXqcWhP8 z&Q~&9VRKGS_ih6c9ltS5H0NCw5=|(W zD&(j>e3lpbyx_1jHZ-&02ym|82r%*M5#ZD7@B^pyBS1`7l#k>v#kJ zmFFE7!`tST4IIwpTM=K|<$s9JbwQ)Z zLf9jKeWzk$`RMS<_N~uM`_?tZ_o>zxRO03WO7C%OJ#@Z`BdoVmGkwd@WOUU~e@<>+ z>-~C*mDv?Fg#kT}&X{C_w<2Q;WrDWkd&BsxPT$cXD~|%l02gVM(q^xUgtDX(WPn-v zQ`g|^!K^J9sj=4Zael3_NmmiS+C_~vlR8V75Wvgj?`+loqOIsB$n=aOG%c+eI+oSj z6e^&H2r4k2eC567L^y79TiOTa>|B+8dN4_XuCQ%Z47WHz-mm^>J7+`J&>pM#F%`75 zO|fF8mxi{fO)2^Euo<6wO^mFq$KiV-f4s>X&0$z4GRpV6zZ`eGz~eS8^?Av+JpZo4 zecac|t94-_80*Q-+PQwu#r4-e8ziStJNb!I0jD8tK7>2mn)Q^tY;u&LhGE)JN~~rd zr%02GpY%_(N*iKFbp*7y4!kV2G&NtQITZXSzb%;% z52JyseS#sE{y5HzWPd-ZSbN{B_#%7RnT;3qkb=MEZWB#3vk#T5w{j4lC%zu&NwY_v3bO@4xtCu`Wo<%0 zTglZx2n4d%^1CRvLkitSM)?z~8%VZgTpshjo1p02f~01esZo=srHzN^on>snTy!hc zq2goQ>Vkhz5$}8nPnIbuxj|rwm$oe?jT+-D>#ElYg&7!{VbCVEGzV#}rWI52YH3Zx z)Bs~$&oSLs!?etp0U53BMk|i3c+8gy$I;6q+8-HmZ|=kzBn)qarEGK$Fh}b`uRGS$to>i6*svhQv=2bJ_OD7d$~UU^93V?m@c^8 z@;xkceJN)q&hW}E2~(BgEdPgraLl$d7k@T_#VHSpG`SFta;S1X1a9ad~$>0 z8SaT4l{%c^<;=@iw$NgRX6X(-Jyh=Fp8A-|bOFEpK*bDGZ^umPQJZ~DjdF|y8;9=} z6XTZdL}upQ-;GCba+B`(;>ku=G}!(V|<<6w>W++zEqocH+$)EpzG3olhA_#XkB zA;lkr&Phe9NdxN&neyP+GI+;mr>%>(hweZq41T9tPm z)Q4aY18<=fHSXP9aWB0Aqi? zjd8!4VO6eGRw!CAj2GBQQ)zzo<#50sgSaK26-6-!LW}yA-;gaZ2Ahp{M`j+LiZ9hm z(Sf(gZswq24*PJ^{By(WPc}6)kb(G|0(Wdut8tcdySh?kAjO%MQFMi&99z~aPI*PG zootXT{$pAe+>Kikpn}Ud0u!=WR_VYcwFyQ%eY(a|uik)w2-=v}RdK~~KuOLJFb5mh zXK>m}-RT$QGq0F7BHE3Ot7S@_130tcXz&Iezt8S8?|krODsSQ-VP>jS#YKCr>Evw^ zLvV!V46atU-tv=ni?f>1;5Y-0N?N zZPgMGHQi)LQI7x>>#+(;{GtXbB_}R4EKCYT*GFV)2AI&0Wo@a3aEvirS9 z09;_`cz5V#y>I!GaLsZ7r>pXF?Atu60$s2YgNt42L={94hi*JwQK4{VjZYCyba`1P zP|ia_6>`Zy_)_OxTb4#9yZB0m_ZbPjXzdJE6T?re_wjbLEyhOs8#?a_pl^0k=TJ|U(hum_p&wVWgC}$- zwV`X#PVXK-G}BoNDa!b~7SEyTM6Yu$u(7_yYv$J-z1jL(nxtV>xUS^dDAgWDY48zX zI=r*+X;r;aw@iU9J-ijUj{}1*2ORK`9xxn=$(8!{`*txsr7$zY2n}otL$u~uwo|?1 z+W;S=N7!U)@U^O+g{EsE!;0c--ZvlLx-53xm+oukv%zY!L{9ER${0M%wbQ`Y%n!O-FzyY8E1D&!#aFc6TdR z@#2srufSNRg#yEgw_YzipGl)e5MfOyI4G>80hicFYA4Tu)| zB4+0<{H{cp)kWkOiF9rI8D;c45Aqj>IzdLJOMI#?R&f`Eys@1d*zuE8Z|Mv{q(fc@ zZln_3CCJSgYk#aNUC+zJ@)q_t8ZaBqXBVej1bLNGHcWb`A9>NZHKpXv>g&_RWY;2J z>Y{Pk`=HcoDHj+r(8F1}!QM#3UOcLzXduM>eKu-@ohyQPG5jWnvxKEr2M4T&4$&;_ z8s5qAQsxl5U*KM0&10zWvBmslR=oN87R=tp>r=VLr~^LT6%1V23)ht68Vn6zq>Quw z7*;O77L>hENz$#06B{iW4AY$x)vd5*ubb`JBBcZm?loU=jEG`ZFR8Z7MyIo!Oqo_8 zELXYK6qb!7JNEGNm7k8t@TFG+r~&BL{K>SW4TEpcp0xsofnSq^_TrZ1v<; zu`S`$7P6CUYE#a^UY~L|+IH$3oC(KPa-=J}7Qfa@=IC?5qu@Ygow8 zTw%O1Kd_X@^746&ELofxcZ4y4zaRhmy3fBTpKpKsvVKh>5Zv0c4ph->CQD$Difypc zV6i0qeJ-*?O6(H=M}Wj>IZ<4_-Ez;WG{R~Ik|5+|3Pd@LL?nR>UoM>=vG}&#Ac>7Z zT~y(mwsjI7%!sv?DiKpOULJ|MdpzU)YQ8#G9ZC6wrjj?Uc}tUK=A#H^gFu?`giMX) z>Jvcc{wpVnVk|pXoAZ>Y1ihg5(V+(=WjFSV-H*8|RCU>ZK=mbuAb3R7?-*S0P7TAn zH5E)6>?-|kJXU=|u|YM#V_5;KyB);g?*{bkhi^6KTPFn$Rf_N5b9pFuu7XG6-KV;a z*2(P8xV%f{sO+?BZS%a-DiPrl}_2Dg~}kB`BkQx$5r)U<_6oUu?kvaF@67-HxL z&+a>>%pmnDzZhx z7xNM2XsCJM2pr$IgG+G>t(qE)cZ0y^jS&Yao_g+rdP^7Y0oR^Q1#0s~JA<1eBJ%jn z9!cRs_}R_RUAOhP7%Iqgtl8l+WMU9!;AldYHxWH)h7W@MLDvdZO#`-DV&hfWAELCeS^hAm;6lE zoYlh^>o6EhRe5fcQR&XOeyIMw!G0}4$0ta+A80!mOKe{aWY4O^c_)IbH6HzO>_vRv z5#U7JWIi?eQM6Y%jhJX!&uR}1^qV?IHTHP?^6AVKHHQ;H~IYH*C)31wn+}^0aOTjzqlN>Ka_%b?cg%* z<>}}?S~EqaOnk*|);aU;&^V&z7N^_WTlP=)$=`BNe6xFXX7u!pxZ7hi0@BfP7Tx!y z1Y+L*NA8S(qA$kFxU1Snqp;r5t>t6fTjQIT6ypj9Umz$G8IxLq5I%hUh9ZY$u|X!I z-%P9iapX=jZ+79(tkr~=kH6)L!U=(m(aOD~?Z5?wdcYMf2c+fYn89?rzB!rsC>NfU@mW!mb;XBsvOJFz@<2n>R~D z`E4g&1chf#-u6jrU7`8LxZiuCy-hTA2)Ry0xxt=e%t$V(}Zn zvNkyB=Zm((f;~wWX)WI!{G~7Q9K?rCUBUqt0zZxb=M8Y9y2X)MdmDL&>wPm^5^xIE z-jwe4ph;1DLD3cm4&oP?k$6^pav(^RtK12EEh!;Gibs@VA-c)p$M>;@t2MZk7&BOg zpCHFqePD%$`9V}_0B~a$qxm4O)>NAuOmZRfmJxMjG|JbUz=T($(B5pmoEy1H-4|gh# z=YC*RP%QNh&{W<`Ge#ksQ=r6p1nIO3f{)5-3EC(0w=oX8nm|&rr`xnYSO&R7Xyiwa z>O$CN2}DXGacOSY&q0!S=YZ%+`grjwv4RiM_8Ga^&{t#rb(UxzG`lltz6;5nf=6xQ z#VxXTeHVi~#CB%-8|XQyRC38YVcKsgfkb>)#!W<Zy1xp_OsJTzWU~^Su=3cPJX>X^s5efoY2DjsI_Ha zC2-KX^&H=$w6K@}odYMacFanblc=E~d~NU9w6%k80DxgHquj z)nG%+`{i&*(!8^srnh2aZeC{up}42yelmN)``=rW)+DkZWoH`3l{z)Lajl2*)s`lU zDJ6lDV)!_)^MgF+{ap#C1Hba9ts_9?a=K{f%V)%ipqH1m!y*p+CBF4$(_+611s+Zy@557HhjbWcnT2|cStIT9PxB3DTTsFq z+U&6sl5sk%UW44}>0}P$U?qrtpr5c>l=cn#Xu^pECnZN@eMC%-p+VXKz1QssaJlx5 z*Y+Irc}{yb$+*fW}Ujp z3p?NII2vioy=WKX>RV}`!J5{{{U{Kwv&e88bkV*B;ownrc_?&MlR$J+pxhjFA`Nd1 z+PznEh=awH$4*@Gag6sC`woLzdqn@}J|Jz$?3bNiH>);eANblYg+FnHNU5nG67hU-! z!W|?C5{>O8%qS`Uh$V(KbrfFRI(Y0l)Xy?T5579n*0wmlURsEQxnUk@FH9xaFw;J0 z;OFOL2}bv!2jYqCDVs7}g`}bKp{?I!`RUyhBYeO+$NeosOLWalC%lqjJ-xTkJ5FRO z{94=|MXX}2#>ROj&A9t=#jX-|bQNbFH3*78uhw+g*jU7!!;x*+n}fM*dNCT&&V~f5v_^q~WPp_P5S2`-OeZQx5u3 zm|8cJxh~7DLG9wDt=$B?6j_3Vz)d}azPjGCa0~ukUQ+c_l39{O^t{2_?Q^+%WwO=kM@2;JWKWOw)s{zEZ&s2qG#^kn34q2T^QO+S?El7a~Iw4w#>o z#Ys27a9O2Sf6Ph9P))bPwukCWAgCqoNQQLl=QbN@?N5~wAHB>(u-H1IVcwjNi0eJR z+So#}Rh~d)bLExA<rjg`Os>9?qfn2s|GJ?1zA*Xvv@v6f7Iauek|De9OnX8#QL3a=e zyA*VC>1{v?uJp7~_b(t@ns05@#l;|6_(6r9rNEe&(a_;dC=YLjG*E9ze&Ahe!~3`;v~~m;e50%1INNORFgKTp7ad;j zRmOD2-i*(Z%Z|C2F|x;l5?ET)l-5ZXp#s$tI7_dR;zYN;2g5T1ssQQ zwmdMmlC|B#EO=|s952l|-ab5#?W!=@e1Ox%mA)@}6DuiXIy3}nLFrx)iY&T%s#m9x z4(WJDq4O`$Rq4BtOw zpHmthOgLjw05p|MyE1}-M^PHsr)EZFn!6DiB;?< zYuv!Xb93>d!*lvRA!`ej(`doqvY-&b%ky6bDc4GF+IAb@unpW~4khksF?@d1VW)HX z)`r^rYPk>DAIjEbV^doe{lbbqk0M9Z&M%D?#dm3yxSwxLrnzXLXcuVZG2)OwrGZ}O zymGV^4K(LYFzevB$J%KcVyQ?`T4}F}vs>v`Ho1SW)K4%ImVypKQ;jUcHnne7g=Lsl z4pW)%rg{Ivk+x~ui7um3_{W$jiLNivInsKBYhLMM^@YGTuLqmfoZ(hla1xbh4p2fyJ6wutLLhtc1Gtw&WZF;SHqi1vF1eRgt3qRDkt5$90gnx zew*{U&9$X7w=!wCkudoAn*LiI*%QWC$)ebc{HFkUdr|_QWmF>*x96raGgV=ZbM%G;~{Lib)HH9?Q z9Nv+hYe=A1+l?OquElSR!H-9`u2~m)904M9cRi;Ar2FrO)(Icn=X(Bs5B%X&IaG;rSVN zMvJl3D^WQ>^Q<5~NdoB&d|DB@1SG2$DssrpL+rA~jT>VmA?gj zjHlwA`~ix=fvIUFEFZgwvF5-acCjR991*hIAnI-S;Q!a-3C^qW9EOn(1w#)>wksLq z)w7ZO6ADLw`RU^;tYcK94j{$q{Jd4$t0FI1dkPSbP~z&61Bu zf?Ynt`TOwHpAK)8=gX-HB$Eh43kJ;qJ-xxmWs17zj=2R%&^KJ)rE z9pu5TAuMHBVzFwCsqbez18lwf>6xizoI{Y)ai|xbme?%IF_P)`y4RcH&DxzK6;IF9 zcLm&bUt0v8eBo+nlPaN~0>0Ba^%gjiCM(GG-QAucs^iPr463L{T&PqeqQO?QIhbN@Zuz2C zUU#(c#qXyr0+x^Q3xxn#1Lg4U zFCwy?t@$GrnhoQdO6o@d88*wU*aB2CNU3;HsW!F&buaE@4qR6%Mr~wQUZiZ8Z{2sv zNj?ca276VXk}K8ymZqHmbovydCCKH9hR8=muBpEL9dJzRyLxws>fuf6Lu1_^(Jw=D z_tE=G)%)4;&;N$n4iHheB%#-+*1{tgSm9ujXrAG|m5Gfq)l5UW4k$@!$g$U{sfD4Hi$H^fx;QViZl z|Kg_oOH+zpj=wun5n3tLneOCW-al>`H$Fq~ZHTr#-KCk|F^~rgfMOt;c-98OEkPUy zcWz{NUgp4ypz~)?L&ly_m7+`6CxWUj2*mQ($2K0g>%Ty(-t6Z<1JMEn-?pM-Ceq-o z?WyLLP&_O_v_QxJ!(ozJQPesJ6nQbn?e6X@gs_@sS_kCjrhD{g zD#btlN&Pf#6fvm1SapR>AgZG?Wx=L-g!m||&VnjHn$!}t-?=@ZxcReMmbnoS7#~+G z)}x3q%>*BDpNDthz+XTtb?F#Z*T2dvkoT-D>{rR^9Qdo zHmXIB0Bnv2!XDB90J!T20GAyd@2;L6kUp;zx=?SquN1#O2pVIJJf1pum83Uki=K(b z9P0n7D=WlQIcpOIfzr?&1-?T>U+neja%#s!>xhop!?z7~$ZmYeyd_x~kS>q#W?XSB(>5dMiW@zMqR?^{T}%ZVk5V`-<568)gDum z6~hC}Di8$w<#<2op=NoP`?Boe(uf(+Msi>4>wC-P-R?MOxR`Om9~7%{q>>%7z7Aqy z7|=BT-1235uRw!os~qP{f7}8tce*$4URxw#D@cos?_-&Zf}if>HIYpjeR*)FkuQS# z+MW4%FnnECY{T3NYf&5wzFFM4|GbPm?jSGX{|ya?rLCR&4gy+;N$|*|NV06 zx;(2;-a=X_^)06b94v+`>@_kgFCUhAmYZy&t0t`~H|GlAb;Xf)9XIVun!jjzHlcGzbmm zFv~!OQ`|Nxo7jT1Bb}eI&c^x=H9D?kc;Md>`p$@TlF73R-~QRi@_#7qc=X#NK)OU| z*ys^}@TubVw)?I?!*qXPUD62TpU%Q{KVMbt?jP`2D6QeEj7Qx!tC{yFcbHoPbVfm14(i>fv7#M4 zUt=!1tb7uJ`vW*QD8iI@qDLye#Gyp9Y@W2S1*hqYQitXr#MK{K6%1D@Rv0iytyK!_ zZoEB#tsev}kho{Ip~k^mGKGS%y-r!E!2MQUbE~d}I-S=~H9xLUWcqW38|6#Gh*7Yv z?hpFKnw3r6v}juJRHrxdQK|6r8hc5QqiA@;5cw)n(Z$p*+SWZ7*N9Kqi*s<;57{d- z)-z^vO@k#b_uQ0|w@B8L{;~Iy>cuDtN1jxsf}AJKlkF+lkUic(Op)9v|_l)A07J6>!BM0^541IYwJ3=O;;>d-~zDZ61M~3%s+-h{0<{s@~~Hthp$=W_%GZP5xPf9zV_U!t zKO??0>*qjkWWRAp2!fZEabVCq%h{js)Il8NmsglcrV2A~b3`>hmfwpXdUm$Isidy7 zN>La!x10j4Gbq6$H8grU!04M2H^NHTQi59+COe0dH;W66exqBFcs{_MES2a5T`IFE zoma@vnbC^{5{BVvGrag-r@?{Ge7$^hmX`chzNTc>Heu3Kxg5neP$g#{T|3uhwH)W~ zHVe{ko-!D2|9HuoXU0!8PsYE*>$0PDn0*$pX}+~yCY&EvClMn+UiB|XP#_YF-R+!< zM>0k#6%^zi%QOj5UAH5z)84V6-aj=@2PrbclQ7id$_2wUCPEl17Q9j^^J_Et zbSI($yixK~x&`;9ri2Voo(R!jiYy-3+JNYb%{`<}0^N5PXRg(1XfkF2Y`RZ4WGk1KCm+T`aH*Upf zHE>f%$fc3x!Orf^ln2~+?*d-|0ZBG;^w@To7`)8WqioS9hqZ4*uf(D^j^)WVmy`+v zvb>!2x6;(>QpTa%wPJT{^ z4k}{pI=soSmAQH^s@?hu&G!&DpfJ2i(-h=MLwO+O#)S!|Pn)<>WzFBTVR?+qv8#e` zk|`lMts=_Ldn*{C^i)E>pn*Y^VdLEyt3iBGJbOR-YUX_7;62yJl5YhEmw}Zq-O9P( z5FUxIl9qkVJG?%-vOjv@MYiHF&+s2w8wA0twsn zYJ(91fshTNBLOyuj-T#*@7PV_da`n`}@9Msfcis z6i`wumlk*M*Au`0dt<*JVgE(rfImFVb>?=Mb2;ghPNP1{O2y_Lx@hWsEZBl#0RYOx zsJAWpM--ALMuh$ls1i(={t&2|7d#pM1zh+Sph_=F=nsJ^*ni{cO(abeVnKwSdmlh{ za>WgDnU(9y8{+LB8jj_cPAEvFYDA&5WkPBCFEmfJ#Dm1f;Od9p{4AtJURfa7c%Bnj zno=^HvpOV=nqKm2(vfI*iRqR{^zbOx06{7d`a`rtUFZ+Nn1Pr74j2=<@yB3H+duE= zIR&EESL7#o6#1rzo*XPm34e`xE=>$Y0lo7Ij#psBrwWD=U#ad#U(?d`CIQD@?5*~| zoC5}zGHt`A8Gs?L_I)%HF+aWcFVNFJacTb*aQ6RnuVOwdy9b#s2ng$Q^`19>w?t53 z-I{=y$~FQVhwEK`5O?;MKWv(R?))c+!0EKrJHT?I*3d?!x8}>J=s_L;$y9ckbeGIK$oF4n6q1A(?occ0Pv>aGSi709n#knYmwodu4%ctIuFt&@T7xQ8pX1r5%&92xvNg>dW}F13qC3am zWWCZmd}o5i_vz7MPZtqmobPqsX=L7?z{Gc%<3T>m$Q5R$dsk!Ez7|elkIDP7BQ%bX z?q@u>4y#Y-_GOvl@8vOdYb}h9G)gHzos_YB56~#q`LmE;?f@CV9{MzFKRCe5uU`~#np-z_v(}`S*wh;ckoRm9GyYbBS(HmxB-Kc}^oI&@>$`Oyco|w?*oFH8=Zy0+$XYiBpZ3}tNdIj%m zeWPbK!vUgI3;v+hWiliZx-Y483^HG*4I|&I+OF^ORvsD}-!1Z!mR$2*_AEnjypvJM zyliC%-S~c`?re(+@%s;P`QH@vPX4C#n53%58wtI&mp3*AE6lE}Hc)GS4~Z3;6(9<- zQT#~$LOgUtwhcAoapT}kP@5{kLwar+bG5w^kV~dU>x0h>#{@LO4TqG-o16CIbLA;V z_IbudyqG{_>p|wOI*wH^H`#5^Li=$q7%)`qTnl%n&dOun1gI{7oII5ds&~G#>+<(C zs+ue3xLmNy@92y8RG;-VnNTqi<1Sv z4A#1xeWSKf!@FgRH!Shi*r|Za4P1rTlx9K;s@j>mf;v|8(yX_|!(j1d(nrHAgVXk_ zAW5*pf%){zNcT>ltS6EStX^sFMC6;V9@wP;(Fd_ztluSyujImnxhCKAa5&cuhs6N` zD4@malK=B8xfPI=c51mp(({X0j$D7NP49a>MdiND0PkOJ9r|Syk_~5Zu{;;--vp^t z1Bb%_1am4ze7zoDchab1_XutB<)%$>|H980*QIP(S)VxnayBg)#XS;4`j-5+|17MJ zzkG5zA!KC*BzLYzP`Way69i)(uA1S=lq`8O{Obi!HHwX%PkJl{E^te7A7p z>=bw@-KQX4V5$5d>X68UIp+1acEu@w-z1(Mo;2od#QC@y&MEd3SYxSST9Q zqNy2VcL_YHAAwCyjO%-o#MVd92%UU^^Jt~}mxY+RuW4H3&KsKvKlK?snhyyhu(caK zbmX%cV8c_E=#+F25A6gm34Z6lp?z~bVA>yx7#~(WjEF54*U@yWX)brS@Xsq%2o6PJ_i{S@QzAslPkO8q-DIR;?yx_6&I849*H#uy&Dl`NvH%}_o zS;o=L+z5SLa-W6ffhMm-J_`Z8x{vTbzc6&TX!9(3Is&E?LQ+=M^O##sPF^Y)$4j8s zs@ZeX^jME--_Jtz#<#&~(KVAS}bXZA}yh=W%`Ji?U?-oK@?*IG}Qjmqq0( zBz^+N|85{8YS<{{Lp0=#O(qEW!uT1P!z8**3p(4aWyd*z6;1cS*TOm8J=55Mvp50d2p^~A+FhaVyg!jpEIuO3QBYIopIsjaiDYkR%@Mw0rX$6+u(SU|EDT&Vmv z(~Ef<@bQ|T_gaC;U1sLc=W-;g#`S8nIZnN{X)*$Z{MHU*zk0}uVD%fJIzBAuiM%-1 zYw=owlrZSi8KVfx?sUc+f7FWE-W5;;@qcxBmMrw;oqvMi(4Uv^Ul0M&774!e^&qhp z3Bs8yLEF+j$Gfxt#tI;=0;IE{27SpF9jMR%^4uz|2myN%p+x)FSgz|zO=nyAFv?iRRu`FKG^_H_S zYC2_r^N%Q&_h=)9mb%u4mHI%2myEq)<32K-1JxC6%B;w) ztE?FKF9_;CPO(7^BA>}%Qa#1!CHB<{xl7#|#_;*IWb=+i4ZRA>L!G2TE<8+b=5E|*X8ZqV4tT#7n6O^_w(h{128Q* z3{nKDIP};5)1n+?YDe@IIy3DXdl8#=KP1{ce)8UDA^m>7i)`-kC;;Ly_yT2aC*u8x zE8xE~7ivcw7f`3)8Y^Vo^g?D@_Ra<4izueYeiQ$z7++6{Z*4x~G-Wiw3Z1tZfRsx* zx`?tVEKuNB^@W`DU+hGaW%JseWL+0a_kI$pR|~Hg0XotzTd9N~ck46??aiC2!^s|| z4s91-KzeeYD?|insRYYkK3}){x1uSLC8-Gd)kePaKce;r8(+MRFEtVA-a~s}^bQfm zlZ7yYiP)7M_g`I;#|iP=*pgB#uK0Y(VVR$vd-F7d996qeQ9+tpZ5Kb+2uc+%JlXo1 z4jULFupN3V-15`!`St56>c~neSm97p!8Ki2Q6uebfl|D4zxB6HcJn|a38^yWbwy_g z5MjiHb49D>?Ml=bO*0PLBcIaI10rzZd|!v8%GJHAXA;6P+&blp-N@3y@{g1x0YC9 zdjL_Rv9+Sd6~Xwp!@+Ien;W~yOPs8*J0CBAQlBSceO|aH^3Un63@p0cMQG|FYcm`y zlyvE|e6zdzX^6dc#C1yX`~}x@U`_oir9{_w9Y19da^A@!gbAytpfs`HF6r4yDXR3k zV7uN@das7)3`JN9tc~i&Qi&YP2aj&54#Cfm=g3;FlfK^f(=p-)0;NzOP=~^P9-MZ# ze~7fX$9>nNdzxI_+mv=s8iDb6E^*j6q{oZKngxhs^+Cf-Ss;MGx05;i+7Qi3H6GrZ zzp_|&UBz7ah6`5R_)YR6Y1q$AQk~DFN50`i7+bx*KhJs;=sRBOCY^CqDknS+uX~Mm z7eHT1GRhPz2-*IU`rFx5VrqmYP3M`jz|$S4c}hp#Y}?FJK_J{dsr|vBNmc3{ul|r& z|5=C;6_%^oEFi*SjKa~Pv%*$zRYT~RRUk1i9MyXLeQA62^|Bpc zg8sG%;+_U5)X^J0_kG9H`~=mURZ?Ah;!KPJW3Rd@-_|~S@DsJNukd<9cerWj_ zqVi^2rDCxBAR$j8&-xdTn>0W1^q?##-IuzxlkEoU6--=jmmLK3L%TygBoht@%wAp5#@+& zZ+uLf4#K)n9px)Kv9+h9?xS@~5+`g*>l8)nTdg6$2A^ z^Xx~VR^o)n<<9RAy;?@wiK6HMMhYS>>qw z<94oe$}z)QLE!3D;OX^<=9<#bxyhZ%e`=P|<7HH^=?*i|z&81*ENv@mC7k$i>k)2= zWzt5{5OMWX;NR}}XxKS@R*E<cuhW4#d210)vjyQA zt7bpkES9N@8t~{75PUuhC04qw92{BMzn*1DKVA!z_d{pJfKMU1?cjEZop)C2F?`+f zn2Kt)$gqKJ45<|ESvWEp+f$#$ammOEzCZFI^u?TNE5L)D!N_vSdrF3i_#Af>q)a+; zf{oJWO9n!&8Y^-N<*t+yHN92Ko9fJQfds5cd5V%Hs($O zMF+^Bbt*PD!RFa(&pn#K>0TZu1X*lG^6@QHv2p8;C^y9#A&ZJx28(O^iS?P%^U8Z) zrhR8UBOa&iseqyCkCUfhNolrfYDIwEl_=&NuNhQ2;qtivdv@(aN;!K&AEb-(^Vz$X zq!wW6Dx0tvASrefl!QEPOn}KW7V~Bx5}zu-EIQRky!)@ze`w3ZwNa zjSlq%`G?e7C49|QK7^EeGoB<ud^o%oW%-=rUQdAT#ZHlM-uNKs$EfOx|(TbcDAb;2m`{X%|J@tl#yxJ{JVx*@iC$^}Rgtw`6kvc!5Hjm6&ta;d^Hj ztPAMgXoM0c_+bwK2qmxOnN7mu9L`027pK+T=ZV2FNL&j*FRe--QL--C{DGvC->iN$ zkDwpTQ-v9drr!CvEbH95^fF>Tg^I~kjv&O~WQgGcrd7&LQs3N*%@3K-E$H<|bER+Df z98)uapB@pKC(=)dQi7nVmR=9Oo*M0)g#!o<88Q*3MrMxDEyDNzK8xcNP1V&Dt9Lig zky_4?qkY{U?IXY2EQ=G4fGbB7txz3fJ=O{Xp5*LXWw<|Zs?8oZ`6^)7ZWC;@UP^RZ zX2%B$e0bCg`q|(7B9W-t`;KpMN=c75tJt$3)9GI1TNa_@6pMA;364MemJoC-wpN~P z)IB>}+EGdv2yG&P=hnRFJb6 z{+_UQiehlA)ue9ytD7_5Xeg)Js%h~(TZ#SA&WBROVslF5&JumIZMDsz$A8Dy>#T)T z?sUS?g#V_3O9jKWo-`^uoubxQwWuVc)H(~^80LyYMoKjMtioCyAXP7%HGIa(N>`)n;JGtsf+78f(X;8Xta4*n1%Ew#PriC=reaLQcEWMO*--72$DFI zb+q)(6}|&oCfCukd8BgoFlu{zuFEHyg$Zh?z0VvJF-{tguMxd@=s8 z;lv%8U7ZBt$G1^`fB)vk)c2G7?E^pk{ryMz6Y9VDtq7#~r~l`N(6g^;e?RQcgZ~WJ zpOxhQ+lI08i0B29&+Lpiib0Fp-;PyM59ii>FsOLkSf3zLG z4UZ6487s3400=ui?;ne3-RFBaaaW3}D#-^=y_P0gLQPTRD3*#K4Ujpxnhaxyp3r#3 zyZBWdTUQe4ja!yQCU2$12nz$XYq!i`wS6lT4-D^rUBn3m8+k{8;an&zU6`doB0y*s zG0OzFQcD8IOvQANdC6Dyy3LmX@k^n9QK63tk_)<4YpbX9wy#lz6W?2jh=};hczSLK zef_(P{-ND~1sdGmlX2SF!e2keb%E^2h@HxOZ==%L-H?)!oAy!&_3st^C9KD#zVFix zDdvcfp3z3eK&+HhV2Ab|JgSICXes1CUf*|9;Kc4t8T7t6on<{&XdHT>>bo$X&{An} zY!wm2lu^#4s1v>Qo8`>1{EUyL)4wo3S7@wiBYQdGhJvH4*pDx2LW~u5S?4zj@KxIc zjfmvrVp;BlZJ>nN6#YhrXutb|@0A9}54LCBfB>(w#1}()eQ)!E+o-LD-eM_74fVAK zVZLnLy^j2+%&M%GM`L|{KR6Fkyn*m&oW0xdBMG|K&se{piG>QRSeG3h1BCd&gfNdZ z?B@%7z5lS-U+j*Vlf}kNP3;5&tfN2t$sg@JcgimJv;kH{#OweN2*{cZIJ0~Rcp!%v zj088PL6===snhAbUPOe(JvHx5?G^nt*sa*NDn%>g_$BFClJLR$RL~LoF&cdlWP;V6 zIu8uYFHO1-Y~+zH>LD4wPJNs$a+lMF*}FE^s{KRPh+G=mCgV$3V%nB zSdgbrohZ$JH+6rDy?KkqolcT{=~GX4 z-c>;9Ps|5DT&%GRcoN=aP6iI!uXLF@fi|*jHuZb2zNvZRW^L+bC`+CQ_Ql?KrDN?5 z4n7%^KdQ6mF7N%I?V>%QwOk$}JZxsSpRgkS^vj{%RH463nSARVd(iVpO}*1)QJiWO zDJ?;cgBzl2P7w9dh%`YWH7+rw{L~WuR?rY~*{rR_Zc@!AyNiXlHql7Rm%O*mYR${5 zcOSFUg<7IRx#8=ICHv2I#h$W*PEp5o;M}Z(I7M?p+Mt!F7~KmSKW68Tr}`UPn4N|v z%6wmGp0oJcD^kkd@%=KW+^OYY?!lQf!o3>bQ|CHK6KRe>i>tTruAD9NDAdeMv2o9E zc=M!Uw@%IO&6|tAsy{vu&44GUyGD!}r3pGRNFtLF_WVQ-XVBKJz8y@v0h#OU?AqR; zwl!-^Cy4%LlAIE(xvwi~Vwa_m$KM@Ug|a3^tX61`J5Q#k%qEWI4~Mi?CVg1)c=yZ{ zDRf%*$A4Q#{_*XUwfpd0wsH%qG*ZM2t}IICzTER|?1Jlg`d*x1%CXLTgjc|a1V3~iKcEiu8<4q|CVl)H**ojxrm6?#-vZru$n=}54-=q6hD zS!m1~CPf*E7nO5J+Az_KQfbSIK8I8wT)yrs?BdrQtMCBa{ls+Hd6IQpI zN%$Et)7`mPnwP4i97(iT1`1D^LN5=zRvWnB|Mu-GPmjcFD|QbuTHI#Cl}g!)kLeLR zN305(0l+X{GGTBxHK&4}HEi5bz7f#Blq(ArnR8l2xK~v(+F8(5DAmqO}NQ`6sff`{9Egk}LM_Gvf zzG>*Iu{PS`u?gNrB1_h3Z&PnSG`npix7yRp+AXIXq+nij2KIv1Avl>R`&BTGXhWHr zg0@C(lY6uhZq3M)%(eRtLDqf`mb%5#{qWW#UR?svXodDqWw|!^#uNbNn8+HOzGD z`%?U5y*xsO5g1Us>-?6}w%M&jx$ZYl381ZFks!wx19!?=&5eeb%I?)j-Ey80=*-D|tF(cJTN73ICwXcv8U1=Agjj$w z!wU$hmK0xrC{>V&tKzkv1$4s!o6+_{ITogx3u*y zQa}TjR`+}K?^<_T3TdHEzd;fzt7!Fh9K;2^lA!yY@b{#L}~l1IBG_E7PBZ2}D{3m4OxG=J>^%wX7?LpB_X7$c}&Gg}t_&*54yb zaZfFWug<2fJW`bG8_4OKSG{clQvjf;-lld&y))HvX5(}teiOsZGq*@Fvq@dP36%!5 zF4erMq7Na91}Ry$qKvr0;$x`!HCl2p(@i2N?M~aBnjiUT=!ISaD3^4XBl?jv z+9Eq8TNbFU3R5)_+uhpEg`u9XU6s_Syr#z5o4kl)kMB~emM`hy7??dUx%xP|);Vu@ zc~X>%?X9jIba;^5<#(#{KU1j=ApnUqkbOBXg|_0O@p0a3IP`M&XsM_AO%|oA=QYRU zgj#Qk2*0Rs=MXimI_W?6VwCbdcCa$W4F9Yoy!o zeqKXh`@+z4rkMoBLhgxo2CghH3?sUdLN;|s_XZtXOrn-3xd}Jm3qSD>Bm0zdNF;(+ zRx87H*%Vf^deA?=#Y@;6EQ_(B%BeF%~w%XIQ5;@L~{$f#OWB z^Si;HrauAq#9Vcs_^q}EoRe2+v{UZG`bBM;J^s*^5(&-(ReSxE*U}IwNZIjB?ta8+Fp2eH_ zyWeM*R;ONlJRehW;6JtPV9ybx`3wZWRFC-1nrq@Tx2D$B7 z&N_SI&wWn_CBdqt9m5Z*aJX$J?iB1%aBh3%gBl$*C{fk@4R#g$YF5wCxD2vIb{WfO z1hFIgd**BhI|uI-S0wgU_@hb`r5J+2Vsfwdr%_HQ(DU0~`v$2AFT^FBnO2()H4YSu zIofZ3k({!#ufQ@W3Dj!KfOyI?{Lgg=% z>}>7c*FBB)abDG-Jx7nS5HT#K55^l39DB44VpMjxs{FO3uK)YytSibxjm$cgwBo^T zh6{G2O9_Xg+@!P4fk4o$l3+ypUE79*ZcmTX($%2R8g#5zy}`r~b!AcD$`G^P0&zWP zeDwURZbs5YRQ=PsNy+mO;Vq;RWvnq5cNV4+5)~4lZJ3xo+Qk6l5%ty!P32Pa&=%%y zU_YMNcI18ma98^y)0s;c*OTIa2XVMsvzd3XPEK1+AUl7g%dc@k0d*P12Ihs6!E+yS zv#=hrKikw>Kt~PWIFf9ZHVDLQeGY-+Sq??&Ntx0L*4yI{UkgQjhkInYd$)g8H%X!j z#g&aD3!eA2PpRKI_T3ITzEu{8kt^eCnuRK0-8*9t?R}0MnE?A6|Cw8Ds66(dt^RByvYu-J#HD^ zvW|dO@QaL{kH(NYr;ba<)Qq7p!xGAg538Ppr&^lGFsY)~M`GYE=&JatLV z62DS?6>>|7D=?d#VeR5rrC@Wack_;g7+J)U;7N^SbLjF}xI?=ztyB#&u@c#f*r0@m zEn>fzoy=iq(xd4xT*^BMLQJLi_B)8_>JL4>Z7*zRs>er}D5;O;KTD`GRUFjrC!A3g3e9}W9h}=uu_%SC_r-LJru_VWnxAJFJ)(W45fr_r? zVm#sMI2}NwO=8A(_s{v$g%RXZ`+4N9tH|8SDAFN76j*&aNoy`HUUk_%n+)umYO3ZG zL|%DThU@2f-?hz>sLACfCMTzir<<)*9objuFzN=N&gYEc;D7)dAXA4x`89-LndX)SC+y8uO*@HG0cg;z4!;m!gjZ zkRzDv7z>Yp)*d;f4mh4!OT1y*l>J#q$uQdRLi^F`%PCNiG`~%)FdTW%-cH{%5E6dK zHZV|4B<~I#$F@itP{HG{gOMNT_16TAu>_ z{6xF9g=~KNEb74z={9i@ir3qvjwW~_fSPev_s3SK%-Q!xS*`GjwZJQpl}GgDRGXa} z4%mPv3(>K9S7nsZY`UweSdC{PurQC<)07Vhn0bN<8i@?4Seqx|UMTf3S&)c`fX zm7CYkkRdNqDcjxi>`x3&{9h)gY`$g=hjHaq9+=OM(~km2`CMvV(q_MUY|-+^s#++~ z9z3e}(KORCAu&@saeptdi;#Gj)O6gQgg~af6W?tkeb~i-v86$i?D)Y7jUNWGXNNm> zjYF=9330lHIcXkPE}tY^R&a`g%nwc}dT-#?H+w80Ev2bAy_ z(TLP{wS5GIwBS803jqT+`_Pr~S*SM#GPkw*Mq)5uf#`+l&G25M;;KI(%AA}Iu-ZYy zz7o$sttW1NQaq<)qYy+Vg4XguYS&ONdtbA1H_c=}xvwq#$U~6J;DgWKxV*LY4_&Uw zq9Tn5-Xr#?E>l?5Ou8wrC?6h>6?G%uf@rz6!)+A?X$ntwS6D~MW%e2H5x+n3g!k9% zIsBU>DGB$fizlTfRh_!K(1Z0? zRX`KWI?|_pg4yb7T%cJD;y`JA2DXyK(x;1D#paUCQVLKL@sJi0zhJs=KN%mGR~&iML-*tIb;qWhnM zoC%h=;bZ-pT2L624QE4F1qA;^X{^4M`0;KCGQB4`&o7eUd*aHS?u|C30oZSj0y1J# z=qKUL%5Cc{Bo0?-)v#;|GPN(WpLrC1zslrFdave_9A+*pi;5`w#o}xU{D2UO1E3aV z_E#zD73km3Tg)?k18akc20!?kh;O5UajN)-JyUtU%3Xe17@wM2X|Z{vKRRp32T4fp z2)4nucfl2@^vNpfQ3NU(*)WrdAn*M(WHZixQzkFcFHC@gnTguDz5Jr<-&u+I4>z|z z5BxI%e@5WX2>cm=KO^wp5CN^YB;D-FEHqj~#QLDEo?zV=OsOjc_77Cmo{YnF`5>tux zFZXvNC;J`CbzHxnuXN?Cq5OeMh2M`WaUt-OGIr=ls5WK<5CmSyg8U>FMVR6U*4R_n zt(oOh4o=G+7&NEAa~5BOWH zUFz+M&+1*_t77nRK!zp9+|}bebUc|Ij$3kVwJ-O$V$U|R?WX1|8d#<@Ni6nrC3$8K zd&2wT5Y8Fgz?D~xBhR#nK4ZB<1FZk4P(GNlm0GAM&=VPF+%etBjq zc>#CWSgcjN(0qQ8@(|n3&uEfjf=e=WgRh}O^@fp!pplq;I&aFI{lMRmqRMGA=z9GT zcMb{2{-Nqg1|YPh#(J|ec}LME+q!EjVvAOVXL_Z5DvZkx@H5B;L_0_~C6loD!Y;ei zT3&9gMX2uWOZ>IPk}W|7V$kmIb1nNVe2;u-Igv4<{%y%*1uGYQ728BdkO6|t@bS1U zoi=;jn(h&(u`N5$Bt70V78ov2jhn8RGE2m?i{xJ{T>`6c;YMC7_GNHfiBe^)Kv;X!A65`4(Q^B2 zYUM!oSCB6rmF&vbOJ*lB#g^i9qmuaml9n}&_NlQdb%M%|{;FxVvfo>7lj|O;b3-C; zg(!wJrKLB|tcywvhF95r*n@l|)RTOaJPg@LLB&9L_%OyxotKcmzc zN3b7;`nP{+V&dmpGgledtFcC@@9-h|G*RpH(k+l`bM}vyDR$v8+=y1tmYtNuN+CO} zTPoh<5}^C&xajU%=XU#g@9@;{*#JakO$MNf9f_wgnkcCDj+TYo0Qe*ks(Jaa>#^L^ zt~p&C7#AopHbd7=Wu3YIqN#@M)AkIRw{AFKb2(JsoGgBZq>US!qEMhqdB@Phz>q$6 zuVMqK9M`B~c8?XU{Ff-}F?eF0r?PbIL#G*<&2pXNhuuiBWBp_505qJr)iaz(JKM&0 z;Dh9HpXztkM%sLbXdtfPxQVAqv-gBUa}gMh(pcvxA>8?CadZr(;7Q!i@3R&{GQMMX9q+rxqGXv=`Ts<{Ws z0m!j7v#^52PRzEh&16hj1h#KdCpoM51gd&8g???Z1T@F61Lri@P8scXi*ehCIf5MY zF3R5iLlSD{mP?BW3>q{lgH!imzjFA#<&>CD?bCVEN7W?ct+cwXP00p3-S%)^VdjG0 za!9PP-KHzyq|fnlnhdALHQM2T7tSPT} z-SB!*N8j9TwChFJ&6>JW6^#LLkpqFQv5#eNG_PQi<^cI&SuRcSR~0)?JyymGR9B+F zK9RTY%rAtqxCW8qeGM<}{&ZX9M9ukd>)@L0;GKf;5`1h-VC(KBoZ_}b@Vqwb zig#CfP;AMB3hPo;ueA=})+4!;r3E6ZVfUyrqx8aH8w{*G^4916>Xy-WnDv-mlpt?7 rn5++KWg_lVc;zkK&B?gHYKnqy&gi(%xyJv7hx(s?@qdZH=gI#8!@XaO literal 0 HcmV?d00001 diff --git a/tests/e2e/runtime/user.spec.ts b/tests/e2e/runtime/user.spec.ts new file mode 100644 index 000000000..4fcec3f1b --- /dev/null +++ b/tests/e2e/runtime/user.spec.ts @@ -0,0 +1,73 @@ +import { test, expect } from "../playwright-test"; +import { SignInBasicWidget } from "../maps/signin-basic"; +import { ProfileWidget } from "../maps/profile"; +import { SignupBasicWidget } from "../maps/signup-basic"; +import { User } from "../../mocks/collection/user"; +import { Templating } from "../../templating"; + +test.describe("user-sign-in", async () => { + test("user-can-sign-in-with-basic-credentials", async function ({page, configuration, cleanUp, mockedData, userService, testRunner}) { + var userInfo = User.getRandomUser("user1"); + mockedData.data = Templating.updateTemplate(JSON.stringify(mockedData.data), userInfo); + + async function populateData(): Promise{ + await userService.putUser("users/"+userInfo.publicId, userInfo.getRequestContract()); + cleanUp.push(async () => userService.deleteUser("users/"+userInfo.publicId, true)); + } + + async function validate(){ + const signInWidget = new SignInBasicWidget(page, configuration); + await signInWidget.signInWithBasic(userInfo); + expect(page.url()).toBe(configuration['urls']['home']); + await page.close(); + } + + await testRunner.runTest(validate, populateData, mockedData.data); + }); + + + test("user-can-visit-his-profile-page", async ({page, configuration, cleanUp, mockedData, userService, testRunner}) => { + var userInfo = User.getRandomUser("user1"); + mockedData.data = Templating.updateTemplate(JSON.stringify(mockedData.data), userInfo); + + async function populateData(): Promise{ + await userService.putUser("users/"+userInfo.publicId, userInfo.getRequestContract()); + cleanUp.push(async () => userService.deleteUser("users/"+userInfo.publicId, true)); + } + + async function validate(){ + const signInWidget = new SignInBasicWidget(page, configuration); + await signInWidget.signInWithBasic(userInfo); + expect(page.url()).toBe(configuration['urls']['home']); + + await page.goto(configuration['urls']['profile']); + + const profileWidget = new ProfileWidget(page); + await profileWidget.waitRuntimeInit(); + + expect(await profileWidget.getUserEmail()).toBe(userInfo.email); + } + + await testRunner.runTest(validate, populateData, mockedData.data); + }); + + + test.skip("user-can-sign-up-with-basic-credentials", async ({page, configuration, cleanUp, mockedData, userService, testRunner}) => { + var userInfo = User.getRandomUser("user1"); + + async function populateData(): Promise{ + cleanUp.push(async () => userService.deleteUser("users/"+userInfo.publicId, true)); + } + + async function validate(){ + await page.goto(configuration['urls']['signup']); + + await page.waitForTimeout(5000); + const signUpWidget = new SignupBasicWidget(page); + await signUpWidget.signUpWithBasic(userInfo); + expect(await signUpWidget.getConfirmationMessageValue()).toBe("Follow the instructions from the email to verify your account.") + } + + await testRunner.runTest(validate, populateData, mockedData.data); + }); +}); \ No newline at end of file diff --git a/tests/mapiClient.ts b/tests/mapiClient.ts new file mode 100644 index 000000000..94629015f --- /dev/null +++ b/tests/mapiClient.ts @@ -0,0 +1,113 @@ +import * as Constants from "../src/constants"; +import { Utils } from "./utils"; +import { HttpClient, HttpRequest, HttpResponse, HttpMethod, HttpHeader } from "@paperbits/common/http"; +import { XmlHttpRequestClient } from "@paperbits/common/http"; +import { KnownHttpHeaders } from "../src/models/knownHttpHeaders"; +import { KnownMimeTypes } from "../src/models/knownMimeTypes"; + +export class MapiClient { + private managementApiUrl: string; + private static _instance: MapiClient; + private token: string; + private constructor( + private readonly httpClient: HttpClient + ) { + + } + + private async initialize(): Promise { + const settings = await Utils.getConfigAsync(); + this.token = settings["accessToken"]; + this.managementApiUrl = settings["managementUrl"]; + } + + public static get Instance() + { + return this._instance || (this._instance = new this(new XmlHttpRequestClient())); + } + + private async requestInternal(httpRequest: HttpRequest): Promise { + await this.initialize(); + if (!httpRequest.url) { + throw new Error("Request URL cannot be empty."); + } + httpRequest.url = this.managementApiUrl + httpRequest.url; + + httpRequest.headers = httpRequest.headers || []; + + if (httpRequest.body && !httpRequest.headers.some(x => x.name === KnownHttpHeaders.ContentType)) { + httpRequest.headers.push({ name: KnownHttpHeaders.ContentType, value: KnownMimeTypes.Json }); + } + + httpRequest.headers.push({ name: KnownHttpHeaders.Authorization, value: `${this.token}` }); + + if (!httpRequest.headers.some(x => x.name === "Accept")) { + httpRequest.headers.push({ name: "Accept", value: "*/*" }); + } + + if (typeof (httpRequest.body) === "object") { + httpRequest.body = JSON.stringify(httpRequest.body); + } + + const call = async () => this.makeRequest(httpRequest); + return await call(); + } + + protected async makeRequest(httpRequest: HttpRequest): Promise { + const url = new URL(httpRequest.url); + if (!url.searchParams.has("api-version")) { + httpRequest.url = Utils.addQueryParameter(httpRequest.url, `api-version=${Constants.managementApiVersion}`); + } + + let response: HttpResponse; + + try { + response = await this.httpClient.send(httpRequest); + } + catch (error) { + throw new Error(`Unable to complete request. Error: ${error.message}`); + } + + return await this.handleResponse(response, httpRequest.url); + } + + private async handleResponse(response: HttpResponse, url: string): Promise { + let contentType = ""; + + if (response.headers) { + const contentTypeHeader = response.headers.find(h => h.name.toLowerCase() === "content-type"); + contentType = contentTypeHeader ? contentTypeHeader.value.toLowerCase() : ""; + } + + const text = response.toText(); + if (response.statusCode >= 200 && response.statusCode < 300) { + if (contentType.includes("json") && text.length > 0) { + return JSON.parse(text) as T; + } + else { + return text; + } + }else{ + throw new Error(`Unable to complete request. Status: ${response.statusCode}. Error: ${text}`); + } + return null; + } + + public async put(url: string, headers?: HttpHeader[], body?: any): Promise { + return await this.requestInternal({ + method: HttpMethod.put, + url: url, + headers: headers, + body: body + }); + } + + public async delete(url: string, headers?: HttpHeader[], body?: any): Promise { + return await this.requestInternal({ + method: HttpMethod.delete, + url: url, + headers: headers, + body: body + }); + } +} \ No newline at end of file diff --git a/tests/mocks/collection/api.ts b/tests/mocks/collection/api.ts index 9789f6039..5890ca17a 100644 --- a/tests/mocks/collection/api.ts +++ b/tests/mocks/collection/api.ts @@ -1,58 +1,50 @@ import { Utils } from "../../utils"; +import { ApiContract } from "../../../src/contracts/api"; +import { Resource } from "./resource"; -export class Api{ +export class Api extends Resource{ public apiId: string; public apiName: string; + public path: string; + public protocols: string[] = ["https"]; + public responseContract: ApiContract; - public constructor(apiId: string, apiName: string){ + public constructor(testId: string ,apiId: string, apiName: string, path: string, protocols?: string[]){ + super(testId); this.apiId = apiId; this.apiName = apiName; + this.path = path; + this.protocols = protocols || this.protocols; + + this.responseContract = this.getResponseContract(); } - - public getApiResponse(){ - let response = {}; - var url = `/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/apis/${this.apiId}`; - response[url] = { - "headers": [ - { - "name": "content-type", - "value": "application/json; charset=utf-8" - } - ], - "statusCode": 200, - "statusText": "OK", - "body": this.getApiBodyResponse() + + private getProperties(): any{ + return { + displayName: this.apiName, + description: "", + subscriptionRequired: true, + path: this.path, + protocols: this.protocols, }; - return response; } - - public getApiBodyResponse(){ + + public getContract(): ApiContract { return { - "id": `/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/apis/${this.apiId}`, - "type": "Microsoft.ApiManagement/service/apis", - "name": this.apiId, - "properties": { - "displayName": this.apiName, - "apiRevision": "1", - "description": null, - "subscriptionRequired": true, - "serviceUrl": "http://echoapi.cloudapp.net/api", - "path": "echo", - "protocols": [ - "https" - ], - "authenticationSettings": null, - "subscriptionKeyParameterNames": null, - "isCurrent": true - } + properties: this.getProperties() }; } - public static getEchoApi(){ - return new Api("echo-api", "Echo api"); + public getResponseContract(): ApiContract{ + return { + id: `/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/apis/${this.apiId}`, + type: "Microsoft.ApiManagement/service/apis", + name: this.apiId, + properties: this.getProperties() + }; } - public static getRandomApi(){ - return new Api(Utils.randomIdentifier(), Utils.randomIdentifier()); + public static getRandomApi(testId: string){ + return new Api(testId, Utils.randomIdentifier(), Utils.randomIdentifier(), Utils.randomIdentifier()); } } diff --git a/tests/mocks/collection/apis.ts b/tests/mocks/collection/apis.ts deleted file mode 100644 index 0251e83c4..000000000 --- a/tests/mocks/collection/apis.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Api } from "./api"; -export class Apis { - public apiList: Api[] - - public constructor(){ - this.apiList = []; - } - - public addApi(api: Api){ - this.apiList.push(api); - } - - public getApisListResponse(){ - let values: Object[] = []; - - this.apiList.forEach(api => { - values.push(api.getApiBodyResponse()); - }); - - return { - "/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/apis":{ - "headers": [ - { - "name": "content-type", - "value": "application/json; charset=utf-8" - } - ], - "statusCode": 200, - "statusText": "OK", - "body": { - value: values, - count: values.length - } - } - }; - } -} diff --git a/tests/mocks/collection/product.ts b/tests/mocks/collection/product.ts index 056abd08e..9aba8517f 100644 --- a/tests/mocks/collection/product.ts +++ b/tests/mocks/collection/product.ts @@ -1,51 +1,52 @@ -export class Product{ +import { ProductContract } from "../../../src/contracts/product"; +import { Utils } from "../../utils"; +import { Resource } from "./resource"; + +export class Product extends Resource{ public productId: string; public productName: string; + public responseContract: ProductContract; - public constructor(productId: string, productName: string){ + public constructor(testId: string, productId: string, productName: string){ + super(testId); this.productId = productId; this.productName = productName; + this.responseContract = this.getResponseContract(); } - - public getProductResponse(){ - let response = {}; - var url = `/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/products/${this.productId}`; - response[url] = { - "headers": [ - { - "name": "content-type", - "value": "application/json; charset=utf-8" - } - ], - "statusCode": 200, - "statusText": "OK", - "body": this.getProductBodyResponse() + + private getProperties(): any{ + return { + displayName: this.productName, + description: "", + approvalRequired: false, + state: "published", + subscriptionRequired: true, + subscriptionsLimit: 2, + terms: "" + } + } + + public getContract(): ProductContract{ + return { + properties: this.getProperties() }; - return response; } - public getProductBodyResponse(){ + public getResponseContract(): ProductContract{ return { - "id": `/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/products/${this.productId}`, - "type": "Microsoft.ApiManagement/service/products", - "name": this.productId, - "properties": { - "displayName": this.productName, - "description": "Subscribers will be able to run 5 calls/minute up to a maximum of 100 calls/week.", - "terms": "", - "subscriptionRequired": true, - "approvalRequired": false, - "subscriptionsLimit": 1, - "state": "published" - } + id: `/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/products/${this.productId}`, + type: "Microsoft.ApiManagement/service/products", + name: this.productId, + properties: this.getProperties() }; } - public static getStartedProduct(){ - return new Product("starter", "Starter"); + public getRequestId(): string{ + return `products/${this.productId}`; } - public static getUnlimitedProduct(){ - return new Product("unlimited", "Unlimited"); + public static getRandomProduct(testId: string){ + var productName = Utils.randomIdentifier(); + return new Product(testId, productName, productName); } } diff --git a/tests/mocks/collection/products.ts b/tests/mocks/collection/products.ts deleted file mode 100644 index 4d286667b..000000000 --- a/tests/mocks/collection/products.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Product } from "./product"; -export class Products { - public productList: Product[] - - public constructor(){ - this.productList = []; - } - - public addProduct(product: Product){ - this.productList.push(product); - } - - public getProductListResponse(){ - let values: Object[] = []; - - this.productList.forEach(product => { - values.push(product.getProductBodyResponse()); - }); - - return { - "/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/products":{ - "headers": [ - { - "name": "content-type", - "value": "application/json; charset=utf-8" - } - ], - "statusCode": 200, - "statusText": "OK", - "body": { - value: values, - count: values.length - } - } - }; - } -} diff --git a/tests/mocks/collection/resource.ts b/tests/mocks/collection/resource.ts new file mode 100644 index 000000000..ae5d0a962 --- /dev/null +++ b/tests/mocks/collection/resource.ts @@ -0,0 +1,7 @@ +export class Resource{ + public testId: string; + + public constructor(testId: string){ + this.testId = testId; + } +} \ No newline at end of file diff --git a/tests/mocks/collection/subscription.ts b/tests/mocks/collection/subscription.ts new file mode 100644 index 000000000..7a49ad792 --- /dev/null +++ b/tests/mocks/collection/subscription.ts @@ -0,0 +1,71 @@ +import { SubscriptionContract } from "../../../src/contracts/subscription"; +import { Utils } from "../../utils"; +import { Resource } from "./resource"; +import { User } from "./user"; +import { Product } from "./product"; + +export class Subscription extends Resource{ + public displayName: string; + public id: string; + + public user: User; + public product: Product; + + public responseContract: any; + + public constructor(testId: string, user: User, product: Product, id: string, displayName: string){ + super(testId); + this.displayName = displayName; + this.id = id; + + this.user = user; + this.product = product; + + this.responseContract = this.getResponseContract(); + } + + public getResponseContract(): any{ + return { + id: `/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/users/${this.user.publicId}/subscriptions/${this.id}`, + type: "Microsoft.ApiManagement/service/users/subscriptions", + name: this.id, + properties: { + ownerId: `/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/users/${this.user.publicId}`, + scope: `/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/products/${this.product.productId}`, + displayName: this.displayName, + state: "active", + createdDate: new Date().toISOString(), + startDate: new Date().toISOString(), + expirationDate: new Date().toISOString(), + endDate: null, + notificationDate: new Date().toISOString(), + stateComment: null, + } + } + } + + public getContract(): SubscriptionContract{ + return { + properties: { + displayName: this.displayName, + createdDate: new Date().toISOString(), + endDate: new Date().toISOString(), + expirationDate: new Date().toISOString(), + notificationDate: new Date().toISOString(), + primaryKey: Utils.randomIdentifier(10), + scope: Utils.randomIdentifier(5), + secondaryKey: Utils.randomIdentifier(10), + startDate: new Date().toISOString(), + state: "active", + stateComment: Utils.randomIdentifier(10), + ownerId: Utils.randomIdentifier(5) + } + }; + } + + public static getRandomSubscription(testId: string, user: User, product: Product){ + var displayName = Utils.randomIdentifier(5); + var id = Utils.randomIdentifier(5); + return new Subscription(testId, user, product, id, displayName); + } +} diff --git a/tests/mocks/collection/user.ts b/tests/mocks/collection/user.ts index 2e791cf65..b0b2b3162 100644 --- a/tests/mocks/collection/user.ts +++ b/tests/mocks/collection/user.ts @@ -1,108 +1,74 @@ -import {Utils} from "../../utils"; -export class UserMockData{ - public email; - public publicId; +import { UserContract } from "../../../src/contracts/user"; +import { Utils } from "../../utils"; +import { Resource } from "./resource"; - public constructor(){ - this.email = "example@example.example"; - this.publicId = "example-example-example"; - } +export class User extends Resource{ + public email: string; + public publicId: string; + public firstName: string; + public lastName: string; + public password: string; + + public accessToken: string; + public responseContract: any; - public getSignInResponse(){ + public constructor(testId: string, email: string, publicId: string, firstName: string, lastName: string, password: string){ + super(testId); + this.email = email; + this.publicId = publicId; + this.firstName = firstName; + this.lastName = lastName; + this.password = password; + this.accessToken = Utils.getSharedAccessToken(this.publicId, "accesskey", 1); + + this.responseContract = this.getResponseContract(); + } + + private getProperties(): any{ return { - "/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/identity":{ - "headers": [ - { - "name": "content-type", - "value": "application/json; charset=utf-9" - }, - { - "name": "ocp-apim-sas-token", - "value": Utils.getSharedAccessToken(this.publicId, "accesskey", 1) - } - ], - "statusCode": 200, - "statusText": "OK", - "body": { - "id": this.publicId - } - } + email: this.email, + firstName: this.firstName, + lastName: this.lastName, + state: "active", + password: this.password, + appType: "developerPortal" }; } - public getUserInfoResponse(){ - let response = {}; - - var url = `/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/users/${this.publicId}`; - response[url] = { - "headers": [ - { - "name": "content-type", - "value": "application/json; charset=utf-8" - } - ], - "statusCode": 200, - "statusText": "OK", - "body": { - "id": `/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/users/${this.publicId}`, - "type": "Microsoft.ApiManagement/service/users", - "name": this.publicId, - "properties": { - "firstName": "name", - "lastName": "surname", - "email": this.email, - "state": "active", - "registrationDate": "2021-11-08T15:45:18.01Z", - "note": null, - "groups": [], - "identities": [ - { - "provider": "Basic", - "id": this.publicId - } - ] - } - } + public getRequestContract(): UserContract{ + return { + properties: this.getProperties() }; - return response; } - public getUserRegisterResponse(email: string, firstName: string, lastName: string){ + public getResponseContract(): any{ return { - "/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/users":{ - "headers": [ - { - "name": "content-type", - "value": "application/json; charset=utf-8" - }, - { - "name": "location", - "value": `/users/${email}?api-version=2021-04-01-preview` - } - ], - "statusCode": 201, - "statusText": "Created", - "body": { - "id": `/users/${email}`, - "firstName": firstName, - "lastName": lastName, - "email": email, - "state": "pending", - "registrationDate": "2022-02-04T13:42:26.36Z", - "note": null, - "groups": [ + id: `/subscriptions/000/resourceGroups/000/providers/Microsoft.ApiManagement/service/sid/users/${this.publicId}`, + type: "Microsoft.ApiManagement/service/users", + name: this.publicId, + properties: { + email: this.email, + firstName: this.firstName, + lastName: this.lastName, + state: "active", + registrationDate : "2021-01-17T19:07:23.67Z", + note: null, + identities: [ { - "id": "/groups/developers", - "name": "Developers", - "description": "Developers is a built-in group. Its membership is managed by the system. Signed-in users fall into this group.", - "builtIn": true, - "type": "system", - "externalId": null + provider: "Basic", + id: this.email } - ], - "identities": [] - } + ] } - } + }; + } + + public static getRandomUser(testId: string){ + var email = `${Utils.randomIdentifier(4, false)}@${Utils.randomIdentifier(4, false)}.${Utils.randomIdentifier(4, false)}`; + var publicId = `${Utils.randomIdentifier(3)}-${Utils.randomIdentifier(3)}-${Utils.randomIdentifier(3)}`; + var firstName = Utils.randomIdentifier(3); + var lastName = Utils.randomIdentifier(3); + var password = Utils.randomIdentifier(10); + return new User(testId, email, publicId, firstName, lastName, password); } } diff --git a/tests/mocks/mockServerData.json b/tests/mocks/mockServerData.json new file mode 100644 index 000000000..03ba11b76 --- /dev/null +++ b/tests/mocks/mockServerData.json @@ -0,0 +1,133 @@ +{ + "products-page-published-products-visible-to-guests": { + "/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/products":{ + "statusCode": 200, + "body": { + "value": [ + "object{{product1.responseContract}}", + "object{{product2.responseContract}}" + ], + "count": 2 + } + } + }, + "user-sign-in-user-can-sign-in-with-basic-credentials": { + "/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/identity":{ + "headers": [ + { + "name": "ocp-apim-sas-token", + "value": "{{user1.accessToken}}" + } + ], + "statusCode": 200, + "body": { + "id": "test-contoso-com" + } + } + }, + "user-sign-in-user-can-visit-his-profile-page": { + "/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/identity":{ + "headers": [ + { + "name": "ocp-apim-sas-token", + "value": "{{user1.accessToken}}" + } + ], + "statusCode": 200, + "body": { + "id": "{{user1.publicId}}" + } + }, + "/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/users/{{user1.publicId}}":{ + "statusCode": 200, + "body": "object{{user1.responseContract}}" + } + }, + "apis-page-published-apis-visible-to-guests": { + "/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/apis":{ + "statusCode": 200, + "body": { + "value": [ + "object{{api1.responseContract}}" + ], + "count": 1 + } + } + }, + "user-resources-user-can-subscribe-to-product-and-see-subscription-key":{ + "/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/identity":{ + "headers": [ + { + "name": "ocp-apim-sas-token", + "value": "{{user1.accessToken}}" + } + ], + "statusCode": 200, + "body": { + "id": "{{user1.publicId}}" + } + }, + "/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/users/{{user1.publicId}}":{ + "statusCode": 200, + "body": "object{{user1.responseContract}}" + }, + "/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/products/{{product1.productId}}":{ + "statusCode": 200, + "body": "object{{product1.responseContract}}" + }, + "/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/products":{ + "statusCode": 200, + "body": { + "value": [ + "object{{product1.responseContract}}" + ], + "count": 1 + } + }, + "/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/products/{{product1.productId}}/apis":{ + "statusCode": 200, + "body": { + "value": [ + + ], + "count": 0 + } + }, + "/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/tenant/settings":{ + "statusCode": 200, + "body": { + + "id": "/settings/public", + "settings": { + "CustomPortalSettings.UserRegistrationTerms": null, + "CustomPortalSettings.UserRegistrationTermsEnabled": "False", + "CustomPortalSettings.UserRegistrationTermsConsentRequired": "False", + "CustomPortalSettings.DelegationEnabled": "False", + "CustomPortalSettings.DelegationUrl": "", + "CustomPortalSettings.DelegatedSubscriptionEnabled": "False" + } + } + }, + "/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/users/{{user1.publicId}}/subscriptions":{ + "statusCode": 200, + "body": { + "value": [ + "object{{subscription1.responseContract}}" + ], + "count": 1 + } + }, + "/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/users/{{user1.publicId}}/subscriptions/{{subscription1.id}}/listSecrets":{ + "statusCode": 200, + "body": { + "primaryKey": "primaryKey", + "secondaryKey": "secondaryKey" + } + }, + "/subscriptions/sid/resourceGroups/rgid/providers/Microsoft.ApiManagement/service/sid/users/{{user1.publicId}}/subscriptions/.*":{ + "methods": ["PUT", "OPTIONS"], + "statusCode": 201, + "body": {} + } + } +} \ No newline at end of file diff --git a/tests/services/IApiService.ts b/tests/services/IApiService.ts new file mode 100644 index 000000000..e7b118a2e --- /dev/null +++ b/tests/services/IApiService.ts @@ -0,0 +1,9 @@ +import { ApiContract } from "../../src/contracts/api"; +export interface IApiService { + + putApi(apiId: string, apiContract: ApiContract): Promise ; + + putApiProduct(productId: string, apiId: string): Promise ; + + deleteApi(apiId: string): Promise ; +} \ No newline at end of file diff --git a/tests/services/IProductService.ts b/tests/services/IProductService.ts new file mode 100644 index 000000000..867e7e181 --- /dev/null +++ b/tests/services/IProductService.ts @@ -0,0 +1,9 @@ +import { ProductContract } from "../../src/contracts/product"; +export interface IProductService { + + putProduct(productId: string, productContract: ProductContract): Promise; + + putProductGroup(productId: string, groupId: string): Promise; + + deleteProduct(productId: string, deleteSubs: boolean): Promise; +} \ No newline at end of file diff --git a/tests/services/ITestRunner.ts b/tests/services/ITestRunner.ts new file mode 100644 index 000000000..ec498c3c9 --- /dev/null +++ b/tests/services/ITestRunner.ts @@ -0,0 +1,3 @@ +export interface ITestRunner { + runTest(...args: any): Promise; +} \ No newline at end of file diff --git a/tests/services/IUserService.ts b/tests/services/IUserService.ts new file mode 100644 index 000000000..5182488fa --- /dev/null +++ b/tests/services/IUserService.ts @@ -0,0 +1,7 @@ +import { UserContract } from "../../src/contracts/user"; +export interface IUserService { + + putUser(userId: string, userContract: UserContract): Promise ; + + deleteUser(userId: string, deleteSubs: boolean): Promise ; +} \ No newline at end of file diff --git a/tests/services/apiService.ts b/tests/services/apiService.ts new file mode 100644 index 000000000..9cf2008c3 --- /dev/null +++ b/tests/services/apiService.ts @@ -0,0 +1,43 @@ +import { ApiContract } from "../../src/contracts/api"; +import { MapiClient } from "../mapiClient"; +import { IApiService } from "./IApiService"; + +export class ApiService implements IApiService { + private readonly mapiClient: MapiClient + constructor() { + this.mapiClient = MapiClient.Instance; + } + + public async putApi(apiId: string, apiContract: ApiContract): Promise { + if (!apiId) { + throw new Error(`Parameter "apiId" not specified.`); + } + + const contract = await this.mapiClient.put(apiId, null, apiContract); + return contract; + } + + public async putApiProduct(productId: string, apiId: string): Promise { + if (!productId) { + throw new Error(`Parameter "productId" not specified.`); + } + + if (!apiId) { + throw new Error(`Parameter "groupId" not specified.`); + } + + var result = await this.mapiClient.put(productId + "/" + apiId, undefined); + return result; + } + + public async deleteApi(apiId: string): Promise { + + if (!apiId) { + throw new Error(`Parameter "productId" not specified.`); + } + + var result = await this.mapiClient.delete(apiId, undefined); + return result; + + } +} \ No newline at end of file diff --git a/tests/services/productService.ts b/tests/services/productService.ts new file mode 100644 index 000000000..a3fb1b64e --- /dev/null +++ b/tests/services/productService.ts @@ -0,0 +1,45 @@ +import { MapiClient } from "../mapiClient"; +import { ProductContract } from "../../src/contracts/product"; +import { IProductService } from "./IProductService"; + +export class ProductService implements IProductService { + private readonly mapiClient: MapiClient + constructor() { + this.mapiClient = MapiClient.Instance; + } + + public async putProduct(productId: string, productContract: ProductContract): Promise { + if (!productId) { + throw new Error(`Parameter "productId" not specified.`); + } + + const contract = await this.mapiClient.put(productId, undefined, productContract); + return contract; + } + + public async putProductGroup(productId: string, groupId: string): Promise { + if (!productId) { + throw new Error(`Parameter "productId" not specified.`); + } + + if (!groupId) { + throw new Error(`Parameter "groupId" not specified.`); + } + + var result = await this.mapiClient.put(productId + "/" + groupId, undefined); + return result; + } + + public async deleteProduct(productId: string, deleteSubs: boolean): Promise { + if (!productId) { + throw new Error(`Parameter "productId" not specified.`); + } + + if (deleteSubs){ + productId = productId + "?deleteSubscriptions=True"; + } + + var result = await this.mapiClient.delete(productId, undefined); + return result; + } +} \ No newline at end of file diff --git a/tests/services/testRunner.ts b/tests/services/testRunner.ts new file mode 100644 index 000000000..7dc8529e0 --- /dev/null +++ b/tests/services/testRunner.ts @@ -0,0 +1,16 @@ +import { ITestRunner } from "./ITestRunner"; +export class TestRunner implements ITestRunner { + public runTest(validate: () => Promise, populateData: () => Promise, testName: string ): Promise { + return new Promise((resolve, reject) => { + populateData().then(() => { + validate().then(() => { + resolve(); + }).catch((err) => { + reject(err); + }); + }).catch((err) => { + reject(err); + }); + }); + } +} \ No newline at end of file diff --git a/tests/services/testRunnerMock.ts b/tests/services/testRunnerMock.ts new file mode 100644 index 000000000..ef31ee85b --- /dev/null +++ b/tests/services/testRunnerMock.ts @@ -0,0 +1,38 @@ +import { Utils } from "../utils"; +import { ITestRunner } from "./ITestRunner"; + +export class TestRunnerMock implements ITestRunner { + public runTest(validate: () => Promise, populateData: () => Promise, data: object ): Promise { + return new Promise(async (resolve, reject) => { + + let server = Utils.createMockServer(data); + let error = undefined; + server.on("ready", () => { + validate().then(() => { + console.log("validation done"); + resolve(); + }).catch((err) => { + error = err; + reject(err); + console.log("server error"); + }).finally(() => { + server.close(); + console.log("server closed"); + }); + }); + + server.on("close", () => { + if (error != undefined){ + reject(error); + } else { + resolve(); + } + }); + + server.listen(8181,"127.0.0.1", function(){ + console.log("server started"); + server.emit("ready"); + }); + }); + } +} \ No newline at end of file diff --git a/tests/services/userService.ts b/tests/services/userService.ts new file mode 100644 index 000000000..466b5703f --- /dev/null +++ b/tests/services/userService.ts @@ -0,0 +1,30 @@ +import { MapiClient } from "../mapiClient"; +import { UserContract } from "../../src/contracts/user"; +import { IUserService } from "./IUserService"; + +export class UserService implements IUserService { + private readonly mapiClient: MapiClient + constructor() { + this.mapiClient = MapiClient.Instance; + } + + public async putUser(userId: string, userContract: UserContract): Promise { + if (!userId) { + throw new Error(`Parameter "userId" not specified.`); + } + + const contract = await this.mapiClient.put(userId, undefined, userContract); + return contract; + } + + public async deleteUser(userId: string, deleteSubs: boolean): Promise { + if (!userId) { + throw new Error(`Parameter "productId" not specified.`); + } + if (deleteSubs === true) { + userId = `${userId}?deleteSubscriptions=true`; + } + var result = await this.mapiClient.delete(userId, undefined); + return result; + } +} \ No newline at end of file diff --git a/tests/templating.ts b/tests/templating.ts new file mode 100644 index 000000000..92d78c85b --- /dev/null +++ b/tests/templating.ts @@ -0,0 +1,20 @@ +import { Resource } from "./mocks/collection/resource"; + +export class Templating { + public static updateTemplate(templateData: string, ...resources: Resource[]): object{ + for(let i = 0; i < resources.length; i++){ + let objectKeys = Object.keys(resources[i]); + + let testId = resources[i].testId; + + for (const key of objectKeys) { + let regex = new RegExp(`"object{{${testId}.${key}}}"`, "g"); + templateData = templateData.replace(regex, JSON.stringify(resources[i][key])); + + let regexString = new RegExp(`{{${testId}.${key}}}`, "g"); + templateData = templateData.replace(regexString, resources[i][key]); + } + } + return JSON.parse(templateData); + } +} \ No newline at end of file diff --git a/tests/tsconfig.json b/tests/tsconfig.json new file mode 100644 index 000000000..2b95584ce --- /dev/null +++ b/tests/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "es6", + "lib": [ + "dom", + "es2019" + ], + "module": "commonjs", + "moduleResolution": "node", + "noStrictGenericChecks": true, + "ignoreDeprecations": "5.0", + "removeComments": true, + "noLib": false, + "skipLibCheck": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "allowUnreachableCode": true, + "sourceMap": true, + "incremental": false, + "resolveJsonModule": true, + "noEmit": false + }, + "include": [ + "../node_modules/@paperbits/common/http/*", + ] +} diff --git a/tests/utils.ts b/tests/utils.ts index c0369819f..41188d831 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -1,10 +1,9 @@ import * as fs from "fs"; import * as crypto from "crypto"; import * as http from "http"; -import { ConsoleMessage, Page } from 'puppeteer'; export class Utils { - public static async getConfig(): Promise { + public static async getConfigAsync(): Promise { const configFile = await fs.promises.readFile("./src/config.validate.json", { encoding: "utf-8" }); const validationConfig = JSON.parse(configFile); Object.keys(validationConfig.urls).forEach(key => { @@ -14,6 +13,28 @@ export class Utils { return validationConfig; } + public static getTestData(testKey: string): object { + const configFile = fs.readFileSync("./tests/mocks/mockServerData.json", { encoding: "utf-8" }); + const validationConfig = JSON.parse(configFile); + if(validationConfig[testKey] == undefined){ + throw new Error(`Test data not found for ${testKey}`); + } + return validationConfig[testKey]; + } + + public static async IsLocalEnv(): Promise { + let config = await Utils.getConfigAsync(); + return config["isLocalRun"] === true; + } + + public static addQueryParameter(uri: string, name: string, value?: string): string { + uri += `${uri.indexOf("?") >= 0 ? "&" : "?"}${name}`; + if (value) { + uri += `=${value}`; + } + return uri; + } + public static getSharedAccessToken(apimUid: string, apimAccessKey: string, validDays: number): string { const expiryDate = new Date(); expiryDate.setDate(expiryDate.getDate() + validDays); @@ -26,9 +47,13 @@ export class Utils { return sasToken; } - public static randomIdentifier(length: number = 8): string { + public static randomIdentifier(length: number = 8, includeNumers = true): string { let result = ""; - const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + let characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + if (includeNumers){ + characters = characters + "0123456789"; + } + const charactersLength = characters.length; for (let i = 0; i < length; i++) { @@ -38,12 +63,18 @@ export class Utils { return result; } - public static createMockServer(responses?: Object[]) { - var obj = {}; - if (responses?.length){ - for (let responseObj of responses) { - obj = {...obj, ...responseObj }; + public static createMockServer(responses?: Object) { + var obj = {...responses} ?? {}; + for (const key in obj) { + var newKey = key; + + if (obj[key]["methods"] && obj[key]["methods"].length > 0){ + const methods = `(${obj[key]["methods"].join("|")})`; + newKey = `${methods}/${key}`; + }else{ + newKey = `(GET|POST|PUT|DELETE|OPTIONS)/${key}`; } + obj[key]['regex'] = new RegExp("^" + newKey + "$"); } var server = http @@ -54,13 +85,27 @@ export class Utils { res.setHeader("Access-Control-Allow-Headers", "*"); res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Expose-Headers", "*"); - if (urlWithoutParameters && obj[urlWithoutParameters] != undefined) { - var response = obj[urlWithoutParameters]; - var headers = {}; - response.headers.forEach(element => { - res.setHeader(element.name, element.value); - headers[element.name] = element.value; - }); + + var urlToSearch = `${req.method}/${urlWithoutParameters}`; + var response = null; + + for (const key in obj) { + if (obj[key]['regex'].test(urlToSearch)) { + response = {...obj[key]}; + delete response['regex']; + break; + } + } + + if (response != null && response != undefined) { + // default header response, the specified header + res.setHeader("Content-Type", "application/json"); + + if (response.headers && response.headers.length > 0){ + response.headers.forEach(element => { + res.setHeader(element.name, element.value); + }); + } res.writeHead(response.statusCode); res.write(Buffer.from(JSON.stringify(response.body))); @@ -79,33 +124,4 @@ export class Utils { server.close(); } } - - public static startTest(server, validate): Promise{ - return new Promise((resolve, reject) => { - server.on("ready", () => { - validate().then(() => { - resolve(); - }).catch((err) => { - reject(err); - }); - }); - - server.listen(8181,"127.0.0.1", function(){ - server.emit("ready"); - }); - }); - } - - public static async getBrowserNewPage(browser): Promise{ - const page = await browser.newPage(); - - page.on('console', async (message: ConsoleMessage) => { - if (message.type() === 'error') { - console.error(message.text()); - } - }); - - return page; - } - } diff --git a/tsconfig.json b/tsconfig.json index 3f7a7e655..caae9103f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,7 @@ "module": "commonjs", "moduleResolution": "node", "noStrictGenericChecks": true, + "ignoreDeprecations": "5.0", "removeComments": true, "noLib": false, "skipLibCheck": true, From f872fa9ef33e96ae807b5e18a5a535d250323afb Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Mon, 24 Jul 2023 11:46:25 +0200 Subject: [PATCH 02/15] fix path of running tests --- .github/workflows/test.yml | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 23ea58c71..c363cdb64 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -49,4 +49,4 @@ jobs: shell: pwsh - name: Run tests - run: npx playwright test /tests --workers 1 \ No newline at end of file + run: npx playwright test tests/ --workers 1 \ No newline at end of file diff --git a/package.json b/package.json index 3c458c73e..c8fa80ffb 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "test": "node node_modules/mocha/bin/_mocha -r mocha.js src/**/*.spec.ts", "deploy-function": "npm run build-function && cd dist/function && func azure functionapp publish < function app name >", "publish": "webpack --config webpack.publisher.js && node dist/publisher/index.js && npm run serve-website", - "serve-website": "webpack serve --open --static ./dist/website --no-stats", + "serve-website": "webpack serve --open --static ./dist/website --no-stats --port 8080", "build-mock-static-data": "webpack --config webpack.mockStaticData.js && node dist/publisher/index.js", "build-static-data": "webpack --config webpack.staticData.js && node dist/publisher/index.js", "serve-static-website": "npm run build-static-data && npm run serve-website", From 6d6734156ddd98172783b9d1746c9125782ce238 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Mon, 24 Jul 2023 11:53:20 +0200 Subject: [PATCH 03/15] remove test value --- tests/mocks/collection/user.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/mocks/collection/user.ts b/tests/mocks/collection/user.ts index b0b2b3162..dbba5fd3c 100644 --- a/tests/mocks/collection/user.ts +++ b/tests/mocks/collection/user.ts @@ -19,7 +19,7 @@ export class User extends Resource{ this.firstName = firstName; this.lastName = lastName; this.password = password; - this.accessToken = Utils.getSharedAccessToken(this.publicId, "accesskey", 1); + this.accessToken = Utils.getSharedAccessToken(this.publicId, Utils.randomIdentifier(), 1); this.responseContract = this.getResponseContract(); } From d979f8ff88b8f27ecce7d45c8b78e1b92776c442 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Mon, 24 Jul 2023 12:18:00 +0200 Subject: [PATCH 04/15] Use ubuntu --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c363cdb64..db941a80a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,7 @@ jobs: run: npm run test end2end-tests: - runs-on: windows-latest + runs-on: ubuntu-latest steps: - name: Checkout code From 285544a3ff06b6ac5b5918e72126e1c878e94bfb Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Mon, 24 Jul 2023 12:19:44 +0200 Subject: [PATCH 05/15] Fix path for compiler --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index db941a80a..4c9f9e052 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,7 +33,7 @@ jobs: run: npm install - name: Compile - run: npx tsc -p tests\tsconfig.json + run: npx tsc -p tests/tsconfig.json - name: Install Playwright Browsers run: npx playwright install --with-deps From ffb9b651d1b7f52112698a4cac1cf80c652a02a0 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Mon, 24 Jul 2023 12:42:02 +0200 Subject: [PATCH 06/15] share the test results --- .github/workflows/test.yml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4c9f9e052..6c03c4d80 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,7 @@ jobs: run: npm run test end2end-tests: - runs-on: ubuntu-latest + runs-on: windows-latest steps: - name: Checkout code @@ -33,7 +33,7 @@ jobs: run: npm install - name: Compile - run: npx tsc -p tests/tsconfig.json + run: npx tsc -p tests\tsconfig.json - name: Install Playwright Browsers run: npx playwright install --with-deps @@ -43,10 +43,18 @@ jobs: - name: Start mock server run: npm run serve-website & + shell: bash - name: Wait for the server run: ./.github/scripts/wait-for-server.ps1 -HostName "http://localhost:8080" shell: pwsh - name: Run tests - run: npx playwright test tests/ --workers 1 \ No newline at end of file + run: npx playwright test tests/ --workers 1 + + - uses: actions/upload-artifact@v3 + if: failure() + with: + name: playwright-report + path: test-results/ + retention-days: 30 \ No newline at end of file From defa6e805ed686092e61fedba67cddd90c0e9070 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Mon, 24 Jul 2023 13:00:47 +0200 Subject: [PATCH 07/15] fix blocks filtration --- tests/e2e/maps/apis.ts | 2 +- tests/e2e/maps/products.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/maps/apis.ts b/tests/e2e/maps/apis.ts index 1ccd42578..0692412bb 100644 --- a/tests/e2e/maps/apis.ts +++ b/tests/e2e/maps/apis.ts @@ -4,7 +4,7 @@ export class ApisWidget { constructor(private readonly page: Page) { } public async waitRuntimeInit(): Promise { - await this.page.waitForSelector("api-list.block"); + await this.page.waitForSelector("api-list"); await this.page.waitForSelector("api-list div.table div.table-body div.table-row"); } diff --git a/tests/e2e/maps/products.ts b/tests/e2e/maps/products.ts index e96e7df98..717e12cfb 100644 --- a/tests/e2e/maps/products.ts +++ b/tests/e2e/maps/products.ts @@ -4,7 +4,7 @@ export class ProductseWidget { constructor(private readonly page: Page) { } public async waitRuntimeInit(): Promise { - await this.page.waitForSelector("product-list-runtime.block"); + await this.page.waitForSelector("product-list-runtime"); await this.page.waitForSelector("product-list-runtime div.table div.table-body div.table-row"); } From 0931d12605523970f1eaea12fc025f2976674e53 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Mon, 24 Jul 2023 13:30:40 +0200 Subject: [PATCH 08/15] rewrite the test image --- playwright.config.ts | 2 +- tests/e2e/runtime/user-subscriptions.spec.ts | 2 +- .../self-hosted/user-resources-win32.jpeg | Bin 0 -> 41066 bytes .../user-resources-win32.jpeg | Bin 40989 -> 0 bytes 4 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 tests/e2e/runtime/user-subscriptions.spec.ts-snapshots/self-hosted/user-resources-win32.jpeg delete mode 100644 tests/e2e/runtime/user-subscriptions.spec.ts-snapshots/user-resources-win32.jpeg diff --git a/playwright.config.ts b/playwright.config.ts index 2113ff4d1..9ba55ba08 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -2,7 +2,7 @@ import { defineConfig } from '@playwright/test'; export default defineConfig({ testIgnore: 'playwright/*', - //timeout: 30_000, + //timeout: 50_000, use: { video: 'retain-on-failure' } diff --git a/tests/e2e/runtime/user-subscriptions.spec.ts b/tests/e2e/runtime/user-subscriptions.spec.ts index 9a1b00dae..4d98122b2 100644 --- a/tests/e2e/runtime/user-subscriptions.spec.ts +++ b/tests/e2e/runtime/user-subscriptions.spec.ts @@ -54,7 +54,7 @@ test.describe("user-resources", async () => { expect(subscriptionSecondaryKeyHidden).not.toBe(subscriptionSecondaryKeyShown); // check profile page screenshot with mocked data for profile page - expect(await page.screenshot({ type: "jpeg", fullPage: true, mask: await profileWidget.getListOfLocatorsToHide(), maskColor: '#ffffff'})).toMatchSnapshot({name: 'user-resources.jpeg', maxDiffPixels: 20}); + expect(await page.screenshot({ type: "jpeg", fullPage: true, mask: await profileWidget.getListOfLocatorsToHide(), maskColor: '#ffffff'})).toMatchSnapshot({name: ['self-hosted', 'user-resources.jpeg'], maxDiffPixels: 20}); } await testRunner.runTest(validate, populateData, mockedData.data); diff --git a/tests/e2e/runtime/user-subscriptions.spec.ts-snapshots/self-hosted/user-resources-win32.jpeg b/tests/e2e/runtime/user-subscriptions.spec.ts-snapshots/self-hosted/user-resources-win32.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..30a197ed00a0a980afa7692c05b30df2b8719bdf GIT binary patch literal 41066 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 literal 0 HcmV?d00001 diff --git a/tests/e2e/runtime/user-subscriptions.spec.ts-snapshots/user-resources-win32.jpeg b/tests/e2e/runtime/user-subscriptions.spec.ts-snapshots/user-resources-win32.jpeg deleted file mode 100644 index 2af066a4c1b6090ac60fd54d27b8c81c9358981d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40989 zcmeFZcT`(RlQ=5pY!AkO4H(8mn`poSlN}J4Yylz&BnCtGC$*E+hlZg=0R>gu{3s;m2`?`RD0 zi;kwYCg9jH0N@zw2RNEI)~EgWvGr3ULrrZx&_5Cy0jzNHE&$-_=8ZDadi1Ng1@zZ5 zWB+*L&omo^m&c#?|01yPGJF2S4ghpX{ug=vyXbRv_Ff29h85Od2*o1KV)hyfzUKH3 z`1YT$?LT0ZKVd&_4{uhUr+>mI6Qjp0*p3C?cKi>p?SFs~9;iR_zh~u9adq+elh&W% zPmV9zyFrXu_p_`&FTe|61keIJ`m_A3XI5}e2LSGG0stpI{1s=L005M}0RXNK{1qqk zI{O6`&1p10VoWEJzk`7a#+WKN5>h!6Tr%#_cedf&Rv*&*~ zfBxLL^Vcq4x%dm$wd>cpu5og5-xTKO<`LxOr_MO{* zA~|;E%$f6N&$FLD&wiVmll%7n>u~f9z;^b;@23DKjtKycvmHCZcI>E@Re)oxh(E*s zQ(6Fv*)u1OA3MuR1^)s#cJlbK<0np?I(h0Gs}QFEtcR1Q*iK))#ePlZjKD*%3B(4M z(aj<2`QohLBO@P9IiNfW`?;uC$kv-&D11-hzOmP1#LFLDzOmms##tz@{~NKtsIkcY ziISB9WMh2`$B&;qef;?GV<%7i$!xX7lqM1dwSd>sOBeCx=u z#Q)ZQO|M}xKux5r|7HiTL?O*SRAEl-6m2nl^@23-_iUG@tq)+yC=fkoqY%oiaDwZ~_uI7bIOc zQX<_1jVwO2kLRU-HPE$S-aj;r(-e_Po$mr3Tv$VidLRB~`6|pq-C4r=nbi^CMq9vT z*(eEDdO;MXW%AXKRx_rPgloxkU=of1>;=Z{6K>j|r^O;q;E%%QA*X`WsiV^o2>|0lB z+Yo8{?6lAiT)*W{?!RJd%p+D@Hb~Vh|A#E1tYa2BLZ&3X{qNI?!%*hj1nLOjqQc)l zm6sKH!3om{!zhRcdAo<@(yYhXV~+r|>X$c--W65xge7KGgx;v_A!`RcaeP~3DF|Wj z&0|Ma*O}Drd5l}^CBMR~W7w|wdTk4vmS@n|TYumQoe|{>hAOalK+$KIJ!Bs}kR#Y) z{|vE+mZ5)y-D{0#i@a$1{R9Nedq_e02#EBKR~~zJ7(L|0vn1OqJd@m+d;=XPdxZeM z;Hqs~zys}dx?Wky*R7vp=RT5CAXMAUv^e0-C^j5MitOcbz+1kKuz%P?F$sHUw#P!a zrFydPR~|7LNT>S~I?P@BaI$lnGy3@9#~`Sb4D8eHjb_zNj;gREXD;nk z!-_7nz~qjLHRTB4RL1*OtqrNLSU+FXSf{BMHYZQCj>d;pcpa+8Q4enUpN3~Ewf2?! zOu0tt+2(tSWhQD#*U;2QPOJCxU;T!PcIN8|ZI%G3#K$)u0gR^+S{pvv@|)GEu?Mx$ zF8A@Jm7hJX*njOyikg(?*dm$|4lM1Eipz6Lhc!olq|%kg%lD@EnuGJbE~dB%;^^j5 zAaPe&D+H%j{XyA)X}T1(vT4-%)FXH0A1IQeaTNN|x8Fo-*YbrJJt7Mo{I=Dm9SVaw zdDT8X{q2S(`fsW!47+4daFYE@&G_w!!t3SHWu~+zF?ZNHfYv#R%G77WexsxV14uJz zMlrhRr~N)vGJ?|R@pY?tsPc*Lj81;+XCag*X`qfVM}aLkjK+JZh5p=L!brYiqCeGG zHp;aOQn5oGk9nx-SABjeabd?&8`_GY;xCr+6E;lUlxB4W9gr`jqA5h^!Zdm@19sK2(py9 z(U}4En}6=V)(TR$PaWCTZ@+W(X#>iF^Uc67%Y9xt%gbPQ;5j; zKzzhBh;r^hKZHA9w2L_c6w+YS9X`LZ%Ut|!N~LSt^Cg*B0XsPh1m6r^AOno2LORnI z7>{`p5%bhflS`})t|$>j6n;yX!e|nGW%9jqBl^nWy&=04FXwf$=EC{SF zKyHYe6YY$|Kd%hn!!u2`aO}&lAe~Xiz7mByQNe`$&%LX~an&Ci5w->JlPI~os5qrX zmqb$qm^8|_pMXeNzp1#l9#O#6o|{^cA_35R4mh6LE0AFsYyAC5VZb0*$y7|hTfwr+ zin=%>XlJ7D_hfjD;ekrpma_QZVEDnLNax&5&r-tEq?-Z*v!qIy+L-O!s8w#vfCMsc zQt{MkgK20rsN}k{vusdPv8)C6+^be7ktIGVrSlBvsyN#bpu8{>(*@xOG_P(gdi7y) zs?7HaR}Ci1+)djSq**uK)Z1C9Q++ZD;#cqjm)Ef97;r|A>~M&DD>nZPlifJ^CeEDF zhne@vSOo_|E3ddwNG(MS@o-XEh`}I@Q5R_ye7a6p=_hV>mY!f-;|!hhO02w)>0HI5 ze@B3sG^2dg^HY({yHJAAX?~R z0uQt;!Z7FF;+z|8Rh114_Dz~YN>K&4<%}Kxe&yp@lEv}JPiaYbXwF$hZJHp(NL7ao zWw6G{pds`fUOg{*um!Y-B2R+Y&atV9A?ol>>3KP@#B>2<7|wv_lU4|lJX%X%S#T3h zEGqC9Yf6lohbi6Ri!yN!R@Tk5j)@CzyxYRj#4Z_aK1U%HGGyBxVDu?)>K53z+`YTa zX1kcOy@^)j#TefS;YDW@+$^abl_G_+wK)q!g%JiVQHo8&3d4RkWKfgap=Z5r|D5u4 ze~G3DylIbx!|@8nb(sWhDZ36jTYhL?`vF07_D3HN0 zE-aNm@vX*~PC0YM3nviYQ}@L5QpBCXn<>0qB|?T*txq*ZkL+?~fqAR0*uk>*QNWxQ z=u`l&o7mp3MVIBD?L(tq>R!0`(`%z4T{(|CEYSqCm{OcqF?ucux%)H25~1w1JU3#? zFE_RSY`;M(eB4LW%wUsN0CO*CojMGz%j@x8NUhhax4>lV^LQ%PV3#%1l}DCH^VAIu z$o7qj(bOp}x%L`xwiB;h-|JM zvLBGshix}acp)$jicLI_@Ck1UVL+{bihigmpHRaGHLWI_3^3c-U@+#LzFNbnOvmyRXSoa{ zNYG-%jA=S&(d`ZRoz)6oJamsyzxgSW$N1dG=Dr|4n-P85Kwm$ti?V9(u(!`sL{l_n z`>NEBP7o`;-3O1;_BH*^SOS|@A`?+!V$RcVxI}(Tty$G|PRRDIUUs`<-tx=CYuGA7 zHVF3-;HrYJAq)sD-%e<`Mtrb%iJ2q#y`w-A8B=-_RO;~dM>8!@vEZdyWq}Dqn|oLa z3-Qi%2u}#B+Sy6%;6oX%B`;#u7w0TgrbRi98^$(+bIuplu1c8Oxy}uUnhxbA4y3y? z9?vtKxuWY81Ve>&NUDtyaoH*F!@(s?4rpbY$kw7bwmb*^MZ9;X7OMb(; zlsdMpnnPDo6YX@;_d2#Os!1G#hS~JTQ%qCtw z!KqZZy@bD0t?nP}C9|W*Eo$?2uEJ~Sf=@iX~ga4@_oD&+c@JNdr`yd)}ckb#;$3!0)WjC;)Eg?vD>-fHm7yufU41C)lU_|I z58g8p-4VyW>svy8`OUTH*NV>~L3-Kr`0u(Tlq#4#`0U7RRpMTxI?=oSYuZ7&n#f}7 zVXSgUw#%abW$Vj^lFfLFw@OccbCstqYAbm+MzY(PkviDIQlOjG`iqsdQ=a{aPVQ_W zS-W_AcVd1UE+TI}MIZ9`m6w`mqipd(hAc*Ht;Fg;IV$Q)_Ocn~{_egIR!OKcdxHux zoeR+%?Hl3aF7?_mkH=<~_FPMl-R_$eOs+)<6w1qlS4!cARrmaiz?;tIUG<(98 zfSpuKGJs{b5=$4rld!6K%UmBr`b^u*uEaXXn)jH?%5F=(b#Q)`n<^R>mN!d6=wIl~ z!k4pV725MT_v;EYjl}j1co*-N3N;1dcD-ugocV61`QmbHe!ZPxjWmlaKfBI52?@o6 zUSKv!y9&Z3Lk>+QLKIWi;KtcnOF@aGr>9Fh`gl_<4VEB=6&t0^cdsqT)>TYPoo6`n z_1l;7K&Pvp)qxpB9_r|-FKSx-{vn)kHv~>}N_k{;>OR3dQ5a;hDrzQ&OsgclK?Ux*%$XqM`{jCz zt|1KK&(655FAml-n|Q2w5}Ar@ZC+;2a;vWyomNl|Z&vG)NzmcC2^p5ZTKV?Lmx88U z2uru;9c29TUyn4fvs=WNG^tEktH%K6k9Lo%*Ah`4g~x@`jiC&d0INALgNa7F9~J5C8SmP-yr zaD8i1iZ9|Ndc|7y`XJB?!Ql`78O~$+v6JRQxCZE#LC^N-3?U!qA~S8UD%%{-Ky+_T zWmR+DY!(r^(oN(DB+FhGK+4|l>vlHp))x>EO&8#Mj{Nq|kp3&~UlRD21pZ42$PEd` z{V{#Ng^P@ZMqHPuBE-}{d&Luoc<(Z_A9ww0{%Dj82DK~T(!L^P{W$5jVO-r~B=?-$ z*j!-`d`dmIe>@HXd>pq|^^8 zJ&{vcSM`ea;7xTaW)mKuR5;YA4b}R&wPb;;tzUjte@CBg^Hh;H>O$B@7LSt!5g^Ts z)LPV^-DOENkh8ajxW^;lH-jJgPi+33566Np>mTr_cej>q*QK#6c|9id(2hdWoa;Zk z{iwE84nLmMZMt<-9|5wC0H}!2k2^lE2_^v>@fPGmTSKPV5kL?dswtWo(lUb`EgxJj z3yrzApfF{3%|%}hi`C*uosmu#5TO~E9bC8d3XTUd+!BEOBW)}~RE;A*+coeZbC_Qk znzuf8+sg6I(uo00)ty!m^v(L!D+#N#7sZ1PW)uRkv0y;p2yiL%O4_5b%`$s)NlaFK3AvM#kKm0B3SU6;mJ#x zrsoCUGzD5IX#t_mgG@%^Vbp6?{gZZ89S8C9ni)xZI-t(|4qFqYojX5Lq~tn~y=*o4 z?*wO52C);WMB~Z6+ZpBYt6>y;bCiqAmqO*;e!lSD&K@8H%vDtt-2T;bzm}|ZAnM_Y z1K$+kSd0!Gna75%frJnyT4}Yh^4X3p(--&oXxmdair)X6SO@na+teg)XX?*t48|x3 z_$X^vLqLb(eZ!t4<1@F%u&9ej32mTb^=EXx5s-tIwlmjQU0dC#&3jLxNjb64f<1ZD zFCEC{R}_5&(DH2@wcX;~Fo{}+dRrbYQwDd^6Y%aJ&vGd;Cq7|R{=ry9b?V;3iH{SL zJxF6RjO3;zhdNzgQmNa;3(g3HfGfoaM8BO~M!*?i%Hm#si!5(y7pmVME1;!O8`-8e z1&>P?5D;+g)t-&W(%Nv2de3|uZVF_x7FtH-)CHNb#*;%~W}#0G3gfSk6_8J7Zh6n= z^|M#|>=j4&tP0Sc)%Rn(iiX?6Yjk`y)(%rQ$*H}vxEnF?F*d85LdYkrmd`S=th#d^ z$t8hy=1eM>&TY~t(W#ZP`7eLax8H61+D5yD&sI|f(TuDR%N;+)V36m~@yyXqcWhP8 z&Q~&9VRKGS_ih6c9ltS5H0NCw5=|(W zD&(j>e3lpbyx_1jHZ-&02ym|82r%*M5#ZD7@B^pyBS1`7l#k>v#kJ zmFFE7!`tST4IIwpTM=K|<$s9JbwQ)Z zLf9jKeWzk$`RMS<_N~uM`_?tZ_o>zxRO03WO7C%OJ#@Z`BdoVmGkwd@WOUU~e@<>+ z>-~C*mDv?Fg#kT}&X{C_w<2Q;WrDWkd&BsxPT$cXD~|%l02gVM(q^xUgtDX(WPn-v zQ`g|^!K^J9sj=4Zael3_NmmiS+C_~vlR8V75Wvgj?`+loqOIsB$n=aOG%c+eI+oSj z6e^&H2r4k2eC567L^y79TiOTa>|B+8dN4_XuCQ%Z47WHz-mm^>J7+`J&>pM#F%`75 zO|fF8mxi{fO)2^Euo<6wO^mFq$KiV-f4s>X&0$z4GRpV6zZ`eGz~eS8^?Av+JpZo4 zecac|t94-_80*Q-+PQwu#r4-e8ziStJNb!I0jD8tK7>2mn)Q^tY;u&LhGE)JN~~rd zr%02GpY%_(N*iKFbp*7y4!kV2G&NtQITZXSzb%;% z52JyseS#sE{y5HzWPd-ZSbN{B_#%7RnT;3qkb=MEZWB#3vk#T5w{j4lC%zu&NwY_v3bO@4xtCu`Wo<%0 zTglZx2n4d%^1CRvLkitSM)?z~8%VZgTpshjo1p02f~01esZo=srHzN^on>snTy!hc zq2goQ>Vkhz5$}8nPnIbuxj|rwm$oe?jT+-D>#ElYg&7!{VbCVEGzV#}rWI52YH3Zx z)Bs~$&oSLs!?etp0U53BMk|i3c+8gy$I;6q+8-HmZ|=kzBn)qarEGK$Fh}b`uRGS$to>i6*svhQv=2bJ_OD7d$~UU^93V?m@c^8 z@;xkceJN)q&hW}E2~(BgEdPgraLl$d7k@T_#VHSpG`SFta;S1X1a9ad~$>0 z8SaT4l{%c^<;=@iw$NgRX6X(-Jyh=Fp8A-|bOFEpK*bDGZ^umPQJZ~DjdF|y8;9=} z6XTZdL}upQ-;GCba+B`(;>ku=G}!(V|<<6w>W++zEqocH+$)EpzG3olhA_#XkB zA;lkr&Phe9NdxN&neyP+GI+;mr>%>(hweZq41T9tPm z)Q4aY18<=fHSXP9aWB0Aqi? zjd8!4VO6eGRw!CAj2GBQQ)zzo<#50sgSaK26-6-!LW}yA-;gaZ2Ahp{M`j+LiZ9hm z(Sf(gZswq24*PJ^{By(WPc}6)kb(G|0(Wdut8tcdySh?kAjO%MQFMi&99z~aPI*PG zootXT{$pAe+>Kikpn}Ud0u!=WR_VYcwFyQ%eY(a|uik)w2-=v}RdK~~KuOLJFb5mh zXK>m}-RT$QGq0F7BHE3Ot7S@_130tcXz&Iezt8S8?|krODsSQ-VP>jS#YKCr>Evw^ zLvV!V46atU-tv=ni?f>1;5Y-0N?N zZPgMGHQi)LQI7x>>#+(;{GtXbB_}R4EKCYT*GFV)2AI&0Wo@a3aEvirS9 z09;_`cz5V#y>I!GaLsZ7r>pXF?Atu60$s2YgNt42L={94hi*JwQK4{VjZYCyba`1P zP|ia_6>`Zy_)_OxTb4#9yZB0m_ZbPjXzdJE6T?re_wjbLEyhOs8#?a_pl^0k=TJ|U(hum_p&wVWgC}$- zwV`X#PVXK-G}BoNDa!b~7SEyTM6Yu$u(7_yYv$J-z1jL(nxtV>xUS^dDAgWDY48zX zI=r*+X;r;aw@iU9J-ijUj{}1*2ORK`9xxn=$(8!{`*txsr7$zY2n}otL$u~uwo|?1 z+W;S=N7!U)@U^O+g{EsE!;0c--ZvlLx-53xm+oukv%zY!L{9ER${0M%wbQ`Y%n!O-FzyY8E1D&!#aFc6TdR z@#2srufSNRg#yEgw_YzipGl)e5MfOyI4G>80hicFYA4Tu)| zB4+0<{H{cp)kWkOiF9rI8D;c45Aqj>IzdLJOMI#?R&f`Eys@1d*zuE8Z|Mv{q(fc@ zZln_3CCJSgYk#aNUC+zJ@)q_t8ZaBqXBVej1bLNGHcWb`A9>NZHKpXv>g&_RWY;2J z>Y{Pk`=HcoDHj+r(8F1}!QM#3UOcLzXduM>eKu-@ohyQPG5jWnvxKEr2M4T&4$&;_ z8s5qAQsxl5U*KM0&10zWvBmslR=oN87R=tp>r=VLr~^LT6%1V23)ht68Vn6zq>Quw z7*;O77L>hENz$#06B{iW4AY$x)vd5*ubb`JBBcZm?loU=jEG`ZFR8Z7MyIo!Oqo_8 zELXYK6qb!7JNEGNm7k8t@TFG+r~&BL{K>SW4TEpcp0xsofnSq^_TrZ1v<; zu`S`$7P6CUYE#a^UY~L|+IH$3oC(KPa-=J}7Qfa@=IC?5qu@Ygow8 zTw%O1Kd_X@^746&ELofxcZ4y4zaRhmy3fBTpKpKsvVKh>5Zv0c4ph->CQD$Difypc zV6i0qeJ-*?O6(H=M}Wj>IZ<4_-Ez;WG{R~Ik|5+|3Pd@LL?nR>UoM>=vG}&#Ac>7Z zT~y(mwsjI7%!sv?DiKpOULJ|MdpzU)YQ8#G9ZC6wrjj?Uc}tUK=A#H^gFu?`giMX) z>Jvcc{wpVnVk|pXoAZ>Y1ihg5(V+(=WjFSV-H*8|RCU>ZK=mbuAb3R7?-*S0P7TAn zH5E)6>?-|kJXU=|u|YM#V_5;KyB);g?*{bkhi^6KTPFn$Rf_N5b9pFuu7XG6-KV;a z*2(P8xV%f{sO+?BZS%a-DiPrl}_2Dg~}kB`BkQx$5r)U<_6oUu?kvaF@67-HxL z&+a>>%pmnDzZhx z7xNM2XsCJM2pr$IgG+G>t(qE)cZ0y^jS&Yao_g+rdP^7Y0oR^Q1#0s~JA<1eBJ%jn z9!cRs_}R_RUAOhP7%Iqgtl8l+WMU9!;AldYHxWH)h7W@MLDvdZO#`-DV&hfWAELCeS^hAm;6lE zoYlh^>o6EhRe5fcQR&XOeyIMw!G0}4$0ta+A80!mOKe{aWY4O^c_)IbH6HzO>_vRv z5#U7JWIi?eQM6Y%jhJX!&uR}1^qV?IHTHP?^6AVKHHQ;H~IYH*C)31wn+}^0aOTjzqlN>Ka_%b?cg%* z<>}}?S~EqaOnk*|);aU;&^V&z7N^_WTlP=)$=`BNe6xFXX7u!pxZ7hi0@BfP7Tx!y z1Y+L*NA8S(qA$kFxU1Snqp;r5t>t6fTjQIT6ypj9Umz$G8IxLq5I%hUh9ZY$u|X!I z-%P9iapX=jZ+79(tkr~=kH6)L!U=(m(aOD~?Z5?wdcYMf2c+fYn89?rzB!rsC>NfU@mW!mb;XBsvOJFz@<2n>R~D z`E4g&1chf#-u6jrU7`8LxZiuCy-hTA2)Ry0xxt=e%t$V(}Zn zvNkyB=Zm((f;~wWX)WI!{G~7Q9K?rCUBUqt0zZxb=M8Y9y2X)MdmDL&>wPm^5^xIE z-jwe4ph;1DLD3cm4&oP?k$6^pav(^RtK12EEh!;Gibs@VA-c)p$M>;@t2MZk7&BOg zpCHFqePD%$`9V}_0B~a$qxm4O)>NAuOmZRfmJxMjG|JbUz=T($(B5pmoEy1H-4|gh# z=YC*RP%QNh&{W<`Ge#ksQ=r6p1nIO3f{)5-3EC(0w=oX8nm|&rr`xnYSO&R7Xyiwa z>O$CN2}DXGacOSY&q0!S=YZ%+`grjwv4RiM_8Ga^&{t#rb(UxzG`lltz6;5nf=6xQ z#VxXTeHVi~#CB%-8|XQyRC38YVcKsgfkb>)#!W<Zy1xp_OsJTzWU~^Su=3cPJX>X^s5efoY2DjsI_Ha zC2-KX^&H=$w6K@}odYMacFanblc=E~d~NU9w6%k80DxgHquj z)nG%+`{i&*(!8^srnh2aZeC{up}42yelmN)``=rW)+DkZWoH`3l{z)Lajl2*)s`lU zDJ6lDV)!_)^MgF+{ap#C1Hba9ts_9?a=K{f%V)%ipqH1m!y*p+CBF4$(_+611s+Zy@557HhjbWcnT2|cStIT9PxB3DTTsFq z+U&6sl5sk%UW44}>0}P$U?qrtpr5c>l=cn#Xu^pECnZN@eMC%-p+VXKz1QssaJlx5 z*Y+Irc}{yb$+*fW}Ujp z3p?NII2vioy=WKX>RV}`!J5{{{U{Kwv&e88bkV*B;ownrc_?&MlR$J+pxhjFA`Nd1 z+PznEh=awH$4*@Gag6sC`woLzdqn@}J|Jz$?3bNiH>);eANblYg+FnHNU5nG67hU-! z!W|?C5{>O8%qS`Uh$V(KbrfFRI(Y0l)Xy?T5579n*0wmlURsEQxnUk@FH9xaFw;J0 z;OFOL2}bv!2jYqCDVs7}g`}bKp{?I!`RUyhBYeO+$NeosOLWalC%lqjJ-xTkJ5FRO z{94=|MXX}2#>ROj&A9t=#jX-|bQNbFH3*78uhw+g*jU7!!;x*+n}fM*dNCT&&V~f5v_^q~WPp_P5S2`-OeZQx5u3 zm|8cJxh~7DLG9wDt=$B?6j_3Vz)d}azPjGCa0~ukUQ+c_l39{O^t{2_?Q^+%WwO=kM@2;JWKWOw)s{zEZ&s2qG#^kn34q2T^QO+S?El7a~Iw4w#>o z#Ys27a9O2Sf6Ph9P))bPwukCWAgCqoNQQLl=QbN@?N5~wAHB>(u-H1IVcwjNi0eJR z+So#}Rh~d)bLExA<rjg`Os>9?qfn2s|GJ?1zA*Xvv@v6f7Iauek|De9OnX8#QL3a=e zyA*VC>1{v?uJp7~_b(t@ns05@#l;|6_(6r9rNEe&(a_;dC=YLjG*E9ze&Ahe!~3`;v~~m;e50%1INNORFgKTp7ad;j zRmOD2-i*(Z%Z|C2F|x;l5?ET)l-5ZXp#s$tI7_dR;zYN;2g5T1ssQQ zwmdMmlC|B#EO=|s952l|-ab5#?W!=@e1Ox%mA)@}6DuiXIy3}nLFrx)iY&T%s#m9x z4(WJDq4O`$Rq4BtOw zpHmthOgLjw05p|MyE1}-M^PHsr)EZFn!6DiB;?< zYuv!Xb93>d!*lvRA!`ej(`doqvY-&b%ky6bDc4GF+IAb@unpW~4khksF?@d1VW)HX z)`r^rYPk>DAIjEbV^doe{lbbqk0M9Z&M%D?#dm3yxSwxLrnzXLXcuVZG2)OwrGZ}O zymGV^4K(LYFzevB$J%KcVyQ?`T4}F}vs>v`Ho1SW)K4%ImVypKQ;jUcHnne7g=Lsl z4pW)%rg{Ivk+x~ui7um3_{W$jiLNivInsKBYhLMM^@YGTuLqmfoZ(hla1xbh4p2fyJ6wutLLhtc1Gtw&WZF;SHqi1vF1eRgt3qRDkt5$90gnx zew*{U&9$X7w=!wCkudoAn*LiI*%QWC$)ebc{HFkUdr|_QWmF>*x96raGgV=ZbM%G;~{Lib)HH9?Q z9Nv+hYe=A1+l?OquElSR!H-9`u2~m)904M9cRi;Ar2FrO)(Icn=X(Bs5B%X&IaG;rSVN zMvJl3D^WQ>^Q<5~NdoB&d|DB@1SG2$DssrpL+rA~jT>VmA?gj zjHlwA`~ix=fvIUFEFZgwvF5-acCjR991*hIAnI-S;Q!a-3C^qW9EOn(1w#)>wksLq z)w7ZO6ADLw`RU^;tYcK94j{$q{Jd4$t0FI1dkPSbP~z&61Bu zf?Ynt`TOwHpAK)8=gX-HB$Eh43kJ;qJ-xxmWs17zj=2R%&^KJ)rE z9pu5TAuMHBVzFwCsqbez18lwf>6xizoI{Y)ai|xbme?%IF_P)`y4RcH&DxzK6;IF9 zcLm&bUt0v8eBo+nlPaN~0>0Ba^%gjiCM(GG-QAucs^iPr463L{T&PqeqQO?QIhbN@Zuz2C zUU#(c#qXyr0+x^Q3xxn#1Lg4U zFCwy?t@$GrnhoQdO6o@d88*wU*aB2CNU3;HsW!F&buaE@4qR6%Mr~wQUZiZ8Z{2sv zNj?ca276VXk}K8ymZqHmbovydCCKH9hR8=muBpEL9dJzRyLxws>fuf6Lu1_^(Jw=D z_tE=G)%)4;&;N$n4iHheB%#-+*1{tgSm9ujXrAG|m5Gfq)l5UW4k$@!$g$U{sfD4Hi$H^fx;QViZl z|Kg_oOH+zpj=wun5n3tLneOCW-al>`H$Fq~ZHTr#-KCk|F^~rgfMOt;c-98OEkPUy zcWz{NUgp4ypz~)?L&ly_m7+`6CxWUj2*mQ($2K0g>%Ty(-t6Z<1JMEn-?pM-Ceq-o z?WyLLP&_O_v_QxJ!(ozJQPesJ6nQbn?e6X@gs_@sS_kCjrhD{g zD#btlN&Pf#6fvm1SapR>AgZG?Wx=L-g!m||&VnjHn$!}t-?=@ZxcReMmbnoS7#~+G z)}x3q%>*BDpNDthz+XTtb?F#Z*T2dvkoT-D>{rR^9Qdo zHmXIB0Bnv2!XDB90J!T20GAyd@2;L6kUp;zx=?SquN1#O2pVIJJf1pum83Uki=K(b z9P0n7D=WlQIcpOIfzr?&1-?T>U+neja%#s!>xhop!?z7~$ZmYeyd_x~kS>q#W?XSB(>5dMiW@zMqR?^{T}%ZVk5V`-<568)gDum z6~hC}Di8$w<#<2op=NoP`?Boe(uf(+Msi>4>wC-P-R?MOxR`Om9~7%{q>>%7z7Aqy z7|=BT-1235uRw!os~qP{f7}8tce*$4URxw#D@cos?_-&Zf}if>HIYpjeR*)FkuQS# z+MW4%FnnECY{T3NYf&5wzFFM4|GbPm?jSGX{|ya?rLCR&4gy+;N$|*|NV06 zx;(2;-a=X_^)06b94v+`>@_kgFCUhAmYZy&t0t`~H|GlAb;Xf)9XIVun!jjzHlcGzbmm zFv~!OQ`|Nxo7jT1Bb}eI&c^x=H9D?kc;Md>`p$@TlF73R-~QRi@_#7qc=X#NK)OU| z*ys^}@TubVw)?I?!*qXPUD62TpU%Q{KVMbt?jP`2D6QeEj7Qx!tC{yFcbHoPbVfm14(i>fv7#M4 zUt=!1tb7uJ`vW*QD8iI@qDLye#Gyp9Y@W2S1*hqYQitXr#MK{K6%1D@Rv0iytyK!_ zZoEB#tsev}kho{Ip~k^mGKGS%y-r!E!2MQUbE~d}I-S=~H9xLUWcqW38|6#Gh*7Yv z?hpFKnw3r6v}juJRHrxdQK|6r8hc5QqiA@;5cw)n(Z$p*+SWZ7*N9Kqi*s<;57{d- z)-z^vO@k#b_uQ0|w@B8L{;~Iy>cuDtN1jxsf}AJKlkF+lkUic(Op)9v|_l)A07J6>!BM0^541IYwJ3=O;;>d-~zDZ61M~3%s+-h{0<{s@~~Hthp$=W_%GZP5xPf9zV_U!t zKO??0>*qjkWWRAp2!fZEabVCq%h{js)Il8NmsglcrV2A~b3`>hmfwpXdUm$Isidy7 zN>La!x10j4Gbq6$H8grU!04M2H^NHTQi59+COe0dH;W66exqBFcs{_MES2a5T`IFE zoma@vnbC^{5{BVvGrag-r@?{Ge7$^hmX`chzNTc>Heu3Kxg5neP$g#{T|3uhwH)W~ zHVe{ko-!D2|9HuoXU0!8PsYE*>$0PDn0*$pX}+~yCY&EvClMn+UiB|XP#_YF-R+!< zM>0k#6%^zi%QOj5UAH5z)84V6-aj=@2PrbclQ7id$_2wUCPEl17Q9j^^J_Et zbSI($yixK~x&`;9ri2Voo(R!jiYy-3+JNYb%{`<}0^N5PXRg(1XfkF2Y`RZ4WGk1KCm+T`aH*Upf zHE>f%$fc3x!Orf^ln2~+?*d-|0ZBG;^w@To7`)8WqioS9hqZ4*uf(D^j^)WVmy`+v zvb>!2x6;(>QpTa%wPJT{^ z4k}{pI=soSmAQH^s@?hu&G!&DpfJ2i(-h=MLwO+O#)S!|Pn)<>WzFBTVR?+qv8#e` zk|`lMts=_Ldn*{C^i)E>pn*Y^VdLEyt3iBGJbOR-YUX_7;62yJl5YhEmw}Zq-O9P( z5FUxIl9qkVJG?%-vOjv@MYiHF&+s2w8wA0twsn zYJ(91fshTNBLOyuj-T#*@7PV_da`n`}@9Msfcis z6i`wumlk*M*Au`0dt<*JVgE(rfImFVb>?=Mb2;ghPNP1{O2y_Lx@hWsEZBl#0RYOx zsJAWpM--ALMuh$ls1i(={t&2|7d#pM1zh+Sph_=F=nsJ^*ni{cO(abeVnKwSdmlh{ za>WgDnU(9y8{+LB8jj_cPAEvFYDA&5WkPBCFEmfJ#Dm1f;Od9p{4AtJURfa7c%Bnj zno=^HvpOV=nqKm2(vfI*iRqR{^zbOx06{7d`a`rtUFZ+Nn1Pr74j2=<@yB3H+duE= zIR&EESL7#o6#1rzo*XPm34e`xE=>$Y0lo7Ij#psBrwWD=U#ad#U(?d`CIQD@?5*~| zoC5}zGHt`A8Gs?L_I)%HF+aWcFVNFJacTb*aQ6RnuVOwdy9b#s2ng$Q^`19>w?t53 z-I{=y$~FQVhwEK`5O?;MKWv(R?))c+!0EKrJHT?I*3d?!x8}>J=s_L;$y9ckbeGIK$oF4n6q1A(?occ0Pv>aGSi709n#knYmwodu4%ctIuFt&@T7xQ8pX1r5%&92xvNg>dW}F13qC3am zWWCZmd}o5i_vz7MPZtqmobPqsX=L7?z{Gc%<3T>m$Q5R$dsk!Ez7|elkIDP7BQ%bX z?q@u>4y#Y-_GOvl@8vOdYb}h9G)gHzos_YB56~#q`LmE;?f@CV9{MzFKRCe5uU`~#np-z_v(}`S*wh;ckoRm9GyYbBS(HmxB-Kc}^oI&@>$`Oyco|w?*oFH8=Zy0+$XYiBpZ3}tNdIj%m zeWPbK!vUgI3;v+hWiliZx-Y483^HG*4I|&I+OF^ORvsD}-!1Z!mR$2*_AEnjypvJM zyliC%-S~c`?re(+@%s;P`QH@vPX4C#n53%58wtI&mp3*AE6lE}Hc)GS4~Z3;6(9<- zQT#~$LOgUtwhcAoapT}kP@5{kLwar+bG5w^kV~dU>x0h>#{@LO4TqG-o16CIbLA;V z_IbudyqG{_>p|wOI*wH^H`#5^Li=$q7%)`qTnl%n&dOun1gI{7oII5ds&~G#>+<(C zs+ue3xLmNy@92y8RG;-VnNTqi<1Sv z4A#1xeWSKf!@FgRH!Shi*r|Za4P1rTlx9K;s@j>mf;v|8(yX_|!(j1d(nrHAgVXk_ zAW5*pf%){zNcT>ltS6EStX^sFMC6;V9@wP;(Fd_ztluSyujImnxhCKAa5&cuhs6N` zD4@malK=B8xfPI=c51mp(({X0j$D7NP49a>MdiND0PkOJ9r|Syk_~5Zu{;;--vp^t z1Bb%_1am4ze7zoDchab1_XutB<)%$>|H980*QIP(S)VxnayBg)#XS;4`j-5+|17MJ zzkG5zA!KC*BzLYzP`Way69i)(uA1S=lq`8O{Obi!HHwX%PkJl{E^te7A7p z>=bw@-KQX4V5$5d>X68UIp+1acEu@w-z1(Mo;2od#QC@y&MEd3SYxSST9Q zqNy2VcL_YHAAwCyjO%-o#MVd92%UU^^Jt~}mxY+RuW4H3&KsKvKlK?snhyyhu(caK zbmX%cV8c_E=#+F25A6gm34Z6lp?z~bVA>yx7#~(WjEF54*U@yWX)brS@Xsq%2o6PJ_i{S@QzAslPkO8q-DIR;?yx_6&I849*H#uy&Dl`NvH%}_o zS;o=L+z5SLa-W6ffhMm-J_`Z8x{vTbzc6&TX!9(3Is&E?LQ+=M^O##sPF^Y)$4j8s zs@ZeX^jME--_Jtz#<#&~(KVAS}bXZA}yh=W%`Ji?U?-oK@?*IG}Qjmqq0( zBz^+N|85{8YS<{{Lp0=#O(qEW!uT1P!z8**3p(4aWyd*z6;1cS*TOm8J=55Mvp50d2p^~A+FhaVyg!jpEIuO3QBYIopIsjaiDYkR%@Mw0rX$6+u(SU|EDT&Vmv z(~Ef<@bQ|T_gaC;U1sLc=W-;g#`S8nIZnN{X)*$Z{MHU*zk0}uVD%fJIzBAuiM%-1 zYw=owlrZSi8KVfx?sUc+f7FWE-W5;;@qcxBmMrw;oqvMi(4Uv^Ul0M&774!e^&qhp z3Bs8yLEF+j$Gfxt#tI;=0;IE{27SpF9jMR%^4uz|2myN%p+x)FSgz|zO=nyAFv?iRRu`FKG^_H_S zYC2_r^N%Q&_h=)9mb%u4mHI%2myEq)<32K-1JxC6%B;w) ztE?FKF9_;CPO(7^BA>}%Qa#1!CHB<{xl7#|#_;*IWb=+i4ZRA>L!G2TE<8+b=5E|*X8ZqV4tT#7n6O^_w(h{128Q* z3{nKDIP};5)1n+?YDe@IIy3DXdl8#=KP1{ce)8UDA^m>7i)`-kC;;Ly_yT2aC*u8x zE8xE~7ivcw7f`3)8Y^Vo^g?D@_Ra<4izueYeiQ$z7++6{Z*4x~G-Wiw3Z1tZfRsx* zx`?tVEKuNB^@W`DU+hGaW%JseWL+0a_kI$pR|~Hg0XotzTd9N~ck46??aiC2!^s|| z4s91-KzeeYD?|insRYYkK3}){x1uSLC8-Gd)kePaKce;r8(+MRFEtVA-a~s}^bQfm zlZ7yYiP)7M_g`I;#|iP=*pgB#uK0Y(VVR$vd-F7d996qeQ9+tpZ5Kb+2uc+%JlXo1 z4jULFupN3V-15`!`St56>c~neSm97p!8Ki2Q6uebfl|D4zxB6HcJn|a38^yWbwy_g z5MjiHb49D>?Ml=bO*0PLBcIaI10rzZd|!v8%GJHAXA;6P+&blp-N@3y@{g1x0YC9 zdjL_Rv9+Sd6~Xwp!@+Ien;W~yOPs8*J0CBAQlBSceO|aH^3Un63@p0cMQG|FYcm`y zlyvE|e6zdzX^6dc#C1yX`~}x@U`_oir9{_w9Y19da^A@!gbAytpfs`HF6r4yDXR3k zV7uN@das7)3`JN9tc~i&Qi&YP2aj&54#Cfm=g3;FlfK^f(=p-)0;NzOP=~^P9-MZ# ze~7fX$9>nNdzxI_+mv=s8iDb6E^*j6q{oZKngxhs^+Cf-Ss;MGx05;i+7Qi3H6GrZ zzp_|&UBz7ah6`5R_)YR6Y1q$AQk~DFN50`i7+bx*KhJs;=sRBOCY^CqDknS+uX~Mm z7eHT1GRhPz2-*IU`rFx5VrqmYP3M`jz|$S4c}hp#Y}?FJK_J{dsr|vBNmc3{ul|r& z|5=C;6_%^oEFi*SjKa~Pv%*$zRYT~RRUk1i9MyXLeQA62^|Bpc zg8sG%;+_U5)X^J0_kG9H`~=mURZ?Ah;!KPJW3Rd@-_|~S@DsJNukd<9cerWj_ zqVi^2rDCxBAR$j8&-xdTn>0W1^q?##-IuzxlkEoU6--=jmmLK3L%TygBoht@%wAp5#@+& zZ+uLf4#K)n9px)Kv9+h9?xS@~5+`g*>l8)nTdg6$2A^ z^Xx~VR^o)n<<9RAy;?@wiK6HMMhYS>>qw z<94oe$}z)QLE!3D;OX^<=9<#bxyhZ%e`=P|<7HH^=?*i|z&81*ENv@mC7k$i>k)2= zWzt5{5OMWX;NR}}XxKS@R*E<cuhW4#d210)vjyQA zt7bpkES9N@8t~{75PUuhC04qw92{BMzn*1DKVA!z_d{pJfKMU1?cjEZop)C2F?`+f zn2Kt)$gqKJ45<|ESvWEp+f$#$ammOEzCZFI^u?TNE5L)D!N_vSdrF3i_#Af>q)a+; zf{oJWO9n!&8Y^-N<*t+yHN92Ko9fJQfds5cd5V%Hs($O zMF+^Bbt*PD!RFa(&pn#K>0TZu1X*lG^6@QHv2p8;C^y9#A&ZJx28(O^iS?P%^U8Z) zrhR8UBOa&iseqyCkCUfhNolrfYDIwEl_=&NuNhQ2;qtivdv@(aN;!K&AEb-(^Vz$X zq!wW6Dx0tvASrefl!QEPOn}KW7V~Bx5}zu-EIQRky!)@ze`w3ZwNa zjSlq%`G?e7C49|QK7^EeGoB<ud^o%oW%-=rUQdAT#ZHlM-uNKs$EfOx|(TbcDAb;2m`{X%|J@tl#yxJ{JVx*@iC$^}Rgtw`6kvc!5Hjm6&ta;d^Hj ztPAMgXoM0c_+bwK2qmxOnN7mu9L`027pK+T=ZV2FNL&j*FRe--QL--C{DGvC->iN$ zkDwpTQ-v9drr!CvEbH95^fF>Tg^I~kjv&O~WQgGcrd7&LQs3N*%@3K-E$H<|bER+Df z98)uapB@pKC(=)dQi7nVmR=9Oo*M0)g#!o<88Q*3MrMxDEyDNzK8xcNP1V&Dt9Lig zky_4?qkY{U?IXY2EQ=G4fGbB7txz3fJ=O{Xp5*LXWw<|Zs?8oZ`6^)7ZWC;@UP^RZ zX2%B$e0bCg`q|(7B9W-t`;KpMN=c75tJt$3)9GI1TNa_@6pMA;364MemJoC-wpN~P z)IB>}+EGdv2yG&P=hnRFJb6 z{+_UQiehlA)ue9ytD7_5Xeg)Js%h~(TZ#SA&WBROVslF5&JumIZMDsz$A8Dy>#T)T z?sUS?g#V_3O9jKWo-`^uoubxQwWuVc)H(~^80LyYMoKjMtioCyAXP7%HGIa(N>`)n;JGtsf+78f(X;8Xta4*n1%Ew#PriC=reaLQcEWMO*--72$DFI zb+q)(6}|&oCfCukd8BgoFlu{zuFEHyg$Zh?z0VvJF-{tguMxd@=s8 z;lv%8U7ZBt$G1^`fB)vk)c2G7?E^pk{ryMz6Y9VDtq7#~r~l`N(6g^;e?RQcgZ~WJ zpOxhQ+lI08i0B29&+Lpiib0Fp-;PyM59ii>FsOLkSf3zLG z4UZ6487s3400=ui?;ne3-RFBaaaW3}D#-^=y_P0gLQPTRD3*#K4Ujpxnhaxyp3r#3 zyZBWdTUQe4ja!yQCU2$12nz$XYq!i`wS6lT4-D^rUBn3m8+k{8;an&zU6`doB0y*s zG0OzFQcD8IOvQANdC6Dyy3LmX@k^n9QK63tk_)<4YpbX9wy#lz6W?2jh=};hczSLK zef_(P{-ND~1sdGmlX2SF!e2keb%E^2h@HxOZ==%L-H?)!oAy!&_3st^C9KD#zVFix zDdvcfp3z3eK&+HhV2Ab|JgSICXes1CUf*|9;Kc4t8T7t6on<{&XdHT>>bo$X&{An} zY!wm2lu^#4s1v>Qo8`>1{EUyL)4wo3S7@wiBYQdGhJvH4*pDx2LW~u5S?4zj@KxIc zjfmvrVp;BlZJ>nN6#YhrXutb|@0A9}54LCBfB>(w#1}()eQ)!E+o-LD-eM_74fVAK zVZLnLy^j2+%&M%GM`L|{KR6Fkyn*m&oW0xdBMG|K&se{piG>QRSeG3h1BCd&gfNdZ z?B@%7z5lS-U+j*Vlf}kNP3;5&tfN2t$sg@JcgimJv;kH{#OweN2*{cZIJ0~Rcp!%v zj088PL6===snhAbUPOe(JvHx5?G^nt*sa*NDn%>g_$BFClJLR$RL~LoF&cdlWP;V6 zIu8uYFHO1-Y~+zH>LD4wPJNs$a+lMF*}FE^s{KRPh+G=mCgV$3V%nB zSdgbrohZ$JH+6rDy?KkqolcT{=~GX4 z-c>;9Ps|5DT&%GRcoN=aP6iI!uXLF@fi|*jHuZb2zNvZRW^L+bC`+CQ_Ql?KrDN?5 z4n7%^KdQ6mF7N%I?V>%QwOk$}JZxsSpRgkS^vj{%RH463nSARVd(iVpO}*1)QJiWO zDJ?;cgBzl2P7w9dh%`YWH7+rw{L~WuR?rY~*{rR_Zc@!AyNiXlHql7Rm%O*mYR${5 zcOSFUg<7IRx#8=ICHv2I#h$W*PEp5o;M}Z(I7M?p+Mt!F7~KmSKW68Tr}`UPn4N|v z%6wmGp0oJcD^kkd@%=KW+^OYY?!lQf!o3>bQ|CHK6KRe>i>tTruAD9NDAdeMv2o9E zc=M!Uw@%IO&6|tAsy{vu&44GUyGD!}r3pGRNFtLF_WVQ-XVBKJz8y@v0h#OU?AqR; zwl!-^Cy4%LlAIE(xvwi~Vwa_m$KM@Ug|a3^tX61`J5Q#k%qEWI4~Mi?CVg1)c=yZ{ zDRf%*$A4Q#{_*XUwfpd0wsH%qG*ZM2t}IICzTER|?1Jlg`d*x1%CXLTgjc|a1V3~iKcEiu8<4q|CVl)H**ojxrm6?#-vZru$n=}54-=q6hD zS!m1~CPf*E7nO5J+Az_KQfbSIK8I8wT)yrs?BdrQtMCBa{ls+Hd6IQpI zN%$Et)7`mPnwP4i97(iT1`1D^LN5=zRvWnB|Mu-GPmjcFD|QbuTHI#Cl}g!)kLeLR zN305(0l+X{GGTBxHK&4}HEi5bz7f#Blq(ArnR8l2xK~v(+F8(5DAmqO}NQ`6sff`{9Egk}LM_Gvf zzG>*Iu{PS`u?gNrB1_h3Z&PnSG`npix7yRp+AXIXq+nij2KIv1Avl>R`&BTGXhWHr zg0@C(lY6uhZq3M)%(eRtLDqf`mb%5#{qWW#UR?svXodDqWw|!^#uNbNn8+HOzGD z`%?U5y*xsO5g1Us>-?6}w%M&jx$ZYl381ZFks!wx19!?=&5eeb%I?)j-Ey80=*-D|tF(cJTN73ICwXcv8U1=Agjj$w z!wU$hmK0xrC{>V&tKzkv1$4s!o6+_{ITogx3u*y zQa}TjR`+}K?^<_T3TdHEzd;fzt7!Fh9K;2^lA!yY@b{#L}~l1IBG_E7PBZ2}D{3m4OxG=J>^%wX7?LpB_X7$c}&Gg}t_&*54yb zaZfFWug<2fJW`bG8_4OKSG{clQvjf;-lld&y))HvX5(}teiOsZGq*@Fvq@dP36%!5 zF4erMq7Na91}Ry$qKvr0;$x`!HCl2p(@i2N?M~aBnjiUT=!ISaD3^4XBl?jv z+9Eq8TNbFU3R5)_+uhpEg`u9XU6s_Syr#z5o4kl)kMB~emM`hy7??dUx%xP|);Vu@ zc~X>%?X9jIba;^5<#(#{KU1j=ApnUqkbOBXg|_0O@p0a3IP`M&XsM_AO%|oA=QYRU zgj#Qk2*0Rs=MXimI_W?6VwCbdcCa$W4F9Yoy!o zeqKXh`@+z4rkMoBLhgxo2CghH3?sUdLN;|s_XZtXOrn-3xd}Jm3qSD>Bm0zdNF;(+ zRx87H*%Vf^deA?=#Y@;6EQ_(B%BeF%~w%XIQ5;@L~{$f#OWB z^Si;HrauAq#9Vcs_^q}EoRe2+v{UZG`bBM;J^s*^5(&-(ReSxE*U}IwNZIjB?ta8+Fp2eH_ zyWeM*R;ONlJRehW;6JtPV9ybx`3wZWRFC-1nrq@Tx2D$B7 z&N_SI&wWn_CBdqt9m5Z*aJX$J?iB1%aBh3%gBl$*C{fk@4R#g$YF5wCxD2vIb{WfO z1hFIgd**BhI|uI-S0wgU_@hb`r5J+2Vsfwdr%_HQ(DU0~`v$2AFT^FBnO2()H4YSu zIofZ3k({!#ufQ@W3Dj!KfOyI?{Lgg=% z>}>7c*FBB)abDG-Jx7nS5HT#K55^l39DB44VpMjxs{FO3uK)YytSibxjm$cgwBo^T zh6{G2O9_Xg+@!P4fk4o$l3+ypUE79*ZcmTX($%2R8g#5zy}`r~b!AcD$`G^P0&zWP zeDwURZbs5YRQ=PsNy+mO;Vq;RWvnq5cNV4+5)~4lZJ3xo+Qk6l5%ty!P32Pa&=%%y zU_YMNcI18ma98^y)0s;c*OTIa2XVMsvzd3XPEK1+AUl7g%dc@k0d*P12Ihs6!E+yS zv#=hrKikw>Kt~PWIFf9ZHVDLQeGY-+Sq??&Ntx0L*4yI{UkgQjhkInYd$)g8H%X!j z#g&aD3!eA2PpRKI_T3ITzEu{8kt^eCnuRK0-8*9t?R}0MnE?A6|Cw8Ds66(dt^RByvYu-J#HD^ zvW|dO@QaL{kH(NYr;ba<)Qq7p!xGAg538Ppr&^lGFsY)~M`GYE=&JatLV z62DS?6>>|7D=?d#VeR5rrC@Wack_;g7+J)U;7N^SbLjF}xI?=ztyB#&u@c#f*r0@m zEn>fzoy=iq(xd4xT*^BMLQJLi_B)8_>JL4>Z7*zRs>er}D5;O;KTD`GRUFjrC!A3g3e9}W9h}=uu_%SC_r-LJru_VWnxAJFJ)(W45fr_r? zVm#sMI2}NwO=8A(_s{v$g%RXZ`+4N9tH|8SDAFN76j*&aNoy`HUUk_%n+)umYO3ZG zL|%DThU@2f-?hz>sLACfCMTzir<<)*9objuFzN=N&gYEc;D7)dAXA4x`89-LndX)SC+y8uO*@HG0cg;z4!;m!gjZ zkRzDv7z>Yp)*d;f4mh4!OT1y*l>J#q$uQdRLi^F`%PCNiG`~%)FdTW%-cH{%5E6dK zHZV|4B<~I#$F@itP{HG{gOMNT_16TAu>_ z{6xF9g=~KNEb74z={9i@ir3qvjwW~_fSPev_s3SK%-Q!xS*`GjwZJQpl}GgDRGXa} z4%mPv3(>K9S7nsZY`UweSdC{PurQC<)07Vhn0bN<8i@?4Seqx|UMTf3S&)c`fX zm7CYkkRdNqDcjxi>`x3&{9h)gY`$g=hjHaq9+=OM(~km2`CMvV(q_MUY|-+^s#++~ z9z3e}(KORCAu&@saeptdi;#Gj)O6gQgg~af6W?tkeb~i-v86$i?D)Y7jUNWGXNNm> zjYF=9330lHIcXkPE}tY^R&a`g%nwc}dT-#?H+w80Ev2bAy_ z(TLP{wS5GIwBS803jqT+`_Pr~S*SM#GPkw*Mq)5uf#`+l&G25M;;KI(%AA}Iu-ZYy zz7o$sttW1NQaq<)qYy+Vg4XguYS&ONdtbA1H_c=}xvwq#$U~6J;DgWKxV*LY4_&Uw zq9Tn5-Xr#?E>l?5Ou8wrC?6h>6?G%uf@rz6!)+A?X$ntwS6D~MW%e2H5x+n3g!k9% zIsBU>DGB$fizlTfRh_!K(1Z0? zRX`KWI?|_pg4yb7T%cJD;y`JA2DXyK(x;1D#paUCQVLKL@sJi0zhJs=KN%mGR~&iML-*tIb;qWhnM zoC%h=;bZ-pT2L624QE4F1qA;^X{^4M`0;KCGQB4`&o7eUd*aHS?u|C30oZSj0y1J# z=qKUL%5Cc{Bo0?-)v#;|GPN(WpLrC1zslrFdave_9A+*pi;5`w#o}xU{D2UO1E3aV z_E#zD73km3Tg)?k18akc20!?kh;O5UajN)-JyUtU%3Xe17@wM2X|Z{vKRRp32T4fp z2)4nucfl2@^vNpfQ3NU(*)WrdAn*M(WHZixQzkFcFHC@gnTguDz5Jr<-&u+I4>z|z z5BxI%e@5WX2>cm=KO^wp5CN^YB;D-FEHqj~#QLDEo?zV=OsOjc_77Cmo{YnF`5>tux zFZXvNC;J`CbzHxnuXN?Cq5OeMh2M`WaUt-OGIr=ls5WK<5CmSyg8U>FMVR6U*4R_n zt(oOh4o=G+7&NEAa~5BOWH zUFz+M&+1*_t77nRK!zp9+|}bebUc|Ij$3kVwJ-O$V$U|R?WX1|8d#<@Ni6nrC3$8K zd&2wT5Y8Fgz?D~xBhR#nK4ZB<1FZk4P(GNlm0GAM&=VPF+%etBjq zc>#CWSgcjN(0qQ8@(|n3&uEfjf=e=WgRh}O^@fp!pplq;I&aFI{lMRmqRMGA=z9GT zcMb{2{-Nqg1|YPh#(J|ec}LME+q!EjVvAOVXL_Z5DvZkx@H5B;L_0_~C6loD!Y;ei zT3&9gMX2uWOZ>IPk}W|7V$kmIb1nNVe2;u-Igv4<{%y%*1uGYQ728BdkO6|t@bS1U zoi=;jn(h&(u`N5$Bt70V78ov2jhn8RGE2m?i{xJ{T>`6c;YMC7_GNHfiBe^)Kv;X!A65`4(Q^B2 zYUM!oSCB6rmF&vbOJ*lB#g^i9qmuaml9n}&_NlQdb%M%|{;FxVvfo>7lj|O;b3-C; zg(!wJrKLB|tcywvhF95r*n@l|)RTOaJPg@LLB&9L_%OyxotKcmzc zN3b7;`nP{+V&dmpGgledtFcC@@9-h|G*RpH(k+l`bM}vyDR$v8+=y1tmYtNuN+CO} zTPoh<5}^C&xajU%=XU#g@9@;{*#JakO$MNf9f_wgnkcCDj+TYo0Qe*ks(Jaa>#^L^ zt~p&C7#AopHbd7=Wu3YIqN#@M)AkIRw{AFKb2(JsoGgBZq>US!qEMhqdB@Phz>q$6 zuVMqK9M`B~c8?XU{Ff-}F?eF0r?PbIL#G*<&2pXNhuuiBWBp_505qJr)iaz(JKM&0 z;Dh9HpXztkM%sLbXdtfPxQVAqv-gBUa}gMh(pcvxA>8?CadZr(;7Q!i@3R&{GQMMX9q+rxqGXv=`Ts<{Ws z0m!j7v#^52PRzEh&16hj1h#KdCpoM51gd&8g???Z1T@F61Lri@P8scXi*ehCIf5MY zF3R5iLlSD{mP?BW3>q{lgH!imzjFA#<&>CD?bCVEN7W?ct+cwXP00p3-S%)^VdjG0 za!9PP-KHzyq|fnlnhdALHQM2T7tSPT} z-SB!*N8j9TwChFJ&6>JW6^#LLkpqFQv5#eNG_PQi<^cI&SuRcSR~0)?JyymGR9B+F zK9RTY%rAtqxCW8qeGM<}{&ZX9M9ukd>)@L0;GKf;5`1h-VC(KBoZ_}b@Vqwb zig#CfP;AMB3hPo;ueA=})+4!;r3E6ZVfUyrqx8aH8w{*G^4916>Xy-WnDv-mlpt?7 rn5++KWg_lVc;zkK&B?gHYKnqy&gi(%xyJv7hx(s?@qdZH=gI#8!@XaO From 868021afb7f32845547c617316f03adf14eaa471 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Mon, 24 Jul 2023 14:49:01 +0200 Subject: [PATCH 09/15] fix the values --- package.json | 2 +- tests/e2e/playwright-test.ts | 2 -- tests/utils.ts | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index c8fa80ffb..3c458c73e 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "test": "node node_modules/mocha/bin/_mocha -r mocha.js src/**/*.spec.ts", "deploy-function": "npm run build-function && cd dist/function && func azure functionapp publish < function app name >", "publish": "webpack --config webpack.publisher.js && node dist/publisher/index.js && npm run serve-website", - "serve-website": "webpack serve --open --static ./dist/website --no-stats --port 8080", + "serve-website": "webpack serve --open --static ./dist/website --no-stats", "build-mock-static-data": "webpack --config webpack.mockStaticData.js && node dist/publisher/index.js", "build-static-data": "webpack --config webpack.staticData.js && node dist/publisher/index.js", "serve-static-website": "npm run build-static-data && npm run serve-website", diff --git a/tests/e2e/playwright-test.ts b/tests/e2e/playwright-test.ts index 1291f58c0..f97a92b7d 100644 --- a/tests/e2e/playwright-test.ts +++ b/tests/e2e/playwright-test.ts @@ -67,12 +67,10 @@ export const test = configurationTest.extend({ }); test.beforeEach(async ( { cleanUp } ) => { - console.log("initializing clean up functions"); cleanUp = []; }); test.afterEach(async ( { cleanUp } ) => { - console.log("amount of clean up functions: " + cleanUp.length); for (const cleanUpFunction of cleanUp) { await cleanUpFunction(); } diff --git a/tests/utils.ts b/tests/utils.ts index 41188d831..b340da857 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -72,7 +72,7 @@ export class Utils { const methods = `(${obj[key]["methods"].join("|")})`; newKey = `${methods}/${key}`; }else{ - newKey = `(GET|POST|PUT|DELETE|OPTIONS)/${key}`; + newKey = `(GET|POST|PUT|DELETE|OPTIONS)/${key}`; } obj[key]['regex'] = new RegExp("^" + newKey + "$"); } From 2fff8c713c68cc0e885471aad73b30eaea29d8cc Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Mon, 24 Jul 2023 15:20:31 +0200 Subject: [PATCH 10/15] choose screenshot folder depending on the env --- tests/e2e/runtime/user-subscriptions.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/runtime/user-subscriptions.spec.ts b/tests/e2e/runtime/user-subscriptions.spec.ts index 4d98122b2..0b91570e5 100644 --- a/tests/e2e/runtime/user-subscriptions.spec.ts +++ b/tests/e2e/runtime/user-subscriptions.spec.ts @@ -54,7 +54,7 @@ test.describe("user-resources", async () => { expect(subscriptionSecondaryKeyHidden).not.toBe(subscriptionSecondaryKeyShown); // check profile page screenshot with mocked data for profile page - expect(await page.screenshot({ type: "jpeg", fullPage: true, mask: await profileWidget.getListOfLocatorsToHide(), maskColor: '#ffffff'})).toMatchSnapshot({name: ['self-hosted', 'user-resources.jpeg'], maxDiffPixels: 20}); + expect(await page.screenshot({ type: "jpeg", fullPage: true, mask: await profileWidget.getListOfLocatorsToHide(), maskColor: '#ffffff'})).toMatchSnapshot({name: [configuration['isLocalRun'] === true ? 'self-hosted': 'deployed', 'user-resources.jpeg'], maxDiffPixels: 20}); } await testRunner.runTest(validate, populateData, mockedData.data); From ea6b7eb5cb81d7970eecb880d8358af17de2f1d5 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Mon, 24 Jul 2023 15:40:32 +0200 Subject: [PATCH 11/15] remove waiting for document load. Locator in the next steps will handle the wait --- tests/e2e/maps/products.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/e2e/maps/products.ts b/tests/e2e/maps/products.ts index 717e12cfb..1113bd35f 100644 --- a/tests/e2e/maps/products.ts +++ b/tests/e2e/maps/products.ts @@ -35,6 +35,5 @@ export class ProductseWidget { await this.page.waitForSelector("product-subscribe-runtime form button"); await this.page.type("product-subscribe-runtime form input", subscriptionName); await this.page.click("product-subscribe-runtime form button"); - await this.page.waitForNavigation({ waitUntil: "domcontentloaded" }); } } \ No newline at end of file From 2abee86a9eea026dd4292702d812f6ad45099e33 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Mon, 24 Jul 2023 16:48:56 +0200 Subject: [PATCH 12/15] use locators --- tests/e2e/maps/apis.ts | 16 ++---- tests/e2e/maps/home.ts | 13 +++++ tests/e2e/maps/products.ts | 23 ++------ tests/e2e/maps/profile.ts | 56 ++++++++++---------- tests/e2e/maps/signin-basic.ts | 3 +- tests/e2e/runtime/apis.spec.ts | 2 +- tests/e2e/runtime/products.spec.ts | 2 +- tests/e2e/runtime/user-subscriptions.spec.ts | 6 ++- tests/e2e/runtime/user.spec.ts | 11 ++-- 9 files changed, 64 insertions(+), 68 deletions(-) create mode 100644 tests/e2e/maps/home.ts diff --git a/tests/e2e/maps/apis.ts b/tests/e2e/maps/apis.ts index 0692412bb..a78f62ca9 100644 --- a/tests/e2e/maps/apis.ts +++ b/tests/e2e/maps/apis.ts @@ -4,20 +4,12 @@ export class ApisWidget { constructor(private readonly page: Page) { } public async waitRuntimeInit(): Promise { - await this.page.waitForSelector("api-list"); - await this.page.waitForSelector("api-list div.table div.table-body div.table-row"); + await this.page.locator("api-list").waitFor(); + } - public async getApiByName(apiName: string): Promise { - const apis = await this.page.$$('api-list div.table div.table-body div.table-row a'); - - for (let i = 0; i < apis.length; i++) { - const productNameHtml = await (await apis[i].getProperty('innerText')).jsonValue(); - if (productNameHtml == apiName){ - return apis[i]; - } - } - return null; + public async getApiByName(apiName: string): Promise { + return await this.page.locator('api-list div.table div.table-body div.table-row a').filter({ hasText: apiName }).first().innerText(); } public async getApisCount(): Promise { diff --git a/tests/e2e/maps/home.ts b/tests/e2e/maps/home.ts new file mode 100644 index 000000000..a438a0860 --- /dev/null +++ b/tests/e2e/maps/home.ts @@ -0,0 +1,13 @@ +import { Locator, Page } from "playwright"; + +export class HomePageWidget { + constructor(private readonly page: Page) { } + + public async waitRuntimeInit(): Promise { + await this.getWelcomeMessageLocator().waitFor(); + } + + public getWelcomeMessageLocator(): Locator { + return this.page.locator("h1 span").filter({ hasText: "Welcome to Contoso!" }); + } +} \ No newline at end of file diff --git a/tests/e2e/maps/products.ts b/tests/e2e/maps/products.ts index 1113bd35f..e71ce179c 100644 --- a/tests/e2e/maps/products.ts +++ b/tests/e2e/maps/products.ts @@ -4,30 +4,15 @@ export class ProductseWidget { constructor(private readonly page: Page) { } public async waitRuntimeInit(): Promise { - await this.page.waitForSelector("product-list-runtime"); - await this.page.waitForSelector("product-list-runtime div.table div.table-body div.table-row"); + await this.page.locator("product-list-runtime").waitFor(); } - public async getProductsCount(): Promise { - return await this.page.evaluate(() => - document.querySelector("product-list-runtime div.table div.table-body div.table-row")?.parentElement?.childElementCount - ); - } - - public async getProductByName(productName: string): Promise { - const products = await this.page.$$('product-list-runtime div.table div.table-body div.table-row a'); - - for (let i = 0; i < products.length; i++) { - const productNameHtml = await (await products[i].getProperty('innerText')).jsonValue(); - if (productNameHtml == productName){ - return products[i]; - } - } - return null; + public async getProductByName(productName: string): Promise { + return await this.page.locator('product-list-runtime div.table div.table-body div.table-row a').filter({ hasText: productName }).first().innerText(); } public async goToProductPage(baseUrl, productId: string): Promise{ - await this.page.goto(`${baseUrl}/product#product=${productId}`); + await this.page.goto(`${baseUrl}/product#product=${productId}`, { waitUntil: 'domcontentloaded' }); } public async subscribeToProduct(baseUrl, productId: string, subscriptionName: string): Promise { diff --git a/tests/e2e/maps/profile.ts b/tests/e2e/maps/profile.ts index 1ffb27bcb..1d4e535d5 100644 --- a/tests/e2e/maps/profile.ts +++ b/tests/e2e/maps/profile.ts @@ -8,62 +8,62 @@ export class ProfileWidget { await this.page.waitForSelector("subscriptions-runtime .table-row"); } - public async getUserEmailLocator(): Promise { + public getUserEmailLocator(): Locator { return this.page.locator("profile-runtime [data-bind='text: user().email']").first(); } - public async getUserEmail(): Promise { - return (await this.getUserEmailLocator()).innerText(); + public async getUserEmail(): Promise { + return await this.getUserEmailLocator().innerText(); } - public async getUserFirstNameLocator(): Promise { + public getUserFirstNameLocator(): Locator { return this.page.locator("profile-runtime [data-bind='text: user().firstName']").first(); } - public async getUserFirstName(): Promise { - return (await this.getUserFirstNameLocator()).innerText(); + public async getUserFirstName(): Promise { + return await this.getUserFirstNameLocator().innerText(); } - public async getUserLastNameLocator(): Promise { + public getUserLastNameLocator(): Locator { return this.page.locator("profile-runtime [data-bind='text: user().lastName']").first(); } - public async getUserLastName(): Promise { - return (await this.getUserLastNameLocator()).innerText(); + public async getUserLastName(): Promise { + return await this.getUserLastNameLocator().innerText(); } - public async getUserRegistrationDataLocator(): Promise { + public getUserRegistrationDataLocator(): Locator { return this.page.locator("profile-runtime [data-bind='text: registrationDate']").first(); } - public async getUserRegistrationDate(): Promise { - return (await this.getUserRegistrationDataLocator()).innerText(); + public async getUserRegistrationDate(): Promise { + return await this.getUserRegistrationDataLocator().innerText(); } - public async getSubscriptionRow(subscriptionName: string): Promise { + public getSubscriptionRow(subscriptionName: string): Locator { return this.page.locator("subscriptions-runtime div.table div.table-body div.table-row", { has: this.page.locator("div.row span[data-bind='text: model.name']").filter({ hasText: subscriptionName })}); } - public async getSubscriptioPrimarynKey(subscriptionName: string): Promise { - var subscriptionRow = await this.getSubscriptionRow(subscriptionName); + public async getSubscriptioPrimarynKey(subscriptionName: string): Promise { + var subscriptionRow = this.getSubscriptionRow(subscriptionName); const primaryKeyElement = subscriptionRow.locator('code[data-bind="text: primaryKey"]').first(); return await primaryKeyElement.textContent(); } - public async getSubscriptioSecondarynKey(subscriptionName: string): Promise { - var subscriptionRow = await this.getSubscriptionRow(subscriptionName); - const primaryKeyElement = subscriptionRow.locator('code[data-bind="text: secondaryKey"]').first(); + public async getSubscriptioSecondarynKey(subscriptionName: string): Promise { + var subscriptionRow = this.getSubscriptionRow(subscriptionName); + const primaryKeyElement = subscriptionRow?.locator('code[data-bind="text: secondaryKey"]').first(); return await primaryKeyElement.textContent(); } public async togglePrimarySubscriptionKey(subscriptionName: string): Promise { - var subscriptionRow = await this.getSubscriptionRow(subscriptionName); - await subscriptionRow.locator("a.btn-link[aria-label='Show primary key']", { hasText: "Show" }).click(); + var subscriptionRow = this.getSubscriptionRow(subscriptionName); + await subscriptionRow?.locator("a.btn-link[aria-label='Show primary key']", { hasText: "Show" }).click(); } public async toggleSecondarySubscriptionKey(subscriptionName: string): Promise { - var subscriptionRow = await this.getSubscriptionRow(subscriptionName); - await subscriptionRow.locator("a.btn-link[aria-label='Show Secondary key']", { hasText: "Show" }).click(); + var subscriptionRow = this.getSubscriptionRow(subscriptionName); + await subscriptionRow?.locator("a.btn-link[aria-label='Show Secondary key']", { hasText: "Show" }).click(); } public async getListOfLocatorsToHide(): Promise { @@ -72,15 +72,15 @@ export class ProfileWidget { const productNames = this.page.locator('span[data-bind="text: model.productName"]'); const subscriptionNames = this.page.locator('span[data-bind="text: model.name"]'); const subscriptionStartDates = this.page.locator('span[data-bind="text: $parent.timeToString(model.startDate)"]'); - return primaryKeyElements.concat(secondaryKeyElements).concat(productNames).concat(await this.getUserProfileData()).concat(subscriptionNames).concat(subscriptionStartDates); + return primaryKeyElements.concat(secondaryKeyElements).concat(productNames).concat(this.getUserProfileData()).concat(subscriptionNames).concat(subscriptionStartDates); } - public async getUserProfileData(): Promise { + public getUserProfileData(): Locator[] { return [ - await this.getUserEmailLocator(), - await this.getUserFirstNameLocator(), - await this.getUserLastNameLocator(), - await this.getUserRegistrationDataLocator() + this.getUserEmailLocator(), + this.getUserFirstNameLocator(), + this.getUserLastNameLocator(), + this.getUserRegistrationDataLocator() ]; } } \ No newline at end of file diff --git a/tests/e2e/maps/signin-basic.ts b/tests/e2e/maps/signin-basic.ts index 315233b0b..0ae6368cc 100644 --- a/tests/e2e/maps/signin-basic.ts +++ b/tests/e2e/maps/signin-basic.ts @@ -5,11 +5,10 @@ export class SignInBasicWidget { constructor(private readonly page: Page, private readonly configuration: object) { } public async signInWithBasic(userInfo: User): Promise { - await this.page.goto(this.configuration['urls']['signin']); + await this.page.goto(this.configuration['urls']['signin'], { waitUntil: 'domcontentloaded' }); await this.page.type("#email", userInfo.email); await this.page.type("#password", userInfo.password); await this.page.click("#signin"); - await this.page.waitForNavigation({ waitUntil: "domcontentloaded" }); } } \ No newline at end of file diff --git a/tests/e2e/runtime/apis.spec.ts b/tests/e2e/runtime/apis.spec.ts index a67af983d..8ab587d2f 100644 --- a/tests/e2e/runtime/apis.spec.ts +++ b/tests/e2e/runtime/apis.spec.ts @@ -22,7 +22,7 @@ test.describe("apis-page", async () => { } async function validate(){ - await page.goto(configuration['urls']['apis']); + await page.goto(configuration['urls']['apis'], { waitUntil: 'domcontentloaded' }); const apiWidget = new ApisWidget(page); await apiWidget.waitRuntimeInit(); diff --git a/tests/e2e/runtime/products.spec.ts b/tests/e2e/runtime/products.spec.ts index 5fdcaa787..2d8b4bc53 100644 --- a/tests/e2e/runtime/products.spec.ts +++ b/tests/e2e/runtime/products.spec.ts @@ -20,7 +20,7 @@ test.describe("products-page", async () => { } async function validate(){ - await page.goto(configuration['urls']['products']); + await page.goto(configuration['urls']['products'], { waitUntil: 'domcontentloaded' }); const productWidget = new ProductseWidget(page); await productWidget.waitRuntimeInit(); diff --git a/tests/e2e/runtime/user-subscriptions.spec.ts b/tests/e2e/runtime/user-subscriptions.spec.ts index 0b91570e5..c120a493c 100644 --- a/tests/e2e/runtime/user-subscriptions.spec.ts +++ b/tests/e2e/runtime/user-subscriptions.spec.ts @@ -1,6 +1,7 @@ import { test, expect } from "../playwright-test"; import { SignInBasicWidget } from "../maps/signin-basic"; import { ProfileWidget } from "../maps/profile"; +import { HomePageWidget } from "../maps/home"; import { ProductseWidget } from "../maps/products"; import { User } from "../../mocks/collection/user"; import { Subscription } from "../../mocks/collection/subscription"; @@ -31,13 +32,14 @@ test.describe("user-resources", async () => { const signInWidget = new SignInBasicWidget(page, configuration); const profileWidget = new ProfileWidget(page); const productsWidget = new ProductseWidget(page); + const homePageWidget = new HomePageWidget(page); //sign in await signInWidget.signInWithBasic(userInfo); - expect(page.url()).toBe(configuration['urls']['home']); + await homePageWidget.waitRuntimeInit(); // subscribe to product - await page.goto(configuration['urls']['products']+"/"+product1.productId); + await page.goto(configuration['urls']['products']+"/"+product1.productId, { waitUntil: 'domcontentloaded' }); await productsWidget.subscribeToProduct(configuration['root'], product1.productId, subscription.displayName); await profileWidget.waitRuntimeInit(); diff --git a/tests/e2e/runtime/user.spec.ts b/tests/e2e/runtime/user.spec.ts index 4fcec3f1b..e89c41086 100644 --- a/tests/e2e/runtime/user.spec.ts +++ b/tests/e2e/runtime/user.spec.ts @@ -1,6 +1,7 @@ import { test, expect } from "../playwright-test"; import { SignInBasicWidget } from "../maps/signin-basic"; import { ProfileWidget } from "../maps/profile"; +import { HomePageWidget } from "../maps/home"; import { SignupBasicWidget } from "../maps/signup-basic"; import { User } from "../../mocks/collection/user"; import { Templating } from "../../templating"; @@ -17,8 +18,10 @@ test.describe("user-sign-in", async () => { async function validate(){ const signInWidget = new SignInBasicWidget(page, configuration); + const homePageWidget = new HomePageWidget(page); + await signInWidget.signInWithBasic(userInfo); - expect(page.url()).toBe(configuration['urls']['home']); + await homePageWidget.waitRuntimeInit(); await page.close(); } @@ -37,10 +40,12 @@ test.describe("user-sign-in", async () => { async function validate(){ const signInWidget = new SignInBasicWidget(page, configuration); + const homePageWidget = new HomePageWidget(page); + await signInWidget.signInWithBasic(userInfo); - expect(page.url()).toBe(configuration['urls']['home']); + await homePageWidget.waitRuntimeInit(); - await page.goto(configuration['urls']['profile']); + await page.goto(configuration['urls']['profile'], { waitUntil: 'domcontentloaded' }); const profileWidget = new ProfileWidget(page); await profileWidget.waitRuntimeInit(); From 996fa56b950b877cb1b620fb70860a8435a7de36 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Tue, 25 Jul 2023 15:43:01 +0200 Subject: [PATCH 13/15] address PR comments --- package.json | 2 -- tests/e2e/maps/signin-social.ts | 24 ------------------ tests/e2e/playwright-test.ts | 22 ++++++++-------- .../self-hosted/user-resources-win32.jpeg | Bin 41066 -> 40029 bytes tests/mapiClient.ts | 6 ++--- tests/mocks/collection/api.ts | 4 +-- tests/mocks/collection/product.ts | 4 +-- tests/mocks/collection/subscription.ts | 16 ++++++------ tests/mocks/collection/user.ts | 14 +++++----- .../{IApiService.ts => ITestApiService.ts} | 2 +- ...oductService.ts => ITestProductService.ts} | 2 +- .../{IUserService.ts => ITestUserService.ts} | 2 +- .../{apiService.ts => testApiService.ts} | 6 ++--- ...roductService.ts => testProductService.ts} | 4 +-- tests/services/testRunnerMock.ts | 4 +-- .../{userService.ts => testUserService.ts} | 4 +-- tests/{utils.ts => testUtils.ts} | 4 +-- 17 files changed, 47 insertions(+), 73 deletions(-) delete mode 100644 tests/e2e/maps/signin-social.ts rename tests/services/{IApiService.ts => ITestApiService.ts} (87%) rename tests/services/{IProductService.ts => ITestProductService.ts} (88%) rename tests/services/{IUserService.ts => ITestUserService.ts} (85%) rename tests/services/{apiService.ts => testApiService.ts} (89%) rename tests/services/{productService.ts => testProductService.ts} (91%) rename tests/services/{userService.ts => testUserService.ts} (89%) rename tests/{utils.ts => testUtils.ts} (98%) diff --git a/package.json b/package.json index 3c458c73e..b62c2320d 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,6 @@ "@types/mime": "^3.0.1", "@types/mocha": "10.0.1", "@types/node": "^20.3.1", - "@types/puppeteer": "5.4.7", "@typescript-eslint/eslint-plugin": "^5.60.0", "@typescript-eslint/parser": "^5.60.0", "autoprefixer": "^10.4.14", @@ -53,7 +52,6 @@ "mocha": "^10.2.0", "path": "^0.12.7", "postcss-loader": "^7.3.3", - "puppeteer": "19.7.5", "querystring-es3": "^0.2.1", "raw-loader": "^4.0.2", "sass": "^1.63.6", diff --git a/tests/e2e/maps/signin-social.ts b/tests/e2e/maps/signin-social.ts deleted file mode 100644 index 2dff54add..000000000 --- a/tests/e2e/maps/signin-social.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Page } from "puppeteer"; - -export class SignInSocialWidget { - constructor(private readonly page: Page) { } - - public async signInWitAadB2C(): Promise { - await this.page.click("#signinB2C"); - - await new Promise((resolve) => { - this.page.once("popup", async (popup) => { - await popup.waitForSelector("[type=email]"); - await popup.type("[type=email]", "foo@bar.com"); - await popup.type("[type=password]", "password"); - await popup.click("#next"); - - popup.on("close", () => resolve()); - - await new Promise(resolve => setTimeout(resolve, 50000000)); // just long wait - }); - }); - - await this.page.waitForNavigation({ waitUntil: "domcontentloaded" }); - } -} \ No newline at end of file diff --git a/tests/e2e/playwright-test.ts b/tests/e2e/playwright-test.ts index f97a92b7d..ebc79997a 100644 --- a/tests/e2e/playwright-test.ts +++ b/tests/e2e/playwright-test.ts @@ -1,22 +1,22 @@ import { test as base } from '@playwright/test'; -import { Utils } from '../utils'; -import { ApiService } from '../services/apiService'; -import { UserService } from '../services/userService'; -import { ProductService } from '../services/productService'; +import { TestUtils } from '../testUtils'; +import { TestApiService } from '../services/testApiService'; +import { TestUserService } from '../services/testUserService'; +import { TestProductService } from '../services/testProductService'; import { ITestRunner } from '../services/ITestRunner'; import { TestRunnerMock } from '../services/testRunnerMock'; import { TestRunner } from '../services/testRunner'; -let configurationTest = base.extend<{}, { configuration: Object, cleanUp: Array, apiService: ApiService, userService: UserService, productService: ProductService, testRunner: ITestRunner }>({ +let configurationTest = base.extend<{}, { configuration: Object, cleanUp: Array, apiService: TestApiService, userService: TestUserService, productService: TestProductService, testRunner: ITestRunner }>({ configuration: [async ({}, use) => { let configuration = {}; - configuration = await Utils.getConfigAsync(); + configuration = await TestUtils.getConfigAsync(); await use(configuration); }, { scope: 'worker' }], testRunner: [async ({}, use) => { let testRunner: ITestRunner; - if (!(await Utils.IsLocalEnv())){ + if (!(await TestUtils.IsLocalEnv())){ testRunner = new TestRunner(); }else{ testRunner = new TestRunnerMock(); @@ -25,17 +25,17 @@ let configurationTest = base.extend<{}, { configuration: Object, cleanUp: Array< }, { scope: 'worker' }], apiService: [async ({}, use) => { - let apiService = new ApiService(); + let apiService = new TestApiService(); await use(apiService); }, { scope: 'worker' }], productService: [async ({}, use) => { - let productService = new ProductService(); + let productService = new TestProductService(); await use(productService); }, { scope: 'worker' }], userService: [async ({}, use) => { - let userService = new UserService(); + let userService = new TestUserService(); await use(userService); }, { scope: 'worker' }], @@ -58,7 +58,7 @@ let configurationTest = base.extend<{}, { configuration: Object, cleanUp: Array< export const test = configurationTest.extend({ mockedData: async ({ }, use, testInfo) => { let testTitle = `${testInfo.titlePath[1]}-${testInfo.titlePath[2]}`; - var dataToUse = Utils.getTestData(testTitle); + var dataToUse = TestUtils.getTestData(testTitle); let mockedData = {}; mockedData["data"] = dataToUse; mockedData["testName"] = testTitle; diff --git a/tests/e2e/runtime/user-subscriptions.spec.ts-snapshots/self-hosted/user-resources-win32.jpeg b/tests/e2e/runtime/user-subscriptions.spec.ts-snapshots/self-hosted/user-resources-win32.jpeg index 30a197ed00a0a980afa7692c05b30df2b8719bdf..6ee9f2a3852ec43503f797c27833bc7cbce12f6b 100644 GIT binary patch literal 40029 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 literal 41066 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 diff --git a/tests/mapiClient.ts b/tests/mapiClient.ts index 94629015f..4d6a12389 100644 --- a/tests/mapiClient.ts +++ b/tests/mapiClient.ts @@ -1,5 +1,5 @@ import * as Constants from "../src/constants"; -import { Utils } from "./utils"; +import { TestUtils } from "./testUtils"; import { HttpClient, HttpRequest, HttpResponse, HttpMethod, HttpHeader } from "@paperbits/common/http"; import { XmlHttpRequestClient } from "@paperbits/common/http"; import { KnownHttpHeaders } from "../src/models/knownHttpHeaders"; @@ -16,7 +16,7 @@ export class MapiClient { } private async initialize(): Promise { - const settings = await Utils.getConfigAsync(); + const settings = await TestUtils.getConfigAsync(); this.token = settings["accessToken"]; this.managementApiUrl = settings["managementUrl"]; } @@ -56,7 +56,7 @@ export class MapiClient { protected async makeRequest(httpRequest: HttpRequest): Promise { const url = new URL(httpRequest.url); if (!url.searchParams.has("api-version")) { - httpRequest.url = Utils.addQueryParameter(httpRequest.url, `api-version=${Constants.managementApiVersion}`); + httpRequest.url = TestUtils.addQueryParameter(httpRequest.url, `api-version=${Constants.managementApiVersion}`); } let response: HttpResponse; diff --git a/tests/mocks/collection/api.ts b/tests/mocks/collection/api.ts index 5890ca17a..88ecacce2 100644 --- a/tests/mocks/collection/api.ts +++ b/tests/mocks/collection/api.ts @@ -1,4 +1,4 @@ -import { Utils } from "../../utils"; +import { TestUtils } from "../../testUtils"; import { ApiContract } from "../../../src/contracts/api"; import { Resource } from "./resource"; @@ -45,6 +45,6 @@ export class Api extends Resource{ } public static getRandomApi(testId: string){ - return new Api(testId, Utils.randomIdentifier(), Utils.randomIdentifier(), Utils.randomIdentifier()); + return new Api(testId, TestUtils.randomIdentifier(), TestUtils.randomIdentifier(), TestUtils.randomIdentifier()); } } diff --git a/tests/mocks/collection/product.ts b/tests/mocks/collection/product.ts index 9aba8517f..1ace6c4d2 100644 --- a/tests/mocks/collection/product.ts +++ b/tests/mocks/collection/product.ts @@ -1,5 +1,5 @@ import { ProductContract } from "../../../src/contracts/product"; -import { Utils } from "../../utils"; +import { TestUtils } from "../../testUtils"; import { Resource } from "./resource"; export class Product extends Resource{ @@ -46,7 +46,7 @@ export class Product extends Resource{ } public static getRandomProduct(testId: string){ - var productName = Utils.randomIdentifier(); + var productName = TestUtils.randomIdentifier(); return new Product(testId, productName, productName); } } diff --git a/tests/mocks/collection/subscription.ts b/tests/mocks/collection/subscription.ts index 7a49ad792..c51bb463c 100644 --- a/tests/mocks/collection/subscription.ts +++ b/tests/mocks/collection/subscription.ts @@ -1,5 +1,5 @@ import { SubscriptionContract } from "../../../src/contracts/subscription"; -import { Utils } from "../../utils"; +import { TestUtils } from "../../testUtils"; import { Resource } from "./resource"; import { User } from "./user"; import { Product } from "./product"; @@ -52,20 +52,20 @@ export class Subscription extends Resource{ endDate: new Date().toISOString(), expirationDate: new Date().toISOString(), notificationDate: new Date().toISOString(), - primaryKey: Utils.randomIdentifier(10), - scope: Utils.randomIdentifier(5), - secondaryKey: Utils.randomIdentifier(10), + primaryKey: TestUtils.randomIdentifier(10), + scope: TestUtils.randomIdentifier(5), + secondaryKey: TestUtils.randomIdentifier(10), startDate: new Date().toISOString(), state: "active", - stateComment: Utils.randomIdentifier(10), - ownerId: Utils.randomIdentifier(5) + stateComment: TestUtils.randomIdentifier(10), + ownerId: TestUtils.randomIdentifier(5) } }; } public static getRandomSubscription(testId: string, user: User, product: Product){ - var displayName = Utils.randomIdentifier(5); - var id = Utils.randomIdentifier(5); + var displayName = TestUtils.randomIdentifier(5); + var id = TestUtils.randomIdentifier(5); return new Subscription(testId, user, product, id, displayName); } } diff --git a/tests/mocks/collection/user.ts b/tests/mocks/collection/user.ts index dbba5fd3c..d28749ef3 100644 --- a/tests/mocks/collection/user.ts +++ b/tests/mocks/collection/user.ts @@ -1,5 +1,5 @@ import { UserContract } from "../../../src/contracts/user"; -import { Utils } from "../../utils"; +import { TestUtils } from "../../testUtils"; import { Resource } from "./resource"; export class User extends Resource{ @@ -19,7 +19,7 @@ export class User extends Resource{ this.firstName = firstName; this.lastName = lastName; this.password = password; - this.accessToken = Utils.getSharedAccessToken(this.publicId, Utils.randomIdentifier(), 1); + this.accessToken = TestUtils.getSharedAccessToken(this.publicId, TestUtils.randomIdentifier(), 1); this.responseContract = this.getResponseContract(); } @@ -64,11 +64,11 @@ export class User extends Resource{ } public static getRandomUser(testId: string){ - var email = `${Utils.randomIdentifier(4, false)}@${Utils.randomIdentifier(4, false)}.${Utils.randomIdentifier(4, false)}`; - var publicId = `${Utils.randomIdentifier(3)}-${Utils.randomIdentifier(3)}-${Utils.randomIdentifier(3)}`; - var firstName = Utils.randomIdentifier(3); - var lastName = Utils.randomIdentifier(3); - var password = Utils.randomIdentifier(10); + var email = `${TestUtils.randomIdentifier(4, false)}@${TestUtils.randomIdentifier(4, false)}.${TestUtils.randomIdentifier(4, false)}`; + var publicId = `${TestUtils.randomIdentifier(3)}-${TestUtils.randomIdentifier(3)}-${TestUtils.randomIdentifier(3)}`; + var firstName = TestUtils.randomIdentifier(3); + var lastName = TestUtils.randomIdentifier(3); + var password = TestUtils.randomIdentifier(10); return new User(testId, email, publicId, firstName, lastName, password); } } diff --git a/tests/services/IApiService.ts b/tests/services/ITestApiService.ts similarity index 87% rename from tests/services/IApiService.ts rename to tests/services/ITestApiService.ts index e7b118a2e..f03a35213 100644 --- a/tests/services/IApiService.ts +++ b/tests/services/ITestApiService.ts @@ -1,5 +1,5 @@ import { ApiContract } from "../../src/contracts/api"; -export interface IApiService { +export interface ITestApiService { putApi(apiId: string, apiContract: ApiContract): Promise ; diff --git a/tests/services/IProductService.ts b/tests/services/ITestProductService.ts similarity index 88% rename from tests/services/IProductService.ts rename to tests/services/ITestProductService.ts index 867e7e181..4ceacc76d 100644 --- a/tests/services/IProductService.ts +++ b/tests/services/ITestProductService.ts @@ -1,5 +1,5 @@ import { ProductContract } from "../../src/contracts/product"; -export interface IProductService { +export interface ITestProductService { putProduct(productId: string, productContract: ProductContract): Promise; diff --git a/tests/services/IUserService.ts b/tests/services/ITestUserService.ts similarity index 85% rename from tests/services/IUserService.ts rename to tests/services/ITestUserService.ts index 5182488fa..a6d2a5d02 100644 --- a/tests/services/IUserService.ts +++ b/tests/services/ITestUserService.ts @@ -1,5 +1,5 @@ import { UserContract } from "../../src/contracts/user"; -export interface IUserService { +export interface ITestUserService { putUser(userId: string, userContract: UserContract): Promise ; diff --git a/tests/services/apiService.ts b/tests/services/testApiService.ts similarity index 89% rename from tests/services/apiService.ts rename to tests/services/testApiService.ts index 9cf2008c3..e697de8e1 100644 --- a/tests/services/apiService.ts +++ b/tests/services/testApiService.ts @@ -1,8 +1,8 @@ import { ApiContract } from "../../src/contracts/api"; import { MapiClient } from "../mapiClient"; -import { IApiService } from "./IApiService"; +import { ITestApiService } from "./ITestApiService"; -export class ApiService implements IApiService { +export class TestApiService implements ITestApiService { private readonly mapiClient: MapiClient constructor() { this.mapiClient = MapiClient.Instance; @@ -13,7 +13,7 @@ export class ApiService implements IApiService { throw new Error(`Parameter "apiId" not specified.`); } - const contract = await this.mapiClient.put(apiId, null, apiContract); + const contract = await this.mapiClient.put(apiId, [], apiContract); return contract; } diff --git a/tests/services/productService.ts b/tests/services/testProductService.ts similarity index 91% rename from tests/services/productService.ts rename to tests/services/testProductService.ts index a3fb1b64e..c25005ea2 100644 --- a/tests/services/productService.ts +++ b/tests/services/testProductService.ts @@ -1,8 +1,8 @@ import { MapiClient } from "../mapiClient"; import { ProductContract } from "../../src/contracts/product"; -import { IProductService } from "./IProductService"; +import { ITestProductService } from "./ITestProductService"; -export class ProductService implements IProductService { +export class TestProductService implements ITestProductService { private readonly mapiClient: MapiClient constructor() { this.mapiClient = MapiClient.Instance; diff --git a/tests/services/testRunnerMock.ts b/tests/services/testRunnerMock.ts index ef31ee85b..f37d09473 100644 --- a/tests/services/testRunnerMock.ts +++ b/tests/services/testRunnerMock.ts @@ -1,11 +1,11 @@ -import { Utils } from "../utils"; +import { TestUtils } from "../testUtils"; import { ITestRunner } from "./ITestRunner"; export class TestRunnerMock implements ITestRunner { public runTest(validate: () => Promise, populateData: () => Promise, data: object ): Promise { return new Promise(async (resolve, reject) => { - let server = Utils.createMockServer(data); + let server = TestUtils.createMockServer(data); let error = undefined; server.on("ready", () => { validate().then(() => { diff --git a/tests/services/userService.ts b/tests/services/testUserService.ts similarity index 89% rename from tests/services/userService.ts rename to tests/services/testUserService.ts index 466b5703f..a936180a5 100644 --- a/tests/services/userService.ts +++ b/tests/services/testUserService.ts @@ -1,8 +1,8 @@ import { MapiClient } from "../mapiClient"; import { UserContract } from "../../src/contracts/user"; -import { IUserService } from "./IUserService"; +import { ITestUserService } from "./ITestUserService"; -export class UserService implements IUserService { +export class TestUserService implements ITestUserService { private readonly mapiClient: MapiClient constructor() { this.mapiClient = MapiClient.Instance; diff --git a/tests/utils.ts b/tests/testUtils.ts similarity index 98% rename from tests/utils.ts rename to tests/testUtils.ts index b340da857..dd16506b7 100644 --- a/tests/utils.ts +++ b/tests/testUtils.ts @@ -2,7 +2,7 @@ import * as fs from "fs"; import * as crypto from "crypto"; import * as http from "http"; -export class Utils { +export class TestUtils { public static async getConfigAsync(): Promise { const configFile = await fs.promises.readFile("./src/config.validate.json", { encoding: "utf-8" }); const validationConfig = JSON.parse(configFile); @@ -23,7 +23,7 @@ export class Utils { } public static async IsLocalEnv(): Promise { - let config = await Utils.getConfigAsync(); + let config = await TestUtils.getConfigAsync(); return config["isLocalRun"] === true; } From 9363885ec78690cd7b46534821a686d03f059bf5 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Tue, 25 Jul 2023 16:07:57 +0200 Subject: [PATCH 14/15] Add retries for the e2e test --- playwright.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/playwright.config.ts b/playwright.config.ts index 9ba55ba08..13a794e7d 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -3,6 +3,7 @@ import { defineConfig } from '@playwright/test'; export default defineConfig({ testIgnore: 'playwright/*', //timeout: 50_000, + retries: 2, use: { video: 'retain-on-failure' } From babd9539524bfeb33474dfe1a37db6d9dc99c674 Mon Sep 17 00:00:00 2001 From: Farhad Alizada Date: Sat, 5 Aug 2023 16:42:55 +0200 Subject: [PATCH 15/15] Address PR comments --- playwright.config.ts | 2 -- src/services/usersService.ts | 1 - 2 files changed, 3 deletions(-) diff --git a/playwright.config.ts b/playwright.config.ts index 13a794e7d..60cb5ee52 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,8 +1,6 @@ import { defineConfig } from '@playwright/test'; export default defineConfig({ - testIgnore: 'playwright/*', - //timeout: 50_000, retries: 2, use: { video: 'retain-on-failure' diff --git a/src/services/usersService.ts b/src/services/usersService.ts index e5971e576..3c2cca2fd 100644 --- a/src/services/usersService.ts +++ b/src/services/usersService.ts @@ -255,7 +255,6 @@ export class UsersService { */ public async ensureSignedIn(): Promise { const userId = await this.getCurrentUserId(); - if (!userId) { this.navigateToSignin(); return; // intentionally exiting without resolving the promise.