@prsm/auth 1.0.0
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/README.md +226 -0
- package/index.d.ts +19 -0
- package/package.json +76 -0
- package/src/__tests__/auth.test.js +1171 -0
- package/src/__tests__/impersonation-test-setup.js +208 -0
- package/src/__tests__/impersonation.test.js +473 -0
- package/src/__tests__/oauth-test-setup.js +136 -0
- package/src/__tests__/oauth.test.js +400 -0
- package/src/__tests__/prsm.test.js +215 -0
- package/src/__tests__/test-setup.js +385 -0
- package/src/__tests__/totp.test.js +158 -0
- package/src/__tests__/two-factor-test-setup.js +331 -0
- package/src/__tests__/two-factor.test.js +396 -0
- package/src/activity-logger.js +228 -0
- package/src/auth-context.js +120 -0
- package/src/auth-functions.js +520 -0
- package/src/auth-manager.js +1371 -0
- package/src/errors.js +173 -0
- package/src/hooks.js +41 -0
- package/src/index.js +23 -0
- package/src/invalidation.js +166 -0
- package/src/middleware.js +33 -0
- package/src/providers/azure-provider.js +114 -0
- package/src/providers/base-provider.js +152 -0
- package/src/providers/github-provider.js +86 -0
- package/src/providers/google-provider.js +76 -0
- package/src/providers/index.js +4 -0
- package/src/queries.js +543 -0
- package/src/schema.js +261 -0
- package/src/totp.js +221 -0
- package/src/two-factor/index.js +3 -0
- package/src/two-factor/otp-provider.js +128 -0
- package/src/two-factor/totp-provider.js +98 -0
- package/src/two-factor/two-factor-manager.js +676 -0
- package/src/types.js +399 -0
- package/src/user-roles.js +128 -0
- package/src/util.js +32 -0
- package/types/activity-logger.d.ts +73 -0
- package/types/auth-context.d.ts +88 -0
- package/types/auth-functions.d.ts +151 -0
- package/types/auth-manager.d.ts +365 -0
- package/types/errors.d.ts +108 -0
- package/types/hooks.d.ts +30 -0
- package/types/index.d.ts +13 -0
- package/types/invalidation.d.ts +40 -0
- package/types/middleware.d.ts +11 -0
- package/types/providers/azure-provider.d.ts +35 -0
- package/types/providers/base-provider.d.ts +52 -0
- package/types/providers/github-provider.d.ts +29 -0
- package/types/providers/google-provider.d.ts +29 -0
- package/types/providers/index.d.ts +4 -0
- package/types/queries.d.ts +287 -0
- package/types/schema.d.ts +37 -0
- package/types/totp.d.ts +72 -0
- package/types/two-factor/index.d.ts +3 -0
- package/types/two-factor/otp-provider.d.ts +57 -0
- package/types/two-factor/totp-provider.d.ts +58 -0
- package/types/two-factor/two-factor-manager.d.ts +191 -0
- package/types/types.d.ts +688 -0
- package/types/user-roles.d.ts +47 -0
- package/types/util.d.ts +3 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import express from "express"
|
|
2
|
+
import session from "express-session"
|
|
3
|
+
import cookieParser from "cookie-parser"
|
|
4
|
+
import pg from "pg"
|
|
5
|
+
import { createAuthMiddleware, createAuthTables, dropAuthTables, closeInvalidationListeners } from "../index.js"
|
|
6
|
+
|
|
7
|
+
const { Pool } = pg
|
|
8
|
+
|
|
9
|
+
export async function createTestDatabase() {
|
|
10
|
+
const url = process.env.AUTH_TEST_POSTGRES_URL
|
|
11
|
+
const poolOptions = { max: 15, idleTimeoutMillis: 1000 }
|
|
12
|
+
const pool = url
|
|
13
|
+
? new Pool({ connectionString: url, ...poolOptions })
|
|
14
|
+
: new Pool({
|
|
15
|
+
host: process.env.PGHOST || "localhost",
|
|
16
|
+
port: parseInt(process.env.PGPORT || "5432"),
|
|
17
|
+
database: process.env.PGDATABASE || "auth_test",
|
|
18
|
+
user: process.env.PGUSER || "auth",
|
|
19
|
+
password: process.env.PGPASSWORD || "auth_password",
|
|
20
|
+
...poolOptions,
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
await pool.query("SELECT NOW()")
|
|
25
|
+
} catch (error) {
|
|
26
|
+
throw new Error("Failed to connect to test database. Make sure to run: make up")
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return pool
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function createOAuthTestApp() {
|
|
33
|
+
const pool = await createTestDatabase()
|
|
34
|
+
|
|
35
|
+
const app = express()
|
|
36
|
+
|
|
37
|
+
app.use(express.json())
|
|
38
|
+
app.use(cookieParser())
|
|
39
|
+
app.use(
|
|
40
|
+
session({
|
|
41
|
+
secret: "oauth-test-secret-key-for-testing-only",
|
|
42
|
+
resave: false,
|
|
43
|
+
saveUninitialized: false,
|
|
44
|
+
cookie: { secure: false, httpOnly: true },
|
|
45
|
+
}),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
const authConfig = {
|
|
49
|
+
db: pool,
|
|
50
|
+
createUser: async (userData) => {
|
|
51
|
+
// simulate creating user in app's user table
|
|
52
|
+
return `oauth-user-${userData.id}`
|
|
53
|
+
},
|
|
54
|
+
tablePrefix: "oauth_test_",
|
|
55
|
+
minPasswordLength: 6,
|
|
56
|
+
maxPasswordLength: 50,
|
|
57
|
+
rememberDuration: "7d",
|
|
58
|
+
rememberCookieName: "oauth_test_remember_token",
|
|
59
|
+
resyncInterval: "30s",
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
await dropAuthTables(authConfig)
|
|
63
|
+
await createAuthTables(authConfig)
|
|
64
|
+
|
|
65
|
+
app.use(createAuthMiddleware(authConfig))
|
|
66
|
+
|
|
67
|
+
// standard auth routes for testing
|
|
68
|
+
app.post("/register", async (req, res) => {
|
|
69
|
+
try {
|
|
70
|
+
const { email, password, requireConfirmation } = req.body
|
|
71
|
+
let confirmationToken
|
|
72
|
+
|
|
73
|
+
const account = await req.auth.register(
|
|
74
|
+
email,
|
|
75
|
+
password,
|
|
76
|
+
"oauth-test-user-123",
|
|
77
|
+
requireConfirmation
|
|
78
|
+
? (token) => {
|
|
79
|
+
confirmationToken = token
|
|
80
|
+
}
|
|
81
|
+
: undefined,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
res.json({
|
|
85
|
+
success: true,
|
|
86
|
+
account: {
|
|
87
|
+
id: account.id,
|
|
88
|
+
email: account.email,
|
|
89
|
+
verified: account.verified,
|
|
90
|
+
status: account.status,
|
|
91
|
+
},
|
|
92
|
+
confirmationToken,
|
|
93
|
+
})
|
|
94
|
+
} catch (error) {
|
|
95
|
+
res.status(400).json({ error: error.message })
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
app.get("/profile", async (req, res) => {
|
|
100
|
+
if (!req.auth.isLoggedIn()) {
|
|
101
|
+
return res.status(401).json({ error: "Not logged in" })
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
res.json({
|
|
105
|
+
id: req.auth.getId(),
|
|
106
|
+
email: req.auth.getEmail(),
|
|
107
|
+
status: req.auth.getStatus(),
|
|
108
|
+
statusName: req.auth.getStatusName(),
|
|
109
|
+
verified: req.auth.getVerified(),
|
|
110
|
+
roles: req.auth.getRoleNames(),
|
|
111
|
+
remembered: req.auth.isRemembered(),
|
|
112
|
+
isAdmin: await req.auth.isAdmin(),
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
// utility routes for OAuth tests
|
|
117
|
+
app.post("/set-user-session", (req, res) => {
|
|
118
|
+
req.session.userId = req.body.userId || "oauth-test-user-123"
|
|
119
|
+
req.session.save((err) => {
|
|
120
|
+
if (err) {
|
|
121
|
+
return res.status(500).json({ error: "Failed to save session" })
|
|
122
|
+
}
|
|
123
|
+
res.json({ success: true })
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
app,
|
|
129
|
+
pool,
|
|
130
|
+
authConfig,
|
|
131
|
+
cleanup: async () => {
|
|
132
|
+
await closeInvalidationListeners()
|
|
133
|
+
await pool.end()
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll, beforeEach, vi } from "vitest"
|
|
2
|
+
import request from "supertest"
|
|
3
|
+
import { createOAuthTestApp } from "./oauth-test-setup.js"
|
|
4
|
+
|
|
5
|
+
// mock fetch for OAuth calls
|
|
6
|
+
const mockFetch = vi.fn()
|
|
7
|
+
global.fetch = mockFetch
|
|
8
|
+
|
|
9
|
+
describe("OAuth Integration Tests", () => {
|
|
10
|
+
let app
|
|
11
|
+
let pool
|
|
12
|
+
let cleanup
|
|
13
|
+
let userIdCounter = 1
|
|
14
|
+
|
|
15
|
+
// mock user database (simulates the app's user table)
|
|
16
|
+
const mockUsers = []
|
|
17
|
+
|
|
18
|
+
beforeAll(async () => {
|
|
19
|
+
const testApp = await createOAuthTestApp()
|
|
20
|
+
app = testApp.app
|
|
21
|
+
pool = testApp.pool
|
|
22
|
+
cleanup = testApp.cleanup
|
|
23
|
+
|
|
24
|
+
// configure OAuth providers and createUser function
|
|
25
|
+
const originalConfig = testApp.authConfig
|
|
26
|
+
originalConfig.providers = {
|
|
27
|
+
github: {
|
|
28
|
+
clientId: "test-github-client-id",
|
|
29
|
+
clientSecret: "test-github-client-secret",
|
|
30
|
+
redirectUri: "http://localhost:3000/auth/github/callback",
|
|
31
|
+
},
|
|
32
|
+
google: {
|
|
33
|
+
clientId: "test-google-client-id",
|
|
34
|
+
clientSecret: "test-google-client-secret",
|
|
35
|
+
redirectUri: "http://localhost:3000/auth/google/callback",
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
originalConfig.createUser = async (userData) => {
|
|
40
|
+
const userId = userIdCounter++
|
|
41
|
+
mockUsers.push({
|
|
42
|
+
id: userId,
|
|
43
|
+
name: userData.name || userData.username,
|
|
44
|
+
email: userData.email,
|
|
45
|
+
})
|
|
46
|
+
return userId.toString()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
app.get("/auth/github", (req, res) => {
|
|
50
|
+
if (!req.auth.providers.github) {
|
|
51
|
+
return res.status(400).json({ error: "GitHub provider not configured" })
|
|
52
|
+
}
|
|
53
|
+
const authUrl = req.auth.providers.github.getAuthUrl()
|
|
54
|
+
res.json({ authUrl })
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
app.get("/auth/github/callback", async (req, res) => {
|
|
58
|
+
try {
|
|
59
|
+
if (!req.auth.providers.github) {
|
|
60
|
+
return res.status(400).json({ error: "GitHub provider not configured" })
|
|
61
|
+
}
|
|
62
|
+
await req.auth.providers.github.handleCallback(req)
|
|
63
|
+
res.json({ success: true })
|
|
64
|
+
} catch (error) {
|
|
65
|
+
res.status(400).json({ error: error.message })
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
app.get("/auth/google", (req, res) => {
|
|
70
|
+
if (!req.auth.providers.google) {
|
|
71
|
+
return res.status(400).json({ error: "Google provider not configured" })
|
|
72
|
+
}
|
|
73
|
+
const authUrl = req.auth.providers.google.getAuthUrl()
|
|
74
|
+
res.json({ authUrl })
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
app.get("/auth/google/callback", async (req, res) => {
|
|
78
|
+
try {
|
|
79
|
+
if (!req.auth.providers.google) {
|
|
80
|
+
return res.status(400).json({ error: "Google provider not configured" })
|
|
81
|
+
}
|
|
82
|
+
await req.auth.providers.google.handleCallback(req)
|
|
83
|
+
res.json({ success: true })
|
|
84
|
+
} catch (error) {
|
|
85
|
+
res.status(400).json({ error: error.message })
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
afterAll(async () => {
|
|
91
|
+
await cleanup()
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
beforeEach(async () => {
|
|
95
|
+
await pool.query("DELETE FROM oauth_test_providers")
|
|
96
|
+
await pool.query("DELETE FROM oauth_test_resets")
|
|
97
|
+
await pool.query("DELETE FROM oauth_test_remembers")
|
|
98
|
+
await pool.query("DELETE FROM oauth_test_confirmations")
|
|
99
|
+
await pool.query("DELETE FROM oauth_test_accounts")
|
|
100
|
+
mockUsers.length = 0
|
|
101
|
+
userIdCounter = 1
|
|
102
|
+
vi.clearAllMocks()
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
describe("GitHub OAuth", () => {
|
|
106
|
+
it("should generate GitHub auth URL", async () => {
|
|
107
|
+
const response = await request(app).get("/auth/github").expect(200)
|
|
108
|
+
|
|
109
|
+
expect(response.body.authUrl).toMatch(/^https:\/\/github\.com\/login\/oauth\/authorize/)
|
|
110
|
+
expect(response.body.authUrl).toContain("client_id=test-github-client-id")
|
|
111
|
+
expect(response.body.authUrl).toContain("scope=user%3Aemail")
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it("should handle GitHub OAuth callback for new user", async () => {
|
|
115
|
+
mockFetch.mockResolvedValueOnce({
|
|
116
|
+
ok: true,
|
|
117
|
+
json: () =>
|
|
118
|
+
Promise.resolve({
|
|
119
|
+
access_token: "github-access-token",
|
|
120
|
+
}),
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
mockFetch
|
|
124
|
+
.mockResolvedValueOnce({
|
|
125
|
+
ok: true,
|
|
126
|
+
json: () =>
|
|
127
|
+
Promise.resolve({
|
|
128
|
+
id: 12345,
|
|
129
|
+
login: "testuser",
|
|
130
|
+
name: "Test User",
|
|
131
|
+
avatar_url: "https://github.com/avatar.jpg",
|
|
132
|
+
}),
|
|
133
|
+
})
|
|
134
|
+
.mockResolvedValueOnce({
|
|
135
|
+
ok: true,
|
|
136
|
+
json: () =>
|
|
137
|
+
Promise.resolve([
|
|
138
|
+
{
|
|
139
|
+
email: "testuser@example.com",
|
|
140
|
+
primary: true,
|
|
141
|
+
verified: true,
|
|
142
|
+
},
|
|
143
|
+
]),
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
const agent = request.agent(app)
|
|
147
|
+
|
|
148
|
+
const response = await agent.get("/auth/github/callback").query({ code: "github-auth-code" }).expect(200)
|
|
149
|
+
|
|
150
|
+
expect(response.body.success).toBe(true)
|
|
151
|
+
|
|
152
|
+
expect(mockUsers).toHaveLength(1)
|
|
153
|
+
expect(mockUsers[0]).toEqual({
|
|
154
|
+
id: 1,
|
|
155
|
+
name: "Test User",
|
|
156
|
+
email: "testuser@example.com",
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
const accounts = await pool.query("SELECT * FROM oauth_test_accounts")
|
|
160
|
+
expect(accounts.rows).toHaveLength(1)
|
|
161
|
+
expect(accounts.rows[0].email).toBe("testuser@example.com")
|
|
162
|
+
expect(accounts.rows[0].password).toBeNull()
|
|
163
|
+
expect(accounts.rows[0].verified).toBe(true)
|
|
164
|
+
|
|
165
|
+
const providers = await pool.query("SELECT * FROM oauth_test_providers")
|
|
166
|
+
expect(providers.rows).toHaveLength(1)
|
|
167
|
+
expect(providers.rows[0].provider).toBe("github")
|
|
168
|
+
expect(providers.rows[0].provider_id).toBe("12345")
|
|
169
|
+
expect(providers.rows[0].provider_email).toBe("testuser@example.com")
|
|
170
|
+
expect(providers.rows[0].provider_username).toBe("testuser")
|
|
171
|
+
|
|
172
|
+
const profileResponse = await agent.get("/profile").expect(200)
|
|
173
|
+
expect(profileResponse.body.email).toBe("testuser@example.com")
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it("should handle GitHub OAuth callback for existing OAuth user", async () => {
|
|
177
|
+
const agent = request.agent(app)
|
|
178
|
+
await agent.post("/set-user-session").send({ userId: "existing-user" })
|
|
179
|
+
|
|
180
|
+
const existingUser = {
|
|
181
|
+
id: 999,
|
|
182
|
+
name: "Existing User",
|
|
183
|
+
email: "existing@example.com",
|
|
184
|
+
}
|
|
185
|
+
mockUsers.push(existingUser)
|
|
186
|
+
|
|
187
|
+
const account = await pool.query(
|
|
188
|
+
`INSERT INTO oauth_test_accounts (user_id, email, password, verified, status, rolemask)
|
|
189
|
+
VALUES ($1, $2, NULL, true, 0, 0) RETURNING *`,
|
|
190
|
+
[existingUser.id, existingUser.email],
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
await pool.query(
|
|
194
|
+
`INSERT INTO oauth_test_providers (account_id, provider, provider_id, provider_email, provider_username)
|
|
195
|
+
VALUES ($1, 'github', '12345', $2, 'existinguser')`,
|
|
196
|
+
[account.rows[0].id, existingUser.email],
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
mockFetch.mockResolvedValueOnce({
|
|
200
|
+
ok: true,
|
|
201
|
+
json: () => Promise.resolve({ access_token: "github-access-token" }),
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
mockFetch
|
|
205
|
+
.mockResolvedValueOnce({
|
|
206
|
+
ok: true,
|
|
207
|
+
json: () =>
|
|
208
|
+
Promise.resolve({
|
|
209
|
+
id: 12345,
|
|
210
|
+
login: "existinguser",
|
|
211
|
+
name: "Existing User",
|
|
212
|
+
avatar_url: "https://github.com/avatar.jpg",
|
|
213
|
+
}),
|
|
214
|
+
})
|
|
215
|
+
.mockResolvedValueOnce({
|
|
216
|
+
ok: true,
|
|
217
|
+
json: () => Promise.resolve([{ email: "existing@example.com", primary: true, verified: true }]),
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
const response = await agent.get("/auth/github/callback").query({ code: "github-auth-code" }).expect(200)
|
|
221
|
+
|
|
222
|
+
expect(response.body.success).toBe(true)
|
|
223
|
+
|
|
224
|
+
expect(mockUsers).toHaveLength(1)
|
|
225
|
+
|
|
226
|
+
const profileResponse = await agent.get("/profile").expect(200)
|
|
227
|
+
expect(profileResponse.body.email).toBe("existing@example.com")
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it("should reject GitHub OAuth when email already exists with different provider", async () => {
|
|
231
|
+
const agent = request.agent(app)
|
|
232
|
+
await agent.post("/set-user-session").send({ userId: "test-user-123" })
|
|
233
|
+
|
|
234
|
+
await agent.post("/register").send({
|
|
235
|
+
email: "conflict@example.com",
|
|
236
|
+
password: "password123",
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
mockFetch.mockResolvedValueOnce({
|
|
240
|
+
ok: true,
|
|
241
|
+
json: () => Promise.resolve({ access_token: "github-access-token" }),
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
mockFetch
|
|
245
|
+
.mockResolvedValueOnce({
|
|
246
|
+
ok: true,
|
|
247
|
+
json: () =>
|
|
248
|
+
Promise.resolve({
|
|
249
|
+
id: 54321,
|
|
250
|
+
login: "conflictuser",
|
|
251
|
+
name: "Conflict User",
|
|
252
|
+
avatar_url: "https://github.com/avatar.jpg",
|
|
253
|
+
}),
|
|
254
|
+
})
|
|
255
|
+
.mockResolvedValueOnce({
|
|
256
|
+
ok: true,
|
|
257
|
+
json: () => Promise.resolve([{ email: "conflict@example.com", primary: true, verified: true }]),
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
const response = await agent.get("/auth/github/callback").query({ code: "github-auth-code" }).expect(400)
|
|
261
|
+
|
|
262
|
+
expect(response.body.error).toContain("already have an account")
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it("should handle GitHub API errors gracefully", async () => {
|
|
266
|
+
mockFetch.mockResolvedValueOnce({
|
|
267
|
+
ok: false,
|
|
268
|
+
status: 400,
|
|
269
|
+
statusText: "Bad Request",
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
const response = await request(app).get("/auth/github/callback").query({ code: "invalid-code" }).expect(400)
|
|
273
|
+
|
|
274
|
+
expect(response.body.error).toContain("OAuth token exchange failed")
|
|
275
|
+
})
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
describe("Google OAuth", () => {
|
|
279
|
+
it("should generate Google auth URL", async () => {
|
|
280
|
+
const response = await request(app).get("/auth/google").expect(200)
|
|
281
|
+
|
|
282
|
+
expect(response.body.authUrl).toMatch(/^https:\/\/accounts\.google\.com\/o\/oauth2\/v2\/auth/)
|
|
283
|
+
expect(response.body.authUrl).toContain("client_id=test-google-client-id")
|
|
284
|
+
expect(response.body.authUrl).toContain("scope=openid+profile+email")
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
it("should handle Google OAuth callback for new user", async () => {
|
|
288
|
+
mockFetch.mockResolvedValueOnce({
|
|
289
|
+
ok: true,
|
|
290
|
+
json: () =>
|
|
291
|
+
Promise.resolve({
|
|
292
|
+
access_token: "google-access-token",
|
|
293
|
+
}),
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
mockFetch.mockResolvedValueOnce({
|
|
297
|
+
ok: true,
|
|
298
|
+
json: () =>
|
|
299
|
+
Promise.resolve({
|
|
300
|
+
id: "google-user-123",
|
|
301
|
+
email: "googleuser@example.com",
|
|
302
|
+
name: "Google User",
|
|
303
|
+
picture: "https://google.com/avatar.jpg",
|
|
304
|
+
}),
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
const agent = request.agent(app)
|
|
308
|
+
|
|
309
|
+
const response = await agent.get("/auth/google/callback").query({ code: "google-auth-code" }).expect(200)
|
|
310
|
+
|
|
311
|
+
expect(response.body.success).toBe(true)
|
|
312
|
+
|
|
313
|
+
expect(mockUsers).toHaveLength(1)
|
|
314
|
+
expect(mockUsers[0].email).toBe("googleuser@example.com")
|
|
315
|
+
|
|
316
|
+
const providers = await pool.query("SELECT * FROM oauth_test_providers")
|
|
317
|
+
expect(providers.rows).toHaveLength(1)
|
|
318
|
+
expect(providers.rows[0].provider).toBe("google")
|
|
319
|
+
expect(providers.rows[0].provider_id).toBe("google-user-123")
|
|
320
|
+
})
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
describe("OAuth Error Handling", () => {
|
|
324
|
+
it("should handle missing authorization code", async () => {
|
|
325
|
+
const response = await request(app).get("/auth/github/callback").expect(400)
|
|
326
|
+
|
|
327
|
+
expect(response.body.error).toContain("No authorization code provided")
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
it("should handle missing createUser function by auto-generating UUID", async () => {
|
|
331
|
+
const testAppWithoutCreateUser = await createOAuthTestApp()
|
|
332
|
+
const appWithoutCreateUser = testAppWithoutCreateUser.app
|
|
333
|
+
|
|
334
|
+
const configWithoutCreateUser = testAppWithoutCreateUser.authConfig
|
|
335
|
+
// Remove createUser to test auto-UUID generation
|
|
336
|
+
delete configWithoutCreateUser.createUser
|
|
337
|
+
configWithoutCreateUser.providers = {
|
|
338
|
+
github: {
|
|
339
|
+
clientId: "test-github-client-id",
|
|
340
|
+
clientSecret: "test-github-client-secret",
|
|
341
|
+
redirectUri: "http://localhost:3000/auth/github/callback",
|
|
342
|
+
},
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
appWithoutCreateUser.get("/auth/github/callback", async (req, res) => {
|
|
346
|
+
try {
|
|
347
|
+
if (!req.auth.providers.github) {
|
|
348
|
+
return res.status(400).json({ error: "GitHub provider not configured" })
|
|
349
|
+
}
|
|
350
|
+
await req.auth.providers.github.handleCallback(req)
|
|
351
|
+
res.json({ success: true })
|
|
352
|
+
} catch (error) {
|
|
353
|
+
res.status(400).json({ error: error.message })
|
|
354
|
+
}
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
mockFetch.mockResolvedValueOnce({
|
|
358
|
+
ok: true,
|
|
359
|
+
json: () => Promise.resolve({ access_token: "github-access-token" }),
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
mockFetch
|
|
363
|
+
.mockResolvedValueOnce({
|
|
364
|
+
ok: true,
|
|
365
|
+
json: () =>
|
|
366
|
+
Promise.resolve({
|
|
367
|
+
id: 99999,
|
|
368
|
+
login: "newuser",
|
|
369
|
+
name: "New User",
|
|
370
|
+
}),
|
|
371
|
+
})
|
|
372
|
+
.mockResolvedValueOnce({
|
|
373
|
+
ok: true,
|
|
374
|
+
json: () => Promise.resolve([{ email: "newuser@example.com", primary: true, verified: true }]),
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
const response = await request(appWithoutCreateUser).get("/auth/github/callback").query({ code: "github-auth-code" }).expect(200)
|
|
378
|
+
|
|
379
|
+
expect(response.body.success).toBe(true)
|
|
380
|
+
|
|
381
|
+
await testAppWithoutCreateUser.cleanup()
|
|
382
|
+
})
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
describe("Provider Integration", () => {
|
|
386
|
+
it("should have providers available on auth object", async () => {
|
|
387
|
+
const agent = request.agent(app)
|
|
388
|
+
|
|
389
|
+
// this is tested indirectly by the successful OAuth flows above,
|
|
390
|
+
// but we can verify the providers are initialized
|
|
391
|
+
const githubResponse = await agent.get("/auth/github")
|
|
392
|
+
expect(githubResponse.status).toBe(200)
|
|
393
|
+
expect(githubResponse.body.authUrl).toBeDefined()
|
|
394
|
+
|
|
395
|
+
const googleResponse = await agent.get("/auth/google")
|
|
396
|
+
expect(googleResponse.status).toBe(200)
|
|
397
|
+
expect(googleResponse.body.authUrl).toBeDefined()
|
|
398
|
+
})
|
|
399
|
+
})
|
|
400
|
+
})
|