Skip to content

Commit e658156

Browse files
authored
chore(tests): added tests (#1072)
## What kind of change does this PR introduce? - updated base images' versions for the test infra - Added jsdom to emulate browser mode for GoTrueClient - Created own package.json and package-lock.json inside the test package to avoid affecting service dependencies
1 parent bc6192a commit e658156

File tree

9 files changed

+4873
-19
lines changed

9 files changed

+4873
-19
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,12 @@ jobs:
3434
3535
- name: Run tests
3636
run: |
37+
cd test && npm ci && cd ..
3738
npm t
3839
3940
- name: Upload coverage results to Coveralls
4041
uses: coverallsapp/github-action@master
4142
with:
4243
github-token: ${{ secrets.GITHUB_TOKEN }}
4344
path-to-lcov: ./test/coverage/lcov.info
45+
base-path: ./src

infra/docker-compose.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
version: '3'
33
services:
44
gotrue: # Signup enabled, autoconfirm off
5-
image: supabase/auth:v2.151.0
5+
image: supabase/auth:v2.176.0
66
ports:
77
- '9999:9999'
88
environment:
@@ -42,7 +42,7 @@ services:
4242
- db
4343
restart: on-failure
4444
autoconfirm: # Signup enabled, autoconfirm on
45-
image: supabase/auth:v2.151.0
45+
image: supabase/auth:v2.176.0
4646
ports:
4747
- '9998:9998'
4848
environment:
@@ -73,7 +73,7 @@ services:
7373
- db
7474
restart: on-failure
7575
autoconfirm_with_asymmetric_keys: # Signup enabled, autoconfirm on
76-
image: supabase/auth:v2.169.0
76+
image: supabase/auth:v2.176.0
7777
ports:
7878
- '9996:9996'
7979
environment:
@@ -103,7 +103,7 @@ services:
103103
- db
104104
restart: on-failure
105105
disabled: # Signup disabled
106-
image: supabase/auth:v2.151.0
106+
image: supabase/auth:v2.176.0
107107
ports:
108108
- '9997:9997'
109109
environment:
@@ -138,7 +138,7 @@ services:
138138
- '9000:9000' # web interface
139139
- '1100:1100' # POP3
140140
db:
141-
image: supabase/postgres:15.1.1.46
141+
image: supabase/postgres:15.8.1.100
142142
ports:
143143
- '5432:5432'
144144
command: postgres -c config_file=/etc/postgresql/postgresql.conf

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"build:module": "tsc -p tsconfig.module.json",
3131
"lint": "eslint ./src/**/* test/**/*.test.ts",
3232
"test": "run-s test:clean test:infra test:suite test:clean",
33-
"test:suite": "jest --runInBand --coverage",
33+
"test:suite": "npm --prefix ./test run test",
3434
"test:infra": "cd infra && docker compose down && docker compose pull && docker compose up -d && sleep 30",
3535
"test:clean": "cd infra && docker compose down",
3636
"docs": "typedoc src/index.ts --out docs/v2 --excludePrivate --excludeProtected",

test/GoTrueClient.browser.test.ts

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
5+
import {
6+
autoRefreshClient,
7+
getClientWithSpecificStorage,
8+
pkceClient
9+
} from './lib/clients'
10+
import { mockUserCredentials } from './lib/utils'
11+
12+
// Add structuredClone polyfill for jsdom
13+
if (typeof structuredClone === 'undefined') {
14+
(global as any).structuredClone = (obj: any) => JSON.parse(JSON.stringify(obj))
15+
}
16+
17+
describe('GoTrueClient in browser environment', () => {
18+
beforeEach(() => {
19+
const mockLocalStorage = {
20+
getItem: jest.fn(),
21+
setItem: jest.fn(),
22+
removeItem: jest.fn(),
23+
}
24+
Object.defineProperty(window, 'localStorage', {
25+
value: mockLocalStorage,
26+
writable: true,
27+
})
28+
29+
// Mock window.location
30+
const mockLocation = {
31+
href: 'http://localhost:9999',
32+
assign: jest.fn(),
33+
replace: jest.fn(),
34+
reload: jest.fn(),
35+
toString: () => 'http://localhost:9999'
36+
}
37+
Object.defineProperty(window, 'location', {
38+
value: mockLocation,
39+
writable: true,
40+
})
41+
})
42+
43+
it('should handle basic OAuth', async () => {
44+
const { data } = await pkceClient.signInWithOAuth({
45+
provider: 'github',
46+
options: {
47+
redirectTo: 'http://localhost:9999/callback'
48+
}
49+
})
50+
51+
expect(data?.url).toBeDefined()
52+
expect(data?.url).toContain('provider=github')
53+
})
54+
55+
it('should handle multiple visibility changes', async () => {
56+
Object.defineProperty(document, 'visibilityState', {
57+
value: 'visible',
58+
writable: true,
59+
})
60+
61+
await autoRefreshClient.startAutoRefresh()
62+
63+
document.dispatchEvent(new Event('visibilitychange'))
64+
Object.defineProperty(document, 'visibilityState', {
65+
value: 'hidden',
66+
writable: true,
67+
})
68+
document.dispatchEvent(new Event('visibilitychange'))
69+
Object.defineProperty(document, 'visibilityState', {
70+
value: 'visible',
71+
writable: true,
72+
})
73+
document.dispatchEvent(new Event('visibilitychange'))
74+
75+
await autoRefreshClient.stopAutoRefresh()
76+
})
77+
78+
it('should handle PKCE flow', async () => {
79+
const { email, password } = mockUserCredentials()
80+
const pkceClient = getClientWithSpecificStorage({
81+
getItem: jest.fn(),
82+
setItem: jest.fn(),
83+
removeItem: jest.fn(),
84+
})
85+
86+
const { data: signupData, error: signupError } = await pkceClient.signUp({
87+
email,
88+
password,
89+
options: {
90+
emailRedirectTo: 'http://localhost:9999/callback'
91+
}
92+
})
93+
94+
expect(signupError).toBeNull()
95+
expect(signupData?.user).toBeDefined()
96+
97+
const { data: signinData, error: signinError } = await pkceClient.signInWithPassword({
98+
email,
99+
password,
100+
})
101+
102+
expect(signinError).toBeNull()
103+
expect(signinData?.session).toBeDefined()
104+
})
105+
})
106+
107+
describe('Callback URL handling', () => {
108+
let mockFetch: jest.Mock
109+
let storedSession: string | null
110+
const mockStorage = {
111+
getItem: jest.fn(() => storedSession),
112+
setItem: jest.fn((key: string, value: string) => {
113+
storedSession = value
114+
}),
115+
removeItem: jest.fn(() => {
116+
storedSession = null
117+
}),
118+
}
119+
120+
beforeEach(() => {
121+
mockFetch = jest.fn()
122+
global.fetch = mockFetch
123+
})
124+
125+
it('should handle implicit grant callback', async () => {
126+
// Set up URL with implicit grant callback parameters
127+
window.location.href = 'http://localhost:9999/callback#access_token=test-token&refresh_token=test-refresh-token&expires_in=3600&token_type=bearer&type=implicit'
128+
129+
// Mock user info response
130+
mockFetch.mockImplementation((url: string) => {
131+
if (url.includes('/user')) {
132+
return Promise.resolve({
133+
ok: true,
134+
json: () => Promise.resolve({
135+
id: 'test-user',
136+
137+
created_at: new Date().toISOString()
138+
})
139+
})
140+
}
141+
return Promise.resolve({
142+
ok: true,
143+
json: () => Promise.resolve({
144+
access_token: 'test-token',
145+
refresh_token: 'test-refresh-token',
146+
expires_in: 3600,
147+
token_type: 'bearer',
148+
user: { id: 'test-user' }
149+
})
150+
})
151+
})
152+
153+
const client = getClientWithSpecificStorage(mockStorage)
154+
await client.initialize()
155+
156+
const { data: { session } } = await client.getSession()
157+
expect(session).toBeDefined()
158+
expect(session?.access_token).toBe('test-token')
159+
expect(session?.refresh_token).toBe('test-refresh-token')
160+
})
161+
162+
it('should handle error in callback URL', async () => {
163+
// Set up URL with error parameters
164+
window.location.href = 'http://localhost:9999/callback#error=invalid_grant&error_description=Invalid+grant'
165+
166+
mockFetch.mockImplementation((url: string) => {
167+
return Promise.resolve({
168+
ok: false,
169+
json: () => Promise.resolve({
170+
error: 'invalid_grant',
171+
error_description: 'Invalid grant'
172+
})
173+
})
174+
})
175+
176+
const client = getClientWithSpecificStorage(mockStorage)
177+
await client.initialize()
178+
179+
const { data: { session } } = await client.getSession()
180+
expect(session).toBeNull()
181+
})
182+
})
183+
184+
describe('GoTrueClient BroadcastChannel', () => {
185+
it('should handle multiple auth state change events', async () => {
186+
const mockBroadcastChannel = jest.fn().mockImplementation(() => ({
187+
postMessage: jest.fn(),
188+
addEventListener: jest.fn(),
189+
removeEventListener: jest.fn(),
190+
close: jest.fn(),
191+
}))
192+
Object.defineProperty(window, 'BroadcastChannel', {
193+
value: mockBroadcastChannel,
194+
writable: true,
195+
})
196+
197+
const mockStorage = {
198+
getItem: jest.fn(),
199+
setItem: jest.fn(),
200+
removeItem: jest.fn(),
201+
}
202+
203+
const client = getClientWithSpecificStorage(mockStorage)
204+
const mockCallback1 = jest.fn()
205+
const mockCallback2 = jest.fn()
206+
207+
const { data: { subscription: sub1 } } = client.onAuthStateChange(mockCallback1)
208+
const { data: { subscription: sub2 } } = client.onAuthStateChange(mockCallback2)
209+
210+
// Simulate a broadcast message
211+
const mockEvent = {
212+
data: {
213+
event: 'SIGNED_IN',
214+
session: {
215+
access_token: 'test-token',
216+
refresh_token: 'test-refresh-token',
217+
expires_in: 3600,
218+
token_type: 'bearer',
219+
user: { id: 'test-user' }
220+
}
221+
}
222+
}
223+
224+
// Get the event listener that was registered
225+
const eventListener = mockBroadcastChannel.mock.results[0].value.addEventListener.mock.calls[0][1]
226+
eventListener(mockEvent)
227+
228+
expect(mockCallback1).toHaveBeenCalledWith('SIGNED_IN', mockEvent.data.session)
229+
expect(mockCallback2).toHaveBeenCalledWith('SIGNED_IN', mockEvent.data.session)
230+
231+
sub1.unsubscribe()
232+
sub2.unsubscribe()
233+
})
234+
})

test/GoTrueClient.test.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from './lib/clients'
2121
import { mockUserCredentials } from './lib/utils'
2222
import { JWK, Session } from '../src'
23+
import { setItemAsync } from '../src/lib/helpers'
2324

2425
const TEST_USER_DATA = { info: 'some info' }
2526

@@ -1849,7 +1850,7 @@ describe('Web3 Authentication', () => {
18491850

18501851
expect(data?.session).toBeNull()
18511852
expect(error).not.toBeNull()
1852-
expect(error?.message).toContain("unsupported_grant_type")
1853+
expect(error?.message).toContain("Web3 provider is disabled")
18531854
})
18541855

18551856
test('signInWithWeb3 should fail solana chain without message', async () => {
@@ -2426,3 +2427,30 @@ describe('Lock functionality', () => {
24262427
expect(mockFn).not.toHaveBeenCalled()
24272428
})
24282429
})
2430+
2431+
describe('userNotAvailableProxy behavior', () => {
2432+
test('should return proxy user when userStorage is set but user is not found', async () => {
2433+
const storage = memoryLocalStorageAdapter()
2434+
const userStorage = memoryLocalStorageAdapter()
2435+
2436+
const client = new GoTrueClient({
2437+
storage,
2438+
userStorage
2439+
})
2440+
2441+
await setItemAsync(storage, STORAGE_KEY, {
2442+
access_token: 'jwt.accesstoken.signature',
2443+
refresh_token: 'refresh-token',
2444+
token_type: 'bearer',
2445+
expires_in: 1000,
2446+
expires_at: Date.now() / 1000 + 1000
2447+
})
2448+
2449+
const { data: { session } } = await client.getSession()
2450+
2451+
expect(session?.user).toBeDefined()
2452+
expect((session?.user as any).__isUserNotAvailableProxy).toBe(true)
2453+
2454+
expect(() => session?.user?.id).toThrow('@supabase/auth-js: client was created with userStorage option')
2455+
})
2456+
})

jest.config.js renamed to test/jest.config.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ module.exports = {
22
preset: 'ts-jest',
33
testEnvironment: 'node',
44
collectCoverage: true,
5-
coverageDirectory: './test/coverage',
5+
coverageDirectory: 'test/coverage',
66
coverageReporters: ['json', 'html', 'lcov'],
77
collectCoverageFrom: [
8-
'./src/**/*.{js,ts}',
9-
'./src/**/*.unit.test.ts',
8+
'src/**/*.{js,ts}',
9+
'src/**/*.unit.test.ts',
1010
'!**/node_modules/**',
11-
'!**/vendor/**',
12-
'!**/vendor/**',
11+
'!**/vendor/**'
1312
],
13+
rootDir: '..',
14+
silent: true
1415
}

0 commit comments

Comments
 (0)