Skip to content

Commit 8d85b62

Browse files
TortoiseWolfeclaude
andcommitted
fix(avatar): use getSession instead of getUser to fix 'Auth session missing'
ROOT CAUSE: Avatar upload was calling supabase.auth.getUser() which can return cached user data even when the session is stale. Then when supabase.auth.updateUser() was called, it failed with "Auth session missing" because updateUser requires an active session with valid tokens. FIX: Changed uploadAvatar() and removeAvatar() to use getSession() first, which validates the session is active before proceeding. This ensures the Supabase client has valid session tokens for subsequent API calls. This fixes E2E test failures in: - avatar-upload-Avatar-Upload-Replace-existing-avatar (all retries) Also updated unit tests to mock getSession instead of getUser. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent f2114fd commit 8d85b62

2 files changed

Lines changed: 43 additions & 36 deletions

File tree

src/lib/avatar/__tests__/upload.test.ts

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
} from '../upload';
1313

1414
// Create persistent mock objects using vi.hoisted()
15-
const mockGetUser = vi.fn();
15+
const mockGetSession = vi.fn();
1616
const mockUpdateUser = vi.fn();
1717
const mockUpload = vi.fn();
1818
const mockRemove = vi.fn();
@@ -35,7 +35,7 @@ const mockDbFrom = vi.fn(() => ({
3535
vi.mock('@/lib/supabase/client', () => ({
3636
createClient: () => ({
3737
auth: {
38-
getUser: mockGetUser,
38+
getSession: mockGetSession,
3939
updateUser: mockUpdateUser,
4040
},
4141
storage: {
@@ -78,9 +78,9 @@ describe('uploadAvatar', () => {
7878
vi.clearAllMocks();
7979
});
8080

81-
it('should return error if user not authenticated', async () => {
82-
mockGetUser.mockResolvedValue({
83-
data: { user: null },
81+
it('should return error if session missing', async () => {
82+
mockGetSession.mockResolvedValue({
83+
data: { session: null },
8484
error: {
8585
message: 'Not authenticated',
8686
name: 'AuthError',
@@ -92,13 +92,13 @@ describe('uploadAvatar', () => {
9292
const result = await uploadAvatar(blob);
9393

9494
expect(result.url).toBe('');
95-
expect(result.error).toContain('not authenticated');
95+
expect(result.error).toContain('session missing');
9696
});
9797

9898
it('should handle upload errors', async () => {
99-
mockGetUser.mockResolvedValue({
99+
mockGetSession.mockResolvedValue({
100100
data: {
101-
user: { id: 'user-123', user_metadata: {} },
101+
session: { user: { id: 'user-123', user_metadata: {} } },
102102
},
103103
error: null,
104104
});
@@ -116,9 +116,9 @@ describe('uploadAvatar', () => {
116116
});
117117

118118
it('should rollback upload if profile update fails', async () => {
119-
mockGetUser.mockResolvedValue({
119+
mockGetSession.mockResolvedValue({
120120
data: {
121-
user: { id: 'user-123', user_metadata: {} },
121+
session: { user: { id: 'user-123', user_metadata: {} } },
122122
},
123123
error: null,
124124
});
@@ -157,9 +157,9 @@ describe('removeAvatar', () => {
157157
vi.clearAllMocks();
158158
});
159159

160-
it('should return error if user not authenticated', async () => {
161-
mockGetUser.mockResolvedValue({
162-
data: { user: null },
160+
it('should return error if session missing', async () => {
161+
mockGetSession.mockResolvedValue({
162+
data: { session: null },
163163
error: {
164164
message: 'Not authenticated',
165165
name: 'AuthError',
@@ -169,13 +169,13 @@ describe('removeAvatar', () => {
169169

170170
const result = await removeAvatar();
171171

172-
expect(result.error).toContain('not authenticated');
172+
expect(result.error).toContain('session missing');
173173
});
174174

175175
it('should return success if no avatar exists', async () => {
176-
mockGetUser.mockResolvedValue({
176+
mockGetSession.mockResolvedValue({
177177
data: {
178-
user: { id: 'user-123', user_metadata: {} },
178+
session: { user: { id: 'user-123', user_metadata: {} } },
179179
},
180180
error: null,
181181
});
@@ -186,11 +186,13 @@ describe('removeAvatar', () => {
186186
});
187187

188188
it('should handle profile update errors', async () => {
189-
mockGetUser.mockResolvedValue({
189+
mockGetSession.mockResolvedValue({
190190
data: {
191-
user: {
192-
id: 'user-123',
193-
user_metadata: { avatar_url: 'https://example.com/avatar.webp' },
191+
session: {
192+
user: {
193+
id: 'user-123',
194+
user_metadata: { avatar_url: 'https://example.com/avatar.webp' },
195+
},
194196
},
195197
},
196198
error: null,
@@ -217,9 +219,9 @@ describe('uploadWithRetry', () => {
217219
});
218220

219221
it('should retry failed uploads', async () => {
220-
mockGetUser.mockResolvedValue({
222+
mockGetSession.mockResolvedValue({
221223
data: {
222-
user: { id: 'user-123', user_metadata: {} },
224+
session: { user: { id: 'user-123', user_metadata: {} } },
223225
},
224226
error: null,
225227
});
@@ -256,8 +258,8 @@ describe('uploadWithRetry', () => {
256258
}, 10000); // Increase timeout for retries
257259

258260
it('should not retry authentication errors', async () => {
259-
mockGetUser.mockResolvedValue({
260-
data: { user: null },
261+
mockGetSession.mockResolvedValue({
262+
data: { session: null },
261263
error: {
262264
message: 'Not authenticated',
263265
name: 'AuthError',
@@ -269,7 +271,7 @@ describe('uploadWithRetry', () => {
269271
const result = await uploadWithRetry(blob, 3);
270272

271273
expect(result.url).toBe('');
272-
expect(result.error).toContain('authenticated');
274+
expect(result.error).toContain('session missing');
273275
// Should fail immediately without retries
274276
});
275277
});

src/lib/avatar/upload.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,19 @@ export async function uploadAvatar(
2727
const supabase = createClient();
2828

2929
try {
30-
// Step 1: Get current user
30+
// Step 1: Ensure we have an active session (not just cached user)
31+
// getSession() validates the session is active, while getUser() can return cached data
3132
const {
32-
data: { user },
33-
error: authError,
34-
} = await supabase.auth.getUser();
33+
data: { session },
34+
error: sessionError,
35+
} = await supabase.auth.getSession();
3536

36-
if (authError || !user) {
37-
return { url: '', error: 'User not authenticated' };
37+
if (sessionError || !session) {
38+
return { url: '', error: 'Auth session missing - please sign in again' };
3839
}
3940

41+
const user = session.user;
42+
4043
// Store old avatar URL for cleanup
4144
const oldAvatarUrl = user.user_metadata?.avatar_url as string | undefined;
4245

@@ -120,15 +123,17 @@ export async function removeAvatar(): Promise<RemoveAvatarResult> {
120123
const supabase = createClient();
121124

122125
try {
126+
// Ensure we have an active session (not just cached user)
123127
const {
124-
data: { user },
125-
error: authError,
126-
} = await supabase.auth.getUser();
128+
data: { session },
129+
error: sessionError,
130+
} = await supabase.auth.getSession();
127131

128-
if (authError || !user) {
129-
return { error: 'User not authenticated' };
132+
if (sessionError || !session) {
133+
return { error: 'Auth session missing - please sign in again' };
130134
}
131135

136+
const user = session.user;
132137
const avatarUrl = user.user_metadata?.avatar_url as string | undefined;
133138

134139
if (!avatarUrl) {

0 commit comments

Comments
 (0)