From 2690f4934c7f983cb0b2eaac0f1fd3a1ee291280 Mon Sep 17 00:00:00 2001 From: Daniel Salazar Date: Mon, 21 Sep 2026 23:33:23 -0700 Subject: [PATCH] fix: tests (#3922) --- src/puter-js/src/modules/Auth.test.js | 133 ++++++++++++++++++++------ 1 file changed, 103 insertions(+), 30 deletions(-) diff --git a/src/puter-js/src/modules/Auth.test.js b/src/puter-js/src/modules/Auth.test.js index d7e8c5d71..5b7c574ac 100644 --- a/src/puter-js/src/modules/Auth.test.js +++ b/src/puter-js/src/modules/Auth.test.js @@ -1,48 +1,121 @@ -import { describe, expect, it, vi } from 'vitest'; -import { AuthModule } from './Auth.js'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +const { fetchUrlMock } = vi.hoisted(() => ({ fetchUrlMock: vi.fn() })); +vi.mock('../lib/networkUtils.js', () => ({ fetchUrl: fetchUrlMock })); + +const { AuthModule } = await import('./Auth.js'); + +const API_ORIGIN = 'https://api.test'; const picture = 'data:image/png;base64,iVBORw0KGgo='; +const profile = { picture, displayName: 'Alice', bio: null }; -const makeAuth = () => { - const puter = { - authToken: 'test-token', - fs: { read: vi.fn().mockResolvedValue(new Blob([JSON.stringify({ picture })])) }, - }; - return new AuthModule(puter); +/** A `fetchUrl` response stub. */ +const respond = (body, ok = true) => ({ ok, json: async () => body }); + +const makeAuth = ({ authToken = 'test-token' } = {}) => + new AuthModule({ authToken, APIOrigin: API_ORIGIN }); + +/** The URL and options of the only request made. */ +const onlyRequest = () => { + expect(fetchUrlMock).toHaveBeenCalledTimes(1); + const [url, opts] = fetchUrlMock.mock.calls[0]; + return { url: new URL(url), opts }; }; -describe('getProfilePicture', () => { - it('reads only the requested public profile and returns its picture', async () => { +beforeEach(() => { + fetchUrlMock.mockReset(); + fetchUrlMock.mockResolvedValue(respond(profile)); +}); + +describe('getProfile', () => { + it('asks the API for the named user, with the session attached and no sign-in UI', async () => { const auth = makeAuth(); - expect(await auth.getProfilePicture('alice')).toBe(picture); - expect(auth.puter.fs.read).toHaveBeenCalledExactlyOnceWith('/alice/Public/.profile'); + expect(await auth.getProfile('alice')).toEqual(profile); + const { url, opts } = onlyRequest(); + expect(url.origin + url.pathname).toBe(`${API_ORIGIN}/profile`); + expect(url.searchParams.get('username')).toBe('alice'); + expect(opts).toMatchObject({ includePuterAuth: true, interactiveReauth: false }); }); - it.each([null, '', ' ', 42, {}, [], '.', '..', '../alice', 'alice/Public', 'alice\\Public', 'alice\u0000'])( - 'returns null for an invalid username (%j) without reading a file', async username => { - const auth = makeAuth(); - expect(await auth.getProfilePicture(username)).toBeNull(); - expect(auth.puter.fs.read).not.toHaveBeenCalled(); + it('asks for the signed-in user when no username is given', async () => { + const auth = makeAuth(); + expect(await auth.getProfile()).toEqual(profile); + expect(onlyRequest().url.searchParams.has('username')).toBe(false); + }); + + it.each([null, '', ' ', 42, {}, [], '.', '..', '../alice', 'alice/Public', 'alice\\Public', 'alice '])( + 'returns null for an invalid username (%j) without a request', async username => { + expect(await makeAuth().getProfile(username)).toBeNull(); + expect(fetchUrlMock).not.toHaveBeenCalled(); }, ); - it('returns null without reading a file when signed out', async () => { - const auth = makeAuth(); - auth.puter.authToken = null; - expect(await auth.getProfilePicture('alice')).toBeNull(); - expect(await auth.getProfilePicture()).toBeNull(); - expect(auth.puter.fs.read).not.toHaveBeenCalled(); + it('returns null for the signed-in user when signed out, without a request', async () => { + expect(await makeAuth({ authToken: null }).getProfile()).toBeNull(); + expect(fetchUrlMock).not.toHaveBeenCalled(); }); - it('returns null when the file request fails', async () => { - const auth = makeAuth(); - auth.puter.fs.read.mockRejectedValue(new Error('Network unavailable')); - expect(await auth.getProfilePicture('alice')).toBeNull(); + it('still asks for a named user when signed out, since public profiles need no session', async () => { + expect(await makeAuth({ authToken: null }).getProfile('alice')).toEqual(profile); + expect(fetchUrlMock).toHaveBeenCalledTimes(1); }); - it('returns null when reading the blob fails', async () => { + it.each([ + ['a hidden or missing profile (404)', () => respond({ code: 'not_found' }, false)], + ['a non-object body', () => respond([])], + ['a request failure', () => Promise.reject(new Error('Network unavailable'))], + ])('returns null on %s', async (_label, response) => { + fetchUrlMock.mockImplementation(response); + expect(await makeAuth().getProfile('alice')).toBeNull(); + }); +}); + +describe('getProfilePicture', () => { + it('returns the picture from the profile', async () => { + expect(await makeAuth().getProfilePicture('alice')).toBe(picture); + expect(onlyRequest().url.searchParams.get('username')).toBe('alice'); + }); + + it.each([ + null, + '', + 'https://example.com/avatar.png', + 'data:text/html;base64,SGk=', + 'data:image/png;base64,', + 'data:image/png;base64,%%%', + ])('returns null for a picture that is not an image data URL (%j)', async value => { + fetchUrlMock.mockResolvedValue(respond({ ...profile, picture: value })); + expect(await makeAuth().getProfilePicture('alice')).toBeNull(); + }); + + it('returns null when the profile is unavailable', async () => { + fetchUrlMock.mockResolvedValue(respond({ code: 'not_found' }, false)); + expect(await makeAuth().getProfilePicture('alice')).toBeNull(); + }); +}); + +describe('updateProfile', () => { + it('posts the patch as JSON with the session and returns the stored profile', async () => { const auth = makeAuth(); - auth.puter.fs.read.mockResolvedValue({ text: async () => { throw new Error('Read failed'); } }); - expect(await auth.getProfilePicture('alice')).toBeNull(); + expect(await auth.updateProfile({ displayName: 'Alice' })).toEqual(profile); + const { url, opts } = onlyRequest(); + expect(url.href).toBe(`${API_ORIGIN}/profile`); + expect(opts).toMatchObject({ + method: 'POST', + includePuterAuth: true, + headers: { 'Content-Type': 'application/json' }, + }); + expect(JSON.parse(opts.body)).toEqual({ displayName: 'Alice' }); + }); + + it.each([null, 'x', 42, ['picture']])('rejects a patch that is not an object (%j) without a request', async patch => { + await expect(makeAuth().updateProfile(patch)).rejects.toMatchObject({ code: 'profile_patch_invalid' }); + expect(fetchUrlMock).not.toHaveBeenCalled(); + }); + + it('rejects with the backend body when the API refuses the patch', async () => { + const refusal = { code: 'profile_field_not_allowed', message: 'Unknown profile field: name', field: 'name' }; + fetchUrlMock.mockResolvedValue(respond(refusal, false)); + await expect(makeAuth().updateProfile({ name: 'x' })).rejects.toEqual(refusal); }); });