een-api-toolkit 0.0.8 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +257 -146
- package/README.md +108 -230
- package/docs/AI-CONTEXT.md +767 -0
- package/examples/vue-basic/.env.example +12 -0
- package/examples/vue-basic/README.md +146 -0
- package/examples/vue-basic/e2e/app.spec.ts +61 -0
- package/examples/vue-basic/e2e/auth.spec.ts +260 -0
- package/examples/vue-basic/index.html +13 -0
- package/examples/vue-basic/package-lock.json +1583 -0
- package/examples/vue-basic/package.json +28 -0
- package/examples/vue-basic/playwright.config.ts +47 -0
- package/examples/vue-basic/src/App.vue +108 -0
- package/examples/vue-basic/src/main.ts +23 -0
- package/examples/vue-basic/src/router/index.ts +61 -0
- package/examples/vue-basic/src/views/Callback.vue +76 -0
- package/examples/vue-basic/src/views/Home.vue +105 -0
- package/examples/vue-basic/src/views/Login.vue +33 -0
- package/examples/vue-basic/src/views/Logout.vue +59 -0
- package/examples/vue-basic/src/views/Users.vue +106 -0
- package/examples/vue-basic/src/vite-env.d.ts +12 -0
- package/examples/vue-basic/tsconfig.json +21 -0
- package/examples/vue-basic/tsconfig.node.json +10 -0
- package/examples/vue-basic/vite.config.ts +12 -0
- package/package.json +3 -1
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# EEN OAuth Client ID (required)
|
|
2
|
+
VITE_EEN_CLIENT_ID=your-een-client-id
|
|
3
|
+
|
|
4
|
+
# OAuth Proxy URL (required)
|
|
5
|
+
VITE_PROXY_URL=http://localhost:8787
|
|
6
|
+
|
|
7
|
+
# OAuth Redirect URI (MUST be http://127.0.0.1:3333 - EEN IDP requirement)
|
|
8
|
+
# Do not change this value - the EEN Identity Provider only permits this URL
|
|
9
|
+
VITE_REDIRECT_URI=http://127.0.0.1:3333
|
|
10
|
+
|
|
11
|
+
# Enable debug logging (optional)
|
|
12
|
+
VITE_DEBUG=true
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# EEN API Toolkit - Vue 3 Example
|
|
2
|
+
|
|
3
|
+
A complete example showing how to use the een-api-toolkit in a Vue 3 application.
|
|
4
|
+
|
|
5
|
+
## Features Demonstrated
|
|
6
|
+
|
|
7
|
+
- OAuth authentication flow (login, callback, logout)
|
|
8
|
+
- Protected routes with navigation guards
|
|
9
|
+
- useCurrentUser composable for current user profile
|
|
10
|
+
- useUsers composable with pagination
|
|
11
|
+
- Error handling
|
|
12
|
+
- Reactive authentication state
|
|
13
|
+
|
|
14
|
+
## Setup
|
|
15
|
+
|
|
16
|
+
### Prerequisites
|
|
17
|
+
|
|
18
|
+
1. **Start the OAuth proxy** (required for authentication):
|
|
19
|
+
|
|
20
|
+
The OAuth proxy is a separate project that handles token management securely.
|
|
21
|
+
Clone and run it from: https://github.com/klaushofrichter/een-oauth-proxy
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# In a separate terminal, from the een-oauth-proxy directory
|
|
25
|
+
npm install
|
|
26
|
+
npm run dev
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The proxy should be running at `http://localhost:8787`.
|
|
30
|
+
|
|
31
|
+
### Example Setup
|
|
32
|
+
|
|
33
|
+
All commands below should be run from this example directory (`examples/vue-basic/`):
|
|
34
|
+
|
|
35
|
+
2. Copy the environment file:
|
|
36
|
+
```bash
|
|
37
|
+
# From examples/vue-basic/
|
|
38
|
+
cp .env.example .env
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
3. Edit `.env` with your EEN credentials:
|
|
42
|
+
```env
|
|
43
|
+
VITE_EEN_CLIENT_ID=your-client-id
|
|
44
|
+
VITE_PROXY_URL=http://localhost:8787
|
|
45
|
+
# DO NOT change the redirect URI - EEN IDP only permits this URL
|
|
46
|
+
VITE_REDIRECT_URI=http://127.0.0.1:3333
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
4. Install dependencies and start:
|
|
50
|
+
```bash
|
|
51
|
+
# From examples/vue-basic/
|
|
52
|
+
npm install
|
|
53
|
+
npm run dev
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
5. Open http://127.0.0.1:3333 in your browser.
|
|
57
|
+
|
|
58
|
+
**Important:** The EEN Identity Provider only permits `http://127.0.0.1:3333` as the OAuth redirect URI. Do not use `localhost` or other ports.
|
|
59
|
+
|
|
60
|
+
## Project Structure
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
src/
|
|
64
|
+
├── main.ts # App entry, toolkit initialization
|
|
65
|
+
├── App.vue # Root component with navigation
|
|
66
|
+
├── router/
|
|
67
|
+
│ └── index.ts # Vue Router with auth guards
|
|
68
|
+
└── views/
|
|
69
|
+
├── Home.vue # Home page with user profile
|
|
70
|
+
├── Login.vue # OAuth login redirect
|
|
71
|
+
├── Callback.vue # OAuth callback handler
|
|
72
|
+
├── Users.vue # User list with pagination
|
|
73
|
+
└── Logout.vue # Logout handler
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Key Code Examples
|
|
77
|
+
|
|
78
|
+
### Initializing the Toolkit (main.ts)
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
import { initEenToolkit } from 'een-api-toolkit'
|
|
82
|
+
|
|
83
|
+
initEenToolkit({
|
|
84
|
+
proxyUrl: import.meta.env.VITE_PROXY_URL,
|
|
85
|
+
clientId: import.meta.env.VITE_EEN_CLIENT_ID,
|
|
86
|
+
debug: true
|
|
87
|
+
})
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### OAuth Login (Login.vue)
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
import { getAuthUrl } from 'een-api-toolkit'
|
|
94
|
+
|
|
95
|
+
function login() {
|
|
96
|
+
window.location.href = getAuthUrl()
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### OAuth Callback (Callback.vue)
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
import { handleAuthCallback } from 'een-api-toolkit'
|
|
104
|
+
|
|
105
|
+
const url = new URL(window.location.href)
|
|
106
|
+
const code = url.searchParams.get('code')
|
|
107
|
+
const state = url.searchParams.get('state')
|
|
108
|
+
|
|
109
|
+
const { error } = await handleAuthCallback(code, state)
|
|
110
|
+
if (error) {
|
|
111
|
+
// Handle error
|
|
112
|
+
} else {
|
|
113
|
+
router.push('/dashboard')
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Using Composables (Users.vue)
|
|
118
|
+
|
|
119
|
+
```vue
|
|
120
|
+
<script setup>
|
|
121
|
+
import { useUsers } from 'een-api-toolkit'
|
|
122
|
+
|
|
123
|
+
const { users, loading, error, hasNextPage, fetchNextPage } = useUsers({ pageSize: 10 })
|
|
124
|
+
</script>
|
|
125
|
+
|
|
126
|
+
<template>
|
|
127
|
+
<ul v-for="user in users" :key="user.id">
|
|
128
|
+
<li>{{ user.email }}</li>
|
|
129
|
+
</ul>
|
|
130
|
+
<button v-if="hasNextPage" @click="fetchNextPage">Load More</button>
|
|
131
|
+
</template>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Auth Guard (router/index.ts)
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
router.beforeEach((to, from, next) => {
|
|
138
|
+
const authStore = useAuthStore()
|
|
139
|
+
|
|
140
|
+
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
|
|
141
|
+
next('/login')
|
|
142
|
+
} else {
|
|
143
|
+
next()
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
```
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { test, expect } from '@playwright/test'
|
|
2
|
+
|
|
3
|
+
test.describe('Vue Basic Example - App', () => {
|
|
4
|
+
test.beforeEach(async ({ page }) => {
|
|
5
|
+
await page.goto('/')
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
test('app loads with correct title', async ({ page }) => {
|
|
9
|
+
await expect(page).toHaveTitle(/EEN API Toolkit/)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
test('header displays app name', async ({ page }) => {
|
|
13
|
+
await expect(page.locator('[data-testid="app-title"]')).toHaveText('EEN API Toolkit Example')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('navigation shows Home and Login links when not authenticated', async ({ page }) => {
|
|
17
|
+
// Home link should be visible
|
|
18
|
+
await expect(page.locator('[data-testid="nav-home"]')).toBeVisible()
|
|
19
|
+
|
|
20
|
+
// Login link should be visible (not authenticated)
|
|
21
|
+
await expect(page.locator('[data-testid="nav-login"]')).toBeVisible()
|
|
22
|
+
|
|
23
|
+
// Users and Logout should NOT be visible (requires auth)
|
|
24
|
+
await expect(page.locator('[data-testid="nav-users"]')).not.toBeVisible()
|
|
25
|
+
await expect(page.locator('[data-testid="nav-logout"]')).not.toBeVisible()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('home page shows not logged in message', async ({ page }) => {
|
|
29
|
+
await expect(page.locator('[data-testid="not-authenticated"]')).toBeVisible()
|
|
30
|
+
await expect(page.locator('[data-testid="not-authenticated-message"]')).toBeVisible()
|
|
31
|
+
await expect(page.locator('[data-testid="login-button"]')).toBeVisible()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('login page displays login button', async ({ page }) => {
|
|
35
|
+
await page.goto('/login')
|
|
36
|
+
|
|
37
|
+
await expect(page.locator('[data-testid="login-title"]')).toHaveText('Login')
|
|
38
|
+
await expect(page.locator('[data-testid="login-button"]')).toBeVisible()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('protected route redirects to login', async ({ page }) => {
|
|
42
|
+
await page.goto('/users')
|
|
43
|
+
|
|
44
|
+
// Should be redirected to login page
|
|
45
|
+
await page.waitForURL('/login')
|
|
46
|
+
await expect(page).toHaveURL('/login')
|
|
47
|
+
await expect(page.locator('[data-testid="login-title"]')).toHaveText('Login')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('navigation between pages works', async ({ page }) => {
|
|
51
|
+
// Click Login link
|
|
52
|
+
await page.click('[data-testid="nav-login"]')
|
|
53
|
+
await page.waitForURL('/login')
|
|
54
|
+
await expect(page).toHaveURL('/login')
|
|
55
|
+
|
|
56
|
+
// Click Home link
|
|
57
|
+
await page.click('[data-testid="nav-home"]')
|
|
58
|
+
await page.waitForURL('/')
|
|
59
|
+
await expect(page).toHaveURL('/')
|
|
60
|
+
})
|
|
61
|
+
})
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { test, expect, Page } from '@playwright/test'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* E2E tests for the Vue Basic Example
|
|
5
|
+
*
|
|
6
|
+
* Tests the OAuth login flow through the UI:
|
|
7
|
+
* 1. Click login button in the example app
|
|
8
|
+
* 2. Enter credentials on EEN OAuth page
|
|
9
|
+
* 3. Complete the OAuth callback
|
|
10
|
+
* 4. Verify authenticated state
|
|
11
|
+
*
|
|
12
|
+
* Required environment variables:
|
|
13
|
+
* - VITE_PROXY_URL: OAuth proxy URL (e.g., http://127.0.0.1:8787)
|
|
14
|
+
* - VITE_EEN_CLIENT_ID: EEN OAuth client ID
|
|
15
|
+
* - TEST_USER: Test user email
|
|
16
|
+
* - TEST_PASSWORD: Test user password
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// Timeout constants for consistent behavior
|
|
20
|
+
// Values chosen based on OAuth flow timing requirements
|
|
21
|
+
const TIMEOUTS = {
|
|
22
|
+
OAUTH_REDIRECT: 30000, // OAuth redirects can be slow on first load
|
|
23
|
+
ELEMENT_VISIBLE: 15000, // Wait for OAuth page elements to render
|
|
24
|
+
PASSWORD_VISIBLE: 10000, // Password field appears after email validation
|
|
25
|
+
AUTH_COMPLETE: 30000, // Full OAuth flow completion
|
|
26
|
+
UI_UPDATE: 10000, // UI state updates after auth changes
|
|
27
|
+
PROXY_CHECK: 5000 // Quick check if proxy is running
|
|
28
|
+
} as const
|
|
29
|
+
|
|
30
|
+
const TEST_USER = process.env.TEST_USER
|
|
31
|
+
const TEST_PASSWORD = process.env.TEST_PASSWORD
|
|
32
|
+
const PROXY_URL = process.env.VITE_PROXY_URL
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Checks if the OAuth proxy is accessible.
|
|
36
|
+
* Returns true if proxy responds (even with 404), false if unreachable.
|
|
37
|
+
*/
|
|
38
|
+
async function isProxyAccessible(): Promise<boolean> {
|
|
39
|
+
if (!PROXY_URL) return false
|
|
40
|
+
const controller = new AbortController()
|
|
41
|
+
const timeoutId = setTimeout(() => controller.abort(), TIMEOUTS.PROXY_CHECK)
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const response = await fetch(PROXY_URL, {
|
|
45
|
+
method: 'HEAD',
|
|
46
|
+
signal: controller.signal
|
|
47
|
+
})
|
|
48
|
+
// 404 is ok - means proxy is running but endpoint doesn't exist
|
|
49
|
+
return response.ok || response.status === 404
|
|
50
|
+
} catch {
|
|
51
|
+
return false
|
|
52
|
+
} finally {
|
|
53
|
+
clearTimeout(timeoutId)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Performs OAuth login flow through the UI.
|
|
59
|
+
* Starts from home page and completes full OAuth authentication.
|
|
60
|
+
*/
|
|
61
|
+
async function performLogin(page: Page, username: string, password: string): Promise<void> {
|
|
62
|
+
// Start at home page
|
|
63
|
+
await page.goto('/')
|
|
64
|
+
|
|
65
|
+
// Click login button and wait for OAuth redirect
|
|
66
|
+
await Promise.all([
|
|
67
|
+
page.waitForURL(/.*eagleeyenetworks.com.*/, { timeout: TIMEOUTS.OAUTH_REDIRECT }),
|
|
68
|
+
page.click('[data-testid="login-button"]')
|
|
69
|
+
])
|
|
70
|
+
|
|
71
|
+
// Fill email
|
|
72
|
+
const emailInput = page.locator('#authentication--input__email')
|
|
73
|
+
await emailInput.waitFor({ state: 'visible', timeout: TIMEOUTS.ELEMENT_VISIBLE })
|
|
74
|
+
await emailInput.fill(username)
|
|
75
|
+
|
|
76
|
+
// Click next
|
|
77
|
+
await page.getByRole('button', { name: 'Next' }).click()
|
|
78
|
+
|
|
79
|
+
// Fill password
|
|
80
|
+
const passwordInput = page.locator('#authentication--input__password')
|
|
81
|
+
await passwordInput.waitFor({ state: 'visible', timeout: TIMEOUTS.PASSWORD_VISIBLE })
|
|
82
|
+
await passwordInput.fill(password)
|
|
83
|
+
|
|
84
|
+
// Click sign in - use OR selector for robustness
|
|
85
|
+
await page.locator('#next, button:has-text("Sign in")').first().click()
|
|
86
|
+
|
|
87
|
+
// Wait for auth to complete and redirect to app
|
|
88
|
+
await page.waitForURL('**/', { timeout: TIMEOUTS.AUTH_COMPLETE })
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Clears browser storage to reset auth state.
|
|
93
|
+
* Handles cases where localStorage isn't accessible (e.g., about:blank, cross-origin).
|
|
94
|
+
*/
|
|
95
|
+
async function clearAuthState(page: Page): Promise<void> {
|
|
96
|
+
try {
|
|
97
|
+
// Only try to clear storage if we're on a page that allows it
|
|
98
|
+
const url = page.url()
|
|
99
|
+
if (url && url.startsWith('http')) {
|
|
100
|
+
await page.evaluate(() => {
|
|
101
|
+
try {
|
|
102
|
+
localStorage.clear()
|
|
103
|
+
sessionStorage.clear()
|
|
104
|
+
} catch {
|
|
105
|
+
// Ignore errors - storage may not be accessible
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
// Ignore errors - page may be closed or in an inaccessible state
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
test.describe('Vue Basic Example', () => {
|
|
115
|
+
// Check proxy accessibility once before all tests
|
|
116
|
+
let proxyAccessible = false
|
|
117
|
+
|
|
118
|
+
// Helper functions to skip tests when prerequisites aren't met
|
|
119
|
+
function skipIfNoProxy() {
|
|
120
|
+
test.skip(!proxyAccessible, 'OAuth proxy not accessible')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function skipIfNoCredentials() {
|
|
124
|
+
test.skip(!TEST_USER || !TEST_PASSWORD, 'Test credentials not available')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function skipIfNoUser() {
|
|
128
|
+
test.skip(!TEST_USER, 'Test user not available')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
test.beforeAll(async () => {
|
|
132
|
+
proxyAccessible = await isProxyAccessible()
|
|
133
|
+
if (!proxyAccessible) {
|
|
134
|
+
console.log('OAuth proxy not accessible - OAuth tests will be skipped')
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test.afterEach(async ({ page }) => {
|
|
139
|
+
// Clear auth state after each test to prevent state pollution
|
|
140
|
+
await clearAuthState(page)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test('shows login button when not authenticated', async ({ page }) => {
|
|
144
|
+
await page.goto('/')
|
|
145
|
+
await expect(page.locator('[data-testid="not-authenticated"]')).toBeVisible()
|
|
146
|
+
await expect(page.locator('[data-testid="nav-login"]')).toBeVisible()
|
|
147
|
+
await expect(page.locator('[data-testid="nav-logout"]')).not.toBeVisible()
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test('users page shows not-authenticated state without login', async ({ page }) => {
|
|
151
|
+
await page.goto('/users')
|
|
152
|
+
await expect(
|
|
153
|
+
page.locator('[data-testid="not-authenticated"], [data-testid="nav-login"], .error, .auth-required').first()
|
|
154
|
+
).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
test('login button redirects to OAuth page', async ({ page }) => {
|
|
158
|
+
skipIfNoProxy()
|
|
159
|
+
skipIfNoCredentials()
|
|
160
|
+
|
|
161
|
+
await page.goto('/')
|
|
162
|
+
await expect(page.locator('[data-testid="login-button"]')).toBeVisible()
|
|
163
|
+
await expect(page.locator('[data-testid="login-button"]')).toBeEnabled()
|
|
164
|
+
|
|
165
|
+
// Click login and verify redirect to OAuth page
|
|
166
|
+
await Promise.all([
|
|
167
|
+
page.waitForURL(/.*eagleeyenetworks.com.*/, { timeout: TIMEOUTS.OAUTH_REDIRECT }),
|
|
168
|
+
page.click('[data-testid="login-button"]')
|
|
169
|
+
])
|
|
170
|
+
|
|
171
|
+
// Verify we're on the OAuth page
|
|
172
|
+
const emailInput = page.locator('#authentication--input__email')
|
|
173
|
+
await expect(emailInput).toBeVisible({ timeout: TIMEOUTS.ELEMENT_VISIBLE })
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
test('complete OAuth login flow', async ({ page }) => {
|
|
177
|
+
skipIfNoProxy()
|
|
178
|
+
skipIfNoCredentials()
|
|
179
|
+
|
|
180
|
+
// Verify initially not authenticated
|
|
181
|
+
await page.goto('/')
|
|
182
|
+
await expect(page.locator('[data-testid="not-authenticated"]')).toBeVisible()
|
|
183
|
+
|
|
184
|
+
// Perform login
|
|
185
|
+
await performLogin(page, TEST_USER!, TEST_PASSWORD!)
|
|
186
|
+
|
|
187
|
+
// Verify authenticated state
|
|
188
|
+
await expect(page.locator('[data-testid="not-authenticated"]')).not.toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
|
|
189
|
+
await expect(page.locator('[data-testid="nav-users"]')).toBeVisible()
|
|
190
|
+
await expect(page.locator('[data-testid="nav-logout"]')).toBeVisible()
|
|
191
|
+
await expect(page.locator('[data-testid="nav-login"]')).not.toBeVisible()
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
test('can view users list after login', async ({ page }) => {
|
|
195
|
+
skipIfNoProxy()
|
|
196
|
+
skipIfNoCredentials()
|
|
197
|
+
|
|
198
|
+
await performLogin(page, TEST_USER!, TEST_PASSWORD!)
|
|
199
|
+
await expect(page.locator('[data-testid="nav-users"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
|
|
200
|
+
|
|
201
|
+
// Navigate to users page
|
|
202
|
+
await page.click('[data-testid="nav-users"]')
|
|
203
|
+
await page.waitForURL('/users')
|
|
204
|
+
|
|
205
|
+
// Should see users table (not error state)
|
|
206
|
+
await expect(page.locator('.users table')).toBeVisible({ timeout: TIMEOUTS.ELEMENT_VISIBLE })
|
|
207
|
+
await expect(page.locator('.error')).not.toBeVisible()
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
test('can logout after login', async ({ page }) => {
|
|
211
|
+
skipIfNoProxy()
|
|
212
|
+
skipIfNoCredentials()
|
|
213
|
+
|
|
214
|
+
await performLogin(page, TEST_USER!, TEST_PASSWORD!)
|
|
215
|
+
await expect(page.locator('[data-testid="nav-logout"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
|
|
216
|
+
|
|
217
|
+
// Click logout
|
|
218
|
+
await page.click('[data-testid="nav-logout"]')
|
|
219
|
+
|
|
220
|
+
// Should show not authenticated
|
|
221
|
+
await page.waitForURL('**/')
|
|
222
|
+
await expect(page.locator('[data-testid="not-authenticated"]')).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
|
|
223
|
+
await expect(page.locator('[data-testid="nav-login"]')).toBeVisible()
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
test('invalid password shows error on OAuth page', async ({ page }) => {
|
|
227
|
+
skipIfNoProxy()
|
|
228
|
+
skipIfNoUser()
|
|
229
|
+
|
|
230
|
+
await page.goto('/')
|
|
231
|
+
|
|
232
|
+
// Click login and wait for OAuth redirect
|
|
233
|
+
await Promise.all([
|
|
234
|
+
page.waitForURL(/.*eagleeyenetworks.com.*/, { timeout: TIMEOUTS.OAUTH_REDIRECT }),
|
|
235
|
+
page.click('[data-testid="login-button"]')
|
|
236
|
+
])
|
|
237
|
+
|
|
238
|
+
// Fill valid email
|
|
239
|
+
const emailInput = page.locator('#authentication--input__email')
|
|
240
|
+
await emailInput.waitFor({ state: 'visible', timeout: TIMEOUTS.ELEMENT_VISIBLE })
|
|
241
|
+
await emailInput.fill(TEST_USER!)
|
|
242
|
+
await page.getByRole('button', { name: 'Next' }).click()
|
|
243
|
+
|
|
244
|
+
// Fill invalid password
|
|
245
|
+
const passwordInput = page.locator('#authentication--input__password')
|
|
246
|
+
await passwordInput.waitFor({ state: 'visible', timeout: TIMEOUTS.PASSWORD_VISIBLE })
|
|
247
|
+
await passwordInput.fill('invalid-password-12345!')
|
|
248
|
+
|
|
249
|
+
// Click sign in
|
|
250
|
+
await page.locator('#next, button:has-text("Sign in")').first().click()
|
|
251
|
+
|
|
252
|
+
// Should show error message on OAuth page
|
|
253
|
+
await expect(
|
|
254
|
+
page.locator('.error, [class*="error"], [data-testid*="error"], #error, .alert-danger').first()
|
|
255
|
+
).toBeVisible({ timeout: TIMEOUTS.UI_UPDATE })
|
|
256
|
+
|
|
257
|
+
// Should still be on OAuth page
|
|
258
|
+
await expect(page).toHaveURL(/eagleeyenetworks\.com/)
|
|
259
|
+
})
|
|
260
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>EEN API Toolkit Example</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="app"></div>
|
|
11
|
+
<script type="module" src="/src/main.ts"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|