diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index ed7d253c..7062e683 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -63,3 +63,10 @@ jobs: uses: defenseunicorns/uds-common/.github/actions/save-logs@859a9b2469c8a6c24c414fe34b127ec5677aea62 # v0.4.3 with: suffix: ${{ matrix.type }}-${{ matrix.flavor }}-${{ github.run_id }}-${{ github.run_attempt }} + + - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + if: always() + with: + name: playwright-report-${{ matrix.type }}-${{ matrix.flavor }}-${{ github.run_id }}-${{ github.run_attempt }} + path: tests/.playwright/reports/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index 00998119..81511050 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,7 @@ test/tf/public-ec2-instance/.terraform terraform.tfstate terraform.tfstate.backup .terraform.lock.hcl + +# Tests +node_modules/ +.playwright/ diff --git a/tasks.yaml b/tasks.yaml index 54afc3bf..933a8fe4 100644 --- a/tasks.yaml +++ b/tasks.yaml @@ -47,8 +47,11 @@ tasks: - task: create-gl-test-bundle - task: setup:k3d-test-cluster - task: deploy:test-bundle + - task: setup:create-doug-user - task: test:health-check - task: test:ingress + - task: test:ui + - task: test:git - name: test-upgrade description: Test an upgrade from the latest released package to the current branch @@ -58,5 +61,8 @@ tasks: - task: deploy:test-bundle - task: create-gl-test-bundle - task: deploy:test-bundle + - task: setup:create-doug-user - task: test:health-check - task: test:ingress + - task: test:ui + - task: test:git diff --git a/tasks/test.yaml b/tasks/test.yaml index b4e4b90c..2d8dd306 100644 --- a/tasks/test.yaml +++ b/tasks/test.yaml @@ -1,7 +1,11 @@ +variables: + - name: GITLAB_TOKEN + default: glpat-NO_DEFAULT_VALUE + tasks: - name: health-check actions: - - description: Gitlab Exporter Health Check + - description: GitLab Exporter Health Check wait: cluster: kind: Deployment @@ -9,7 +13,7 @@ tasks: namespace: gitlab condition: Available - - description: Gitlab Registry Health Check + - description: GitLab Registry Health Check wait: cluster: kind: Deployment @@ -17,7 +21,7 @@ tasks: namespace: gitlab condition: Available - - description: Gitlab Shell Health Check + - description: GitLab Shell Health Check wait: cluster: kind: Deployment @@ -25,7 +29,7 @@ tasks: namespace: gitlab condition: Available - - description: Gitlab Toolbox Health Check + - description: GitLab Toolbox Health Check wait: cluster: kind: Deployment @@ -33,7 +37,7 @@ tasks: namespace: gitlab condition: Available - - description: Gitlab Sidekiq Health Check + - description: GitLab Sidekiq Health Check wait: cluster: kind: Deployment @@ -41,7 +45,7 @@ tasks: namespace: gitlab condition: Available - - description: Gitlab Webservice Health Check + - description: GitLab Webservice Health Check wait: cluster: kind: Deployment @@ -49,7 +53,7 @@ tasks: namespace: gitlab condition: Available - - description: Gitlab Pages Health Check + - description: GitLab Pages Health Check wait: cluster: kind: Deployment @@ -58,7 +62,7 @@ tasks: condition: Available # StatefulSets don't show conditions themselves so we look for an underlying Pod - - description: Gitlab Gitaly Health Check + - description: GitLab Gitaly Health Check wait: cluster: kind: Pod @@ -66,7 +70,7 @@ tasks: namespace: gitlab condition: Ready - - description: Gitlab Migrations Health Check + - description: GitLab Migrations Health Check wait: cluster: kind: Job @@ -76,12 +80,70 @@ tasks: - name: ingress actions: - - description: Gitlab UI Health Check - wait: - network: - protocol: https - address: gitlab.uds.dev - code: 200 + # `/-/readiness` endpoint returns 503 if any checks fail. + # When `?all=1` is specified, dependent services are also checked. + # https://docs.gitlab.com/ee/administration/monitoring/health_check.html#readiness + - description: GitLab Readiness Check + maxRetries: 30 + cmd: | + STATUS=$(curl -s -o /dev/null --write-out '%{http_code}' 'https://gitlab.uds.dev/-/readiness?all=1') + echo "GitLab readiness status: ${STATUS}" + if [ $STATUS != "200" ]; then + sleep 10 + exit 1 + fi + + - name: git + description: GitLab Repository Mirror Checks + actions: + - cmd: | + ./uds zarf package create --confirm + dir: tests/data + - task: create-doug-pat + - cmd: | + ./uds zarf package mirror-resources \ + zarf-package-gitlab-git-tests-${UDS_ARCH}-0.0.1.tar.zst \ + --git-url "https://gitlab.uds.dev" \ + --git-push-username "doug" \ + --git-push-password "${GITLAB_TOKEN}" \ + --confirm + dir: tests/data + + - name: ui + description: GitLab UI Checks + actions: + - cmd: npm ci + dir: tests + - cmd: npx playwright install --with-deps + dir: tests + - cmd: npx playwright test + dir: tests + + - name: create-doug-admin + description: Create "doug" account as admin (must be run *before* first login) + actions: + - cmd: | + ./uds zarf tools kubectl exec -n gitlab deployment/gitlab-toolbox -- gitlab-rails runner -e production '\ + user = User.new(username: "doug", name: "Doug Unicorn", email: "doug@uds.dev", password: "0123456789!", password_confirmation: "0123456789!"); \ + user.assign_personal_namespace!; \ + user.skip_confirmation!; \ + user.admin = true; \ + user.save!; \ + puts user.username; puts user.id; \ + ' + + - name: create-doug-pat + description: Create personal access token (PAT) for "doug" account + actions: + - cmd: | + ./uds zarf tools kubectl exec -n gitlab deployment/gitlab-toolbox -- gitlab-rails runner -e production '\ + user = User.find_by_username("doug"); \ + token = user.personal_access_tokens.create(scopes: ["api"], name: "doug", expires_at: 365.days.from_now); \ + token.save!; \ + puts token.token; \ + ' + setVariables: + - name: GITLAB_TOKEN - name: root-password actions: diff --git a/tests/auth.setup.ts b/tests/auth.setup.ts new file mode 100644 index 00000000..03899310 --- /dev/null +++ b/tests/auth.setup.ts @@ -0,0 +1,26 @@ +import { test as setup, expect } from '@playwright/test'; +import { authFile } from './playwright.config'; + +setup('authenticate', async ({ page, context }) => { + await page.goto('/'); + + await page.getByLabel('Username or email').fill('doug'); + await page.getByLabel('Password').fill('unicorn123!@#'); + await page.getByRole('button', { name: "Log In" }).click(); + + await page.waitForURL('/'); // successful redirect + + // ensure auth cookies were set + const cookies = await context.cookies(); + const keycloakCookie = cookies.find( + (cookie) => cookie.name === "KEYCLOAK_SESSION", + ); + + expect(keycloakCookie).toBeDefined(); + expect(keycloakCookie?.value).not.toBe(""); + expect(keycloakCookie?.domain).toContain("sso."); + + await page.context().storageState({ path: authFile }); + + await expect(page).toHaveURL('/'); +}) diff --git a/tests/data/unicorns.jpeg b/tests/data/unicorns.jpeg new file mode 100644 index 00000000..571d08da Binary files /dev/null and b/tests/data/unicorns.jpeg differ diff --git a/tests/data/zarf.yaml b/tests/data/zarf.yaml new file mode 100644 index 00000000..1f798886 --- /dev/null +++ b/tests/data/zarf.yaml @@ -0,0 +1,12 @@ +kind: ZarfPackageConfig +metadata: + name: gitlab-git-tests + version: 0.0.1 + description: A package with git repos used for testing + +components: + - name: git-repos + repos: + # This references a commit that has a .gitlab-ci.yml in it - to update this push a PR and a new commit. + - https://github.com/defenseunicorns/uds-package-gitlab-runner.git + - https://github.com/defenseunicorns/uds-core.git diff --git a/tests/gitlab.test.ts b/tests/gitlab.test.ts new file mode 100644 index 00000000..88e61ab8 --- /dev/null +++ b/tests/gitlab.test.ts @@ -0,0 +1,68 @@ +import { test, expect } from "@playwright/test"; +import path from "path"; + +function randomProjectName(prefix: string = 'uds-package-test') { + return [ prefix, Math.floor((Math.random() * 10_000)) ].join('-'); +} + +test('setup a project', async ({ page }) => { + const projectName = randomProjectName(); + + await page.goto('/projects/new#blank_project'); + await page.getByLabel('Project name').fill(projectName); + await page.getByLabel('Initialize repository with a README').setChecked(true); + await page.getByRole('button', { name: 'Create project' }).click(); + + await expect(page).toHaveURL(`/doug/${projectName}`); + + await test.step('create a file', async () => { + await page.goto(`/doug/${projectName}/-/new/main`); + + await page.getByTestId('file-name-field').fill('docs/README.md'); + await page.getByLabel('Editor content;Press Alt+F1').fill('# Docs', { force: true }); + await page.getByTestId('commit-button').click(); + + await expect(page).toHaveURL(`/doug/${projectName}/-/blob/main/docs/README.md`) + await expect(page.getByRole('heading', { level: 1 })).toContainText('Docs'); + }); + + await test.step('create an issue', async () => { + await page.goto(`/doug/${projectName}/-/issues/new`); + + await page.getByTestId('issuable-form-title-field').fill('We should write more tests!'); + + const descriptionBox = page.getByTestId('issuable-form-description-field'); + + await descriptionBox.fill(`Why are there no tests???\n\n`); + + // upload a file + const fileChooserPromise = page.waitForEvent('filechooser'); + await page.getByRole('button', { name: 'Attach a file or image' }).click(); + const fileChooser = await fileChooserPromise; + await fileChooser.setFiles(path.join(__dirname, 'data/unicorns.jpeg')); + + // check that markdown description box is updated + await expect(descriptionBox).toHaveValue(/uploads\/[a-z0-9]+\/unicorns\.jpeg/); + + await page.getByTestId('issuable-create-button').click(); + + // check that rendered image ends up in issue description + await expect(page.getByRole('img', { name: 'unicorns' })) + .toHaveAttribute('src', /uploads\/[a-z0-9]+\/unicorns\.jpeg/); + }); + + // regression test for Istio path decoding: https://github.com/defenseunicorns/uds-core/issues/288 + await test.step('fetch via API', async () => { + const projectPath = encodeURIComponent(`doug/${projectName}`); + + const res = await page.request.get(`/api/v4/projects/${projectPath}`); + + expect(res.url()).toContain('%2F'); + await expect(res).toBeOK(); + + const project = await res.json(); + + expect(project.path).toBe(projectName); + expect(project.namespace.path).toBe('doug'); + }); +}); diff --git a/tests/package-lock.json b/tests/package-lock.json new file mode 100644 index 00000000..fc17509f --- /dev/null +++ b/tests/package-lock.json @@ -0,0 +1,104 @@ +{ + "name": "uds-package-gitlab", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "uds-package-gitlab", + "license": "Apache-2.0", + "devDependencies": { + "@playwright/test": "^1.43.1", + "@types/node": "^20.12.12", + "typescript": "^5.4.5" + } + }, + "node_modules/@playwright/test": { + "version": "1.43.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.43.1.tgz", + "integrity": "sha512-HgtQzFgNEEo4TE22K/X7sYTYNqEMMTZmFS8kTq6m8hXj+m1D8TgwgIbumHddJa9h4yl4GkKb8/bgAl2+g7eDgA==", + "dev": true, + "dependencies": { + "playwright": "1.43.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@types/node": { + "version": "20.12.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz", + "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.43.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.43.1.tgz", + "integrity": "sha512-V7SoH0ai2kNt1Md9E3Gwas5B9m8KR2GVvwZnAI6Pg0m3sh7UvgiYhRrhsziCmqMJNouPckiOhk8T+9bSAK0VIA==", + "dev": true, + "dependencies": { + "playwright-core": "1.43.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.43.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.43.1.tgz", + "integrity": "sha512-EI36Mto2Vrx6VF7rm708qSnesVQKbxEWvPrfA1IPY6HgczBplDx7ENtx+K2n4kJ41sLLkuGfmb0ZLSSXlDhqPg==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + } + } +} diff --git a/tests/package.json b/tests/package.json new file mode 100644 index 00000000..cb7750cf --- /dev/null +++ b/tests/package.json @@ -0,0 +1,9 @@ +{ + "name": "uds-package-gitlab", + "license": "Apache-2.0", + "devDependencies": { + "@playwright/test": "^1.43.1", + "@types/node": "^20.12.12", + "typescript": "^5.4.5" + } +} diff --git a/tests/playwright.config.ts b/tests/playwright.config.ts new file mode 100644 index 00000000..33e2b098 --- /dev/null +++ b/tests/playwright.config.ts @@ -0,0 +1,43 @@ +import { defineConfig, devices } from '@playwright/test'; + +export const playwrightDir = '.playwright'; +export const authFile = `${playwrightDir}/auth/user.json`; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + fullyParallel: true, + forbidOnly: !!process.env.CI, // fail CI if you accidently leave `test.only` in source + retries: process.env.CI ? 1 : 0, + workers: 1, + reporter: [ + // Reporter to use. See https://playwright.dev/docs/test-reporters + ['html', { outputFolder: `${playwrightDir}/reports`, open: 'never' }], + ['json', { outputFile: `${playwrightDir}/reports/test-results.json`, open: 'never' }], + ['list'] + ], + + outputDir: `${playwrightDir}/output`, + + use: { + baseURL: process.env.BASE_URL || 'https://gitlab.uds.dev', // for `await page.goto('/')` etc + trace: 'on-first-retry', // collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer + }, + + projects: [ + { name: 'setup', testMatch: /.*\.setup\.ts/ }, // authentication + + ...[ + 'Desktop Chrome', + 'Desktop Firefox', + ].map((p) => ({ + name: devices[p].defaultBrowserType, + dependencies: ['setup'], + use: { + ...devices[p], + storageState: authFile, + }, + })), + ], +}); diff --git a/tests/tsconfig.json b/tests/tsconfig.json new file mode 100644 index 00000000..a3b60e83 --- /dev/null +++ b/tests/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "module": "commonjs", /* Specify what module code is generated. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + "strict": true, /* Enable all strict type-checking options. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +}