web: merge branch 'web/wdio-2' into application-wizard-2-with-api-and-tests

* web/wdio-2:
  web/test: changed the name of one test to reflect it's 'good' status
  web/adding tests: added comments and cleaned up some administrative features.
  web/add webdriverIO testing layer
This commit is contained in:
Ken Sternberg 2023-09-25 15:20:32 -07:00
commit 6c7e30dc7d
26 changed files with 9534 additions and 6969 deletions

4
tests/wdio/.eslintignore Normal file
View File

@ -0,0 +1,4 @@
# don't ever lint node_modules
node_modules
# don't lint nyc coverage output
coverage

20
tests/wdio/.eslintrc.json Normal file
View File

@ -0,0 +1,20 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"rules": {
"indent": "off",
"linebreak-style": ["error", "unix"],
"quotes": ["error", "double", { "avoidEscape": true }],
"semi": ["error", "always"],
"@typescript-eslint/ban-ts-comment": "off"
}
}

View File

@ -0,0 +1,26 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:sonarjs/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": ["@typescript-eslint", "sonarjs"],
"rules": {
"indent": "off",
"linebreak-style": ["error", "unix"],
"quotes": ["error", "double", { "avoidEscape": true }],
"semi": ["error", "always"],
"@typescript-eslint/ban-ts-comment": "off",
"sonarjs/cognitive-complexity": ["error", 9],
"sonarjs/no-nested-template-literals": "off"
}
}

View File

@ -0,0 +1,4 @@
# don't ever lint node_modules
node_modules
# don't lint nyc coverage output
coverage

View File

@ -0,0 +1,22 @@
{
"arrowParens": "always",
"bracketSpacing": true,
"embeddedLanguageFormatting": "auto",
"htmlWhitespaceSensitivity": "css",
"insertPragma": false,
"jsxSingleQuote": false,
"printWidth": 100,
"proseWrap": "preserve",
"quoteProps": "consistent",
"requirePragma": false,
"semi": true,
"singleQuote": false,
"tabWidth": 4,
"trailingComma": "all",
"useTabs": false,
"vueIndentScriptAndStyle": false,
"plugins": ["@trivago/prettier-plugin-sort-imports"],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,
"importOrderParserPlugins": ["typescript", "classProperties", "decorators-legacy"]
}

37
tests/wdio/Makefile Normal file
View File

@ -0,0 +1,37 @@
.PHONY: help precommit admin-user test-good-login test-bad-login
help: ## Show this help
@echo "\nSpecify a command. The choices are:\n"
@grep -E '^[0-9a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*?## "}; {printf " \033[0;36m%-20s\033[m %s\n", $$1, $$2}'
@echo ""
ROOT := $(shell git rev-parse --show-toplevel 2> /dev/null)
WDIO = npm run wdio
SPEC = $(WDIO) -- --spec ./test/specs
LOCAL_BLUEPRINTS=$(ROOT)/blueprints/local
node_modules: ## Runs `npm install` to prepare this feature
npm ci
precommit: node_modules ## Run the precommit: spell check all comments, eslint with sonarJS, prettier-write
npm run precommit
# Actual tests are down below:
$(ROOT)/blueprints/local/admin-user.yaml:
mkdir -p $(LOCAL_BLUEPRINTS)
cp ./blueprints/admin-user.yaml $(LOCAL_BLUEPRINTS)
cd $(ROOT) && ak apply_blueprint local/admin-user.yaml
admin-user: $(ROOT)/blueprints/local/admin-user.yaml
test-good-login: node_modules admin-user ## Test that we can log into the server. Requires a running instance of the server.
$(SPEC)/good-login.ts
test-bad-login: node_modules admin-user ## Test that bad usernames and passwords create appropriate error messages
$(SPEC)/bad-logins.ts

View File

@ -0,0 +1,18 @@
context: {}
entries:
- attrs:
attributes: {}
email: test-admin@goauthentik.io
is_active: true
name: authentik Default Admin
password: test-runner
path: users
type: internal
groups:
- !Find [authentik_core.group, [name, "authentik Admins"]]
conditions: []
id: null
identifiers:
username: akadmin
model: authentik_core.user
state: present

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +1,29 @@
{ {
"name": "my-new-project", "name": "my-new-project",
"type": "module", "type": "module",
"devDependencies": { "devDependencies": {
"@wdio/cli": "^8.16.11", "@trivago/prettier-plugin-sort-imports": "^4.2.0",
"@wdio/local-runner": "^8.16.11", "@typescript-eslint/eslint-plugin": "^6.7.2",
"@wdio/mocha-framework": "^8.16.11", "@typescript-eslint/parser": "^6.7.2",
"@wdio/spec-reporter": "^8.16.9", "@wdio/cli": "^8.16.11",
"ts-node": "^10.9.1", "@wdio/local-runner": "^8.16.11",
"typescript": "^5.2.2", "@wdio/mocha-framework": "^8.16.11",
"wdio-wait-for": "^3.0.7" "@wdio/spec-reporter": "^8.16.9",
}, "eslint": "^8.49.0",
"scripts": { "eslint-config-google": "^0.14.0",
"wdio": "wdio run ./wdio.conf.ts" "eslint-plugin-sonarjs": "^0.21.0",
} "npm-run-all": "^4.1.5",
"prettier": "^3.0.3",
"ts-node": "^10.9.1",
"typescript": "^5.2.2",
"wdio-wait-for": "^3.0.7"
},
"scripts": {
"wdio": "wdio run ./wdio.conf.ts",
"lint:precommit": "eslint --max-warnings 0 --config ./.eslintrc.precommit.json $(git status --porcelain . | grep '^[AM?][M?]' | cut -d'/' -f3- | grep -E '\\.(ts|js|tsx|jsx)$')",
"lint:spelling": "codespell -D - -D $(git rev-parse --show-toplevel 2> /dev/null)/.github/codespell-dictionary.txt -I $(git rev-parse --show-toplevel 2> /dev/null)/.github/codespell-words.txt ./test -s",
"precommit": "run-s lint:precommit lint:spelling prettier",
"prettier-check": "prettier --check .",
"prettier": "prettier --write ."
}
} }

View File

@ -1,4 +1,3 @@
import { $ } from "@wdio/globals";
import AdminPage from "./admin.page.js"; import AdminPage from "./admin.page.js";
/** /**

View File

@ -1,5 +1,5 @@
import { $ } from "@wdio/globals";
import AdminPage from "./admin.page.js"; import AdminPage from "./admin.page.js";
import { $ } from "@wdio/globals";
/** /**
* sub page containing specific selectors and methods for a specific page * sub page containing specific selectors and methods for a specific page
@ -9,9 +9,14 @@ class ApplicationsListPage extends AdminPage {
* define selectors using getter methods * define selectors using getter methods
*/ */
async startCreateApplicationWizard() { get startWizardButton() {
await $('>>>ak-wizard-frame button[slot="trigger"]').click(); return $('>>>ak-wizard-frame button[slot="trigger"]');
} }
async open() {
return await super.open("if/admin/#/core/applications");
}
} }
export default new ApplicationsListPage(); export default new ApplicationsListPage();

View File

@ -1,13 +1,13 @@
import { $ } from "@wdio/globals";
import Page from "./page.js"; import Page from "./page.js";
import UserLibraryPage from "./user-library.page.js"; import UserLibraryPage from "./user-library.page.js";
import { $ } from "@wdio/globals";
/** /**
* sub page containing specific selectors and methods for a specific page * sub page containing specific selectors and methods for a specific page
*/ */
class LoginPage extends Page { class LoginPage extends Page {
/** /**
* define selectors using getter methods * Selectors
*/ */
get inputUsername() { get inputUsername() {
return $('>>>input[name="uidField"]'); return $('>>>input[name="uidField"]');
@ -26,20 +26,20 @@ class LoginPage extends Page {
} }
/** /**
* a method to encapsule automation code to interact with the page * Specific interactions
* e.g. to login using username and password
*/ */
async username(username: string) { async username(username: string) {
await this.inputPassword.isDisplayed(); await this.inputUsername.waitForClickable();
await this.inputUsername.setValue(username); await this.inputUsername.setValue(username);
await this.btnSubmit.isEnabled(); await this.btnSubmit.waitForEnabled();
await this.btnSubmit.click(); await this.btnSubmit.click();
} }
async password(password: string) { async password(password: string) {
await this.inputPassword.isDisplayed(); await this.inputPassword.waitForClickable();
await this.inputPassword.setValue(password); await this.inputPassword.setValue(password);
await this.btnSubmit.isEnabled(); await this.btnSubmit.waitForEnabled();
await this.btnSubmit.click(); await this.btnSubmit.click();
} }
@ -53,7 +53,7 @@ class LoginPage extends Page {
} }
/** /**
* overwrite specific options to adapt it to page object * URL for accessing this page (if necessary)
*/ */
open() { open() {
return super.open(""); return super.open("");

View File

@ -5,7 +5,7 @@ export class OauthForm extends Page {
await this.searchSelect( await this.searchSelect(
'>>>ak-flow-search[name="authorizationFlow"] input[type="text"]', '>>>ak-flow-search[name="authorizationFlow"] input[type="text"]',
"authorizationFlow", "authorizationFlow",
`button*=${selector}` `button*=${selector}`,
); );
} }
} }

View File

@ -3,8 +3,8 @@ import { browser } from "@wdio/globals";
const CLICK_TIME_DELAY = 250; const CLICK_TIME_DELAY = 250;
/** /**
* main page object containing all methods, selectors and functionality * Main page object containing all methods, selectors and functionality that is shared across all
* that is shared across all page objects * page objects
*/ */
export default class Page { export default class Page {
/** /**
@ -22,6 +22,15 @@ export default class Page {
return browser.pause(CLICK_TIME_DELAY); return browser.pause(CLICK_TIME_DELAY);
} }
/**
* Target a specific entry in SearchSelect. Requires that the SearchSelect have the `name`
* attribute set, so that the managed selector can find the *right* SearchSelect if there are
* multiple open SearchSelects on the board. See `./ldap-form.view:LdapForm.setBindFlow` for an
* example, and see `./oauth-form.view:OauthForm:setAuthorizationFlow` for a further example of
* why it would be hard to simplify this further (`flow` vs `tentanted-flow` vs a straight-up
* SearchSelect each have different a `searchSelector`).
*/
async searchSelect(searchSelector: string, managedSelector: string, buttonSelector: string) { async searchSelect(searchSelector: string, managedSelector: string, buttonSelector: string) {
const inputBind = await $(searchSelector); const inputBind = await $(searchSelector);
await inputBind.click(); await inputBind.click();

View File

@ -1 +0,0 @@
export type Constructor<T = object> = new (...args: any[]) => T;

View File

@ -1,5 +1,5 @@
import { $ } from "@wdio/globals";
import Page from "./page.js"; import Page from "./page.js";
import { $ } from "@wdio/globals";
/** /**
* sub page containing specific selectors and methods for a specific page * sub page containing specific selectors and methods for a specific page

View File

@ -1,35 +0,0 @@
import { expect } from "@wdio/globals";
import LoginPage from "../pageobjects/login.page.js";
import UserLibraryPage from "../pageobjects/user-library.page.js";
import AdminOverviewPage from "../pageobjects/admin-overview.page.js";
import ApplicationsListPage from "../pageobjects/applications-list.page.js";
import ApplicationWizard from "../pageobjects/application-wizard.page.js";
import { randomId } from "../utils/index.js";
describe("Configure new Application", () => {
it("should navigate to the wizard and configure LDAP", async () => {
const newId = randomId();
await LoginPage.open();
await LoginPage.login("ken@goauthentik.io", "eat10bugs");
expect(await UserLibraryPage.pageHeader).toHaveText("My Applications");
await UserLibraryPage.goToAdmin();
expect(await AdminOverviewPage.pageHeader).toHaveText("Welcome, ");
await AdminOverviewPage.openApplicationsListPage();
expect(await ApplicationsListPage.pageHeader).toHaveText("Applications");
ApplicationsListPage.startCreateApplicationWizard();
await ApplicationWizard.app.name.setValue(`Test application ${newId}`);
await ApplicationWizard.nextButton.click();
await (await ApplicationWizard.getProviderType("ldapprovider")).click();
await ApplicationWizard.nextButton.click();
await ApplicationWizard.ldap.setBindFlow("default-authentication-flow");
await ApplicationWizard.nextButton.click();
await expect(await ApplicationWizard.commitMessage).toHaveText(
"Your application has been saved"
);
});
});

View File

@ -1,37 +0,0 @@
import { expect } from "@wdio/globals";
import LoginPage from "../pageobjects/login.page.js";
import UserLibraryPage from "../pageobjects/user-library.page.js";
import AdminOverviewPage from "../pageobjects/admin-overview.page.js";
import ApplicationsListPage from "../pageobjects/applications-list.page.js";
import ApplicationWizard from "../pageobjects/application-wizard.page.js";
import { randomId } from "../utils/index.js";
describe("Configure new Application", () => {
it("should navigate to the wizard and configure Oauth2", async () => {
const newId = randomId();
await LoginPage.open();
await LoginPage.login("ken@goauthentik.io", "eat10bugs");
expect(await UserLibraryPage.pageHeader).toHaveText("My Applications");
await UserLibraryPage.goToAdmin();
expect(await AdminOverviewPage.pageHeader).toHaveText("Welcome, ");
await AdminOverviewPage.openApplicationsListPage();
expect(await ApplicationsListPage.pageHeader).toHaveText("Applications");
ApplicationsListPage.startCreateApplicationWizard();
await ApplicationWizard.app.name.setValue(`Test application ${newId}`);
await ApplicationWizard.nextButton.click();
await (await ApplicationWizard.getProviderType("oauth2provider")).click();
await ApplicationWizard.nextButton.click();
await ApplicationWizard.oauth.setAuthorizationFlow(
"default-provider-authorization-explicit-consent"
);
await ApplicationWizard.nextButton.click();
await expect(await ApplicationWizard.commitMessage).toHaveText(
"Your application has been saved"
);
});
});

View File

@ -1,21 +1,21 @@
import { expect } from "@wdio/globals";
import LoginPage from "../pageobjects/login.page.js"; import LoginPage from "../pageobjects/login.page.js";
import UserLibraryPage from "../pageobjects/user-library.page.js"; import { BAD_PASSWORD, BAD_USERNAME, GOOD_USERNAME } from "../utils/constants.js";
import { expect } from "@wdio/globals";
describe("Log into Authentik", () => { describe("Log into Authentik", () => {
it("should fail on a bad username", async () => { it("should fail on a bad username", async () => {
await LoginPage.open(); await LoginPage.open();
await LoginPage.username("bad-username@bad-logio.io"); await LoginPage.username(BAD_USERNAME);
const failure = await LoginPage.authFailure; const failure = await LoginPage.authFailure;
expect(failure).toHaveText("Failed to authenticate."); expect(failure).toHaveText("Failed to authenticate.");
}); });
it("should fail on a bad password", async () => { it("should fail on a bad password", async () => {
await LoginPage.open(); await LoginPage.open();
await LoginPage.username("ken@goauthentik.io"); await LoginPage.username(GOOD_USERNAME);
await LoginPage.pause(); await LoginPage.pause();
await LoginPage.password("-this-is-a-bad-password-"); await LoginPage.password(BAD_PASSWORD);
const failure = await LoginPage.authFailure; const failure = await LoginPage.authFailure;
expect(failure).toHaveText("Failed to authenticate."); expect(failure).toHaveText("Failed to authenticate.");
}); });
}); });

View File

@ -1,11 +1,5 @@
import { expect } from "@wdio/globals"; import { login } from "../utils/login.js";
import LoginPage from "../pageobjects/login.page.js";
import UserLibraryPage from "../pageobjects/user-library.page.js";
describe("Log into Authentik", () => { describe("Log into Authentik", () => {
it("should login with valid credentials", async () => { it("should login with valid credentials and reach the UserLibrary", login);
await LoginPage.open();
await LoginPage.login("ken@goauthentik.io", "eat10bugs");
await expect(UserLibraryPage.header).toHaveText("My applications");
});
}); });

View File

@ -0,0 +1,22 @@
import { login } from "../utils/login.js";
import { randomId } from "../utils/index.js";
import ApplicationsListPage from "../pageobjects/applications-list.page.js";
import ApplicationsWizardView from "../pageobjects/applications-wizard.page.js";
import ApplicationForm from "../pageobjects/application-form.view.js";
import { expect } from "@wdio/globals";
describe("Log into Authentik", () => {
it("should login with valid credentials and reach the UserLibrary", () => {
const newPrefix = randomId();
await login();
await ApplicationsListPage.open();
expect(await ApplicationsListPage.pageHeader).toHaveText("Applications");
await ApplicationsListPage.startWizardButton.click();
await ApplicationsWizardView.wizardTitle.toBeVisible();
expect(await ApplicationsWizardView.wizardTitle).toHaveText("Create Application");
await ApplicationForm.name.setValue(`New LDAP Application - ${newPrefix}`);
});

View File

@ -0,0 +1,7 @@
export const BAD_USERNAME = process.env.AK_BAD_USERNAME ?? "bad-username@bad-login.io";
export const GOOD_USERNAME = process.env.AK_GOOD_USERNAME ?? "test-admin@goauthentik.io";
export const BAD_PASSWORD = process.env.AK_BAD_PASSWORD ?? "-this-is-a-bad-password-";
export const GOOD_PASSWORD = process.env.AK_GOOD_PASSWORD ?? "test-runner"

View File

@ -1,15 +0,0 @@
export function randomId() {
let dt = new Date().getTime();
return "xxxxxxxx".replace(/x/g, (c) => {
const r = (dt + Math.random() * 16) % 16 | 0;
dt = Math.floor(dt / 16);
return (c == "x" ? r : (r & 0x3) | 0x8).toString(16);
});
}
export function convertToSlug(text: string) {
return text
.toLowerCase()
.replace(/ /g, "-")
.replace(/[^\w-]+/g, "");
}

View File

@ -0,0 +1,10 @@
import LoginPage from "../pageobjects/login.page.js";
import UserLibraryPage from "../pageobjects/user-library.page.js";
import { expect } from "@wdio/globals";
import { GOOD_PASSWORD, GOOD_USERNAME } from "./constants.js";
export const login = async () => {
await LoginPage.open();
await LoginPage.login(GOOD_USERNAME, GOOD_PASSWORD);
await expect(UserLibraryPage.pageHeader).toHaveText("My applications");
};

View File

@ -3,12 +3,7 @@
"moduleResolution": "node", "moduleResolution": "node",
"module": "ESNext", "module": "ESNext",
"target": "es2022", "target": "es2022",
"types": [ "types": ["node", "@wdio/globals/types", "expect-webdriverio", "@wdio/mocha-framework"],
"node",
"@wdio/globals/types",
"expect-webdriverio",
"@wdio/mocha-framework"
],
"skipLibCheck": true, "skipLibCheck": true,
"noEmit": true, "noEmit": true,
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
@ -19,7 +14,5 @@
"noUnusedParameters": true, "noUnusedParameters": true,
"noFallthroughCasesInSwitch": true "noFallthroughCasesInSwitch": true
}, },
"include": [ "include": ["test"]
"test"
]
} }

View File

@ -1,4 +1,5 @@
import type { Options } from "@wdio/types"; import type { Options } from "@wdio/types";
export const config: Options.Testrunner = { export const config: Options.Testrunner = {
// //
// ==================== // ====================
@ -59,7 +60,22 @@ export const config: Options.Testrunner = {
// //
capabilities: [ capabilities: [
{ {
browserName: "chrome", "browserName": "chrome",
"goog:chromeOptions": {
args: ["--disable-infobars", "--window-size=1280,800"].concat(
(function () {
return process.env.HEADLESS_CHROME === "1"
? [
"--headless",
"--no-sandbox",
"--disable-gpu",
"--disable-setuid-sandbox",
"--disable-dev-shm-usage",
]
: [];
})()
),
},
}, },
], ],