Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Playwright

on:
push:
branches:
- main
pull_request:

permissions:
contents: read

jobs:
playwright:
name: Playwright
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5

- uses: actions/setup-node@v4
with:
node-version: '22'
cache: npm

- run: npm ci

- run: npx playwright install --with-deps chromium

- run: npm run env:start

- run: npm run test:e2e

- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: tests/E2E/.report
include-hidden-files: true
retention-days: 7
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
# Local clone managed by Makefile (not tracked in this repository)
/site/
/node_modules/
/tests/E2E/.results/
/tests/E2E/.report/
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,24 @@ On a fresh WordPress database, `make up` also performs the initial WordPress
installation via WP-CLI and restarts the WordPress container once so the
`wordpress-docker` entrypoint can install the configured plugins and themes.

## End-to-end tests

End-to-end tests cover the customer workflows across WordPress and Nextcloud:
signing up for a plan, logging into Nextcloud with the store account, renewing,
cancelling and changing plan.

They run on a separate stack built from scratch, with the plugins and theme
declared in `docker-compose.override.yml`, so they never touch the local
database. They need Docker and Node.js:

```bash
npm ci
npx playwright install chromium
npm run env:start # WordPress on :8891, Nextcloud on :8892, Mailpit on :8893
npm run test:e2e
npm run env:stop
```

## Integration Flow

WordPress is the customer-facing commerce and account portal (plans, subscriptions, invoices, payments, and account reports).
Expand Down
59 changes: 59 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "libresign-saas",
"private": true,
"license": "AGPL-3.0-or-later",
"description": "End-to-end tests for the LibreSign SaaS stack",
"scripts": {
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"env:start": "sh tests/E2E/support/setup.sh",
"env:stop": "docker compose -f tests/E2E/docker-compose.yml down -v"
},
"devDependencies": {
"@playwright/test": "^1.63.0"
}
}
29 changes: 29 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { defineConfig, devices } from '@playwright/test';

export default defineConfig( {
testDir: 'tests/E2E',
outputDir: 'tests/E2E/.results',
workers: 1,
fullyParallel: false,
forbidOnly: Boolean( process.env.CI ),
retries: process.env.CI ? 1 : 0,
timeout: 120_000,
reporter: process.env.CI
? [
[ 'github' ],
[
'html',
{ open: 'never', outputFolder: 'tests/E2E/.report' },
],
]
: [ [ 'list' ] ],
use: {
...devices[ 'Desktop Chrome' ],
baseURL: 'http://localhost:8891',
trace: 'retain-on-failure',
screenshot: 'only-on-failure',
launchOptions: {
args: [ '--host-resolver-rules=MAP nextcloud:80 127.0.0.1:8892' ],
},
},
} );
67 changes: 67 additions & 0 deletions tests/E2E/NextcloudLogin.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { expect, test } from '@playwright/test';

import { nextcloud } from './support/nextcloud';
import { aSubscriber, logInAsAdmin, logInToTheStore, newCustomer } from './support/store';
import { wp } from './support/wordpress';

test.describe( 'Logging into Nextcloud with the store account', () => {
test( 'a store account without a plan is refused', async ( { browser } ) => {
const customer = newCustomer( 'Elisa' );
wp( 'user', 'create', customer.email, customer.email, `--user_pass=${ customer.password }`, '--role=customer' );

await nextcloud.expectWrongLoginOrPassword( browser, customer.email, customer.password );
expect( await nextcloud.account( customer.email ) ).toBeNull();
} );

test( 'a subscriber with a wrong password is refused', async ( { browser, page } ) => {
const { customer } = await aSubscriber( page, 'Fernanda' );

await nextcloud.expectWrongLoginOrPassword( browser, customer.email, 'not-the-password' );
} );

test.fail( 'a password changed in the account details replaces the old one', { annotation: { type: 'issue', description: 'https://github.com/LibreSign/wordpress_login_backend/issues/22' } }, async ( { browser, page } ) => {
const { customer } = await aSubscriber( page, 'Gustavo' );
const newPassword = `${ customer.password }-changed`;

await logInToTheStore( page, customer.email, customer.password );
await page.goto( '/account/edit-account/' );
await page.locator( '#password_current' ).fill( customer.password );
await page.locator( '#password_1' ).fill( newPassword );
await page.locator( '#password_2' ).fill( newPassword );
await page.getByRole( 'button', { name: 'Save changes' } ).click();
await expect( page.getByText( 'Account details changed successfully.' ) ).toBeVisible();

await nextcloud.expectToLogIn( browser, customer.email, newPassword );
await nextcloud.expectWrongLoginOrPassword( browser, customer.email, customer.password );
} );

test( 'an e-mail changed in the account details is copied to Nextcloud', async ( { browser, page } ) => {
const { customer } = await aSubscriber( page, 'Heitor' );
const newEmail = newCustomer( 'Heitor' ).email;

await logInToTheStore( page, customer.email, customer.password );
await page.goto( '/account/edit-account/' );
await page.locator( '#account_email' ).fill( newEmail );
await page.getByRole( 'button', { name: 'Save changes' } ).click();
await expect( page.getByText( 'Account details changed successfully.' ) ).toBeVisible();

expect( await nextcloud.account( customer.email ) ).toMatchObject( { email: newEmail } );
} );

test( 'a store account deleted by the admin is refused', async ( { browser, page } ) => {
const { customer } = await aSubscriber( page, 'Iara' );
await nextcloud.expectToLogIn( browser, customer.email, customer.password );

await logInAsAdmin( page );
await page.goto( `/wp-admin/users.php?s=${ encodeURIComponent( customer.email ) }` );
const userRow = page.locator( '#the-list tr' ).filter( { hasText: customer.email } );
await userRow.hover();
page.once( 'dialog', ( dialog ) => dialog.accept() );
await userRow.getByRole( 'link', { name: 'Delete', exact: true } ).click();
await page.getByRole( 'radio', { name: 'Delete all content.' } ).check();
await page.getByRole( 'button', { name: 'Confirm Deletion' } ).click();
await expect( page.getByText( 'User deleted.' ) ).toBeVisible();

await nextcloud.expectAccountDisabled( browser, customer.email, customer.password );
} );
} );
43 changes: 43 additions & 0 deletions tests/E2E/PlanChange.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { expect, test } from '@playwright/test';

import { nextcloud } from './support/nextcloud';
import { aSubscriber, logInToTheStore, placeTheOrder, plansInTheStore, STORE_URL } from './support/store';
import { subscriptionStatus } from './support/wordpress';

const GIGABYTE = 1024 ** 3;

test.describe( 'Changing plan', () => {
test( 'a visitor sees every plan, a Basic subscriber only Basic and its upgrade', async ( { page } ) => {
expect( ( await plansInTheStore( page ) ).sort() ).toEqual( [ 'Basic', 'Enterprise', 'Professional' ] );

const { customer, subscriptionId } = await aSubscriber( page, 'Marta' );
await logInToTheStore( page, customer.email, customer.password );

expect( ( await plansInTheStore( page ) ).sort() ).toEqual( [ 'Basic', 'Enterprise' ] );

await page.goto( `${ STORE_URL }/account/view-subscription/${ subscriptionId }/` );
await page.getByRole( 'link', { name: 'Upgrade' } ).click();
await expect( page.getByText( 'Choose a new subscription.' ) ).toBeVisible();
await expect( page.getByRole( 'link', { name: 'Enterprise', exact: true } ) ).toBeVisible();
await expect( page.getByRole( 'link', { name: 'Basic', exact: true } ) ).toBeVisible();
await expect( page.getByRole( 'link', { name: 'Professional', exact: true } ) ).toHaveCount( 0 );
} );

test.fail( 'upgrading from Basic to Enterprise raises the workspace storage to 800 GB', { annotation: { type: 'issue', description: 'https://github.com/LibreSign/woocommerce-nextcloud-admin-group-manager/issues/33' } }, async ( { browser, page } ) => {
const { customer, subscriptionId } = await aSubscriber( page, 'Nilo' );
await logInToTheStore( page, customer.email, customer.password );

await page.goto( `${ STORE_URL }/account/view-subscription/${ subscriptionId }/` );
await page.getByRole( 'link', { name: 'Upgrade' } ).click();
await page.getByRole( 'link', { name: 'Enterprise', exact: true } ).click();
await page.locator( '.cfvsw-swatches-option', { hasText: 'Monthly' } ).click();
await page.getByRole( 'button', { name: 'Switch subscription' } ).click();
await page.goto( `${ STORE_URL }/checkout/` );
await page.getByRole( 'button', { name: 'Switch subscription' } ).click();
await expect( page ).toHaveURL( /\/checkout\/order-received\/\d+\//, { timeout: 90_000 } );

expect( subscriptionStatus( subscriptionId ) ).toBe( 'active' );
expect( await nextcloud.account( customer.email ) ).toMatchObject( { enabled: true, quota: { quota: 800 * GIGABYTE } } );
await nextcloud.expectToLogIn( browser, customer.email, customer.password );
} );
} );
81 changes: 81 additions & 0 deletions tests/E2E/SignUp.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { expect, test } from '@playwright/test';

import { mailTo } from './support/mailpit';
import { nextcloud } from './support/nextcloud';
import { logInToTheStore, newCustomer, setThePasswordFromTheWelcomeMail, signUp, subscriptionOf } from './support/store';
import { orderStatus, runTheScheduledNextcloudSyncRetry, subscriptionStatus } from './support/wordpress';

const GIGABYTE = 1024 ** 3;

const plans = [
{ plan: 'Basic', term: 'Monthly', storage: 2 },
{ plan: 'Professional', term: 'Yearly', storage: 120 },
] as const;

test.describe( 'Signing up for a plan', () => {
for ( const { plan, term, storage } of plans ) {
test( `a visitor who signs up for ${ plan } ${ term.toLowerCase() } gets a Nextcloud workspace with ${ storage } GB`, async ( { page } ) => {
const customer = newCustomer( 'Ana' );

await signUp( page, customer, plan, term );

await expect( page.getByRole( 'heading', { name: 'Order completed' } ) ).toBeVisible();
expect( subscriptionStatus( await subscriptionOf( page ) ) ).toBe( 'active' );

const workspace = await nextcloud.account( customer.email );
expect( workspace ).toMatchObject( {
enabled: true,
email: customer.email,
displayname: `${ customer.firstName } ${ customer.lastName }`,
groups: [ customer.email ],
subadmin: [ customer.email ],
quota: { quota: storage * GIGABYTE },
} );
} );
}

test( 'the customer sets a password from the welcome mail and reaches Nextcloud from the account page', async ( { page } ) => {
const customer = newCustomer( 'Bruno' );
await signUp( page, customer, 'Basic', 'Monthly' );

await setThePasswordFromTheWelcomeMail( page, customer );
await logInToTheStore( page, customer.email, customer.password );
const nextcloudTab = page.waitForEvent( 'popup' );
await page.getByRole( 'link', { name: 'Ir para o sistema de assinaturas' } ).click();
const nextcloudPage = await nextcloudTab;
await nextcloudPage.getByLabel( 'Account name or email' ).fill( customer.email );
await nextcloudPage.getByLabel( 'Password', { exact: true } ).fill( customer.password );
await nextcloudPage.getByRole( 'button', { name: 'Log in', exact: true } ).click();

await expect( nextcloudPage ).toHaveURL( /\/apps\/dashboard\// );
} );

test( 'the store and the customer are notified by mail', async ( { page } ) => {
const customer = newCustomer( 'Clara' );

const orderId = await signUp( page, customer, 'Basic', 'Monthly' );

await mailTo( customer.email, 'order has been received' );
await mailTo( 'admin@example.com', `#${ orderId }` );
} );

test.fail( 'a workspace ordered while Nextcloud is down is created by the scheduled retry', { annotation: { type: 'issue', description: 'https://github.com/LibreSign/woocommerce-nextcloud-admin-group-manager/issues/34' } }, async ( { page } ) => {
const customer = newCustomer( 'Diego' );

nextcloud.stop();
let orderId: number;
try {
orderId = await signUp( page, customer, 'Basic', 'Monthly' );
} finally {
nextcloud.start();
}

expect( await nextcloud.account( customer.email ) ).toBeNull();
expect( orderStatus( orderId ) ).toBe( 'processing' );

runTheScheduledNextcloudSyncRetry( orderId );

expect( await nextcloud.account( customer.email ) ).toMatchObject( { enabled: true, quota: { quota: 2 * GIGABYTE } } );
expect( orderStatus( orderId ) ).toBe( 'completed' );
} );
} );
Loading
Loading