een-api-toolkit 0.0.8 → 0.0.11
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 +78 -175
- 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 +2 -1
|
@@ -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>
|