Skip to content
Merged
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
41 changes: 41 additions & 0 deletions .github/workflows/ci-e2e.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: CI (e2e)

on:
push:
paths:
- "packages/core/**"
- "e2e/**"
- ".github/workflows/ci-e2e.yaml"

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
e2e:
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.51.1-noble
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
with:
run_install: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
cache: pnpm
cache-dependency-path: ./pnpm-lock.yaml
node-version-file: "./package.json"
- name: Install dependencies
run: pnpm install
- name: Build packages
run: pnpm exec turbo run build
- name: Run E2E tests
run: pnpm --filter e2e test
env:
VITE_TAILOR_APP_URL: ${{ secrets.E2E_TAILOR_APP_URL }}
VITE_TAILOR_CLIENT_ID: ${{ secrets.E2E_TAILOR_CLIENT_ID }}
E2E_USER_EMAIL: ${{ secrets.E2E_USER_EMAIL }}
E2E_USER_PASSWORD: ${{ secrets.E2E_USER_PASSWORD }}
8 changes: 8 additions & 0 deletions e2e/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Tailor Platform workspace URL (e.g., https://xyz.erp.dev)
VITE_TAILOR_APP_URL=
# OAuth2 public client ID
VITE_TAILOR_CLIENT_ID=

# E2E test user credentials
E2E_USER_EMAIL=e2e-test@example.com
E2E_USER_PASSWORD=TestPassword123!
6 changes: 6 additions & 0 deletions e2e/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules/
.env
test-results/
playwright-report/
blob-report/
.tailor-sdk
97 changes: 97 additions & 0 deletions e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# E2E Tests for AuthProvider

Playwright-based E2E tests that verify the AuthProvider OAuth authentication flow against a real Tailor Platform workspace.

## Setup

### 1. Deploy backend to Tailor Platform

```bash
cd e2e/backend
TAILOR_PLATFORM_WORKSPACE_ID=<your-workspace-id> pnpm deploy
```

After deploy, retrieve the app URL and client ID using `tailor-sdk`:

```bash
# Get the app URL
npx tailor-sdk show --workspace-id <your-workspace-id> --json
# → {"url": "https://<slug>.erp.dev", ...}

# Get the OAuth2 client ID
npx tailor-sdk oauth2client list --workspace-id <your-workspace-id> --json
# → [{"clientId": "tpoc_...", ...}]
```

### 2. Create a test user

Open the GraphQL Playground for the workspace at [console.tailor.tech](https://console.tailor.tech) and run:

```graphql
# 1. Create the IDP user (authentication credentials)
mutation CreateIdpUser {
_createUser(input: { password: "TestPassword123!", name: "e2e-test@example.com" }) {
id
name
}
}

# 2. Create the TailorDB user profile (required for user lookup after auth)
mutation CreateUserProfile {
createUser(input: { email: "e2e-test@example.com", name: "E2E Test User", roles: [] }) {
id
}
}
```

### 3. Configure environment

```bash
cp e2e/.env.example e2e/.env
```

Fill in `VITE_TAILOR_APP_URL` and `VITE_TAILOR_CLIENT_ID` (retrieved above). The test user credentials are pre-filled.

### 4. Install dependencies & browsers

```bash
pnpm install
cd e2e && npx playwright install chromium
```

## Running Tests

```bash
# From monorepo root
cd e2e && pnpm test

# With Playwright UI
cd e2e && pnpm test:ui

# Dev server only (for debugging)
cd e2e && pnpm dev
```

## Test Scenarios

| Test | Description |
| ------------------- | ---------------------------------------------------------------- |
| Auth guard display | Verifies unauthenticated users see the login UI |
| Login flow | Full OAuth redirect → IDP login → callback → authenticated state |
| Logout | Verifies logout returns to auth guard |
| Session persistence | Confirms page reload maintains authentication |

## Architecture

```
e2e/
├── app/ # Minimal Vite app with AuthProvider
│ ├── src/App.tsx # Test app using AuthProvider + guardComponent
│ └── vite.config.ts
├── backend/ # Tailor Platform config for E2E workspace
│ ├── tailor.config.ts
│ └── src/tailordb/user.ts
├── tests/
│ └── auth.spec.ts # Playwright test specs
└── playwright.config.ts
```
12 changes: 12 additions & 0 deletions e2e/app/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AppShell E2E Test</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
54 changes: 54 additions & 0 deletions e2e/app/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { AuthProvider, createAuthClient, useAuth } from "@tailor-platform/app-shell";

const authClient = createAuthClient({
appUri: import.meta.env.VITE_TAILOR_APP_URL,
clientId: import.meta.env.VITE_TAILOR_CLIENT_ID,
});

const AuthGuard = () => {
const { login } = useAuth();

return (
<main data-testid="auth-guard">
<h1>Sign in required</h1>
<p>You need to sign in to access this app.</p>
<button
type="button"
data-testid="login-button"
onClick={() => {
void login();
}}
>
Sign in
</button>
</main>
);
};

const AuthenticatedContent = () => {
const { logout, isAuthenticated } = useAuth();

return (
<main data-testid="authenticated-content">
<h1>Authenticated</h1>
<p data-testid="auth-status">{isAuthenticated ? "Logged in" : "Not logged in"}</p>
<button
type="button"
data-testid="logout-button"
onClick={() => {
void logout();
}}
>
Log out
</button>
</main>
);
};

export const App = () => {
return (
<AuthProvider client={authClient} guardComponent={AuthGuard}>
<AuthenticatedContent />
</AuthProvider>
);
};
3 changes: 3 additions & 0 deletions e2e/app/src/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@import "tailwindcss";
@import "@tailor-platform/app-shell/styles";
@import "@tailor-platform/app-shell/theme.css";
10 changes: 10 additions & 0 deletions e2e/app/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { App } from "./App";
import "./index.css";

createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>,
);
12 changes: 12 additions & 0 deletions e2e/app/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";

export default defineConfig({
root: "./app",
envDir: "..",
plugins: [react(), tailwindcss()],
server: {
port: 3100,
},
});
14 changes: 14 additions & 0 deletions e2e/backend/src/tailordb/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {
db,
unsafeAllowAllGqlPermission,
unsafeAllowAllTypePermission,
} from "@tailor-platform/sdk";

export const user = db
.type("User", "A user account for E2E testing", {
name: db.string().description("Display name of the user"),
email: db.string().unique().description("Email address used for authentication"),
roles: db.string({ array: true }).description("List of roles assigned to the user"),
})
.permission(unsafeAllowAllTypePermission)
.gqlPermission(unsafeAllowAllGqlPermission);
45 changes: 45 additions & 0 deletions e2e/backend/tailor.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { defineAuth, defineConfig, defineIdp } from "@tailor-platform/sdk";
import { user } from "./src/tailordb/user";

const oauth2Config = {
redirectURIs: ["http://localhost:3100" as const],
grantTypes: ["authorization_code" as const, "refresh_token" as const],
};

const idp = defineIdp("e2e-idp", {
clients: ["e2e-idp-client"],
permission: {
create: [{ conditions: [], permit: true }],
read: [{ conditions: [], permit: true }],
update: [{ conditions: [], permit: true }],
delete: [{ conditions: [], permit: true }],
sendPasswordResetEmail: [{ conditions: [], permit: true }],
},
});

const auth = defineAuth("e2e-auth", {
userProfile: {
type: user,
usernameField: "email",
attributes: { roles: true },
},
oauth2Clients: {
"e2e-oauth2-client-public": {
...oauth2Config,
clientType: "public",
},
},
idProvider: idp.provider(idp.name, idp.clients[0]),
});

export default defineConfig({
name: "app-shell-e2e",
cors: [oauth2Config.redirectURIs[0]],

db: {
"e2e-db": { files: [`./src/tailordb/**/*.ts`] },
},

auth,
idp: [idp],
});
19 changes: 19 additions & 0 deletions e2e/backend/tailor.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// This file is auto-generated by @tailor-platform/sdk
// Do not edit this file manually
// Regenerated automatically when running 'tailor-sdk apply' or 'tailor-sdk generate'

declare module "@tailor-platform/sdk" {
interface AttributeMap {
roles: string[];
}
interface AttributeList {
__tuple?: [];
}
interface Env {}
interface MachineUserNameRegistry {}
interface IdpNameRegistry {
"e2e-idp": true;
}
}

export {};
27 changes: 27 additions & 0 deletions e2e/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "e2e",
"private": true,
"type": "module",
"scripts": {
"test": "playwright test",
"test:ui": "playwright test --ui",
"dev": "vite --config app/vite.config.ts",
"deploy:backend": "cd backend && tailor-sdk apply --workspace-id $TAILOR_PLATFORM_WORKSPACE_ID --yes"
},
"devDependencies": {
"@playwright/test": "1.51.1",
"@tailor-platform/app-shell": "workspace:*",
"@tailor-platform/sdk": "^1.45.1",
"@tailwindcss/vite": "^4.3.0",
"@types/node": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@vitejs/plugin-react": "catalog:",
"dotenv": "^16.5.0",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"tailwindcss": "catalog:",
"typescript": "catalog:",
"vite": "catalog:"
}
}
25 changes: 25 additions & 0 deletions e2e/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { defineConfig, devices } from "@playwright/test";
import { config } from "dotenv";
import { resolve } from "path";

config({ path: resolve(import.meta.dirname, ".env") });

export default defineConfig({
testDir: "./tests",
forbidOnly: !!process.env.CI,
use: {
baseURL: "http://localhost:3100",
trace: "on-first-retry",
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
webServer: {
command: "pnpm dev",
url: "http://localhost:3100",
reuseExistingServer: !process.env.CI,
},
});
Loading
Loading