Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions packages/blob/jest/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@
// but they are available everywhere else.
// See https://stackoverflow.com/questions/68468203/why-am-i-getting-textencoder-is-not-defined-in-jest
const { TextEncoder, TextDecoder } = require('node:util');
// eslint-disable-next-line import/order -- On purpose to make requiring undici work
const { ReadableStream } = require('node:stream/web');

Object.assign(global, { TextDecoder, TextEncoder, ReadableStream });

const { Request, Response } = require('undici');

Object.assign(global, { Request, Response });
4 changes: 1 addition & 3 deletions packages/blob/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"browser": {
"undici": "./dist/undici-browser.js",
"crypto": "./dist/crypto-browser.js",
"stream": "./dist/stream-browser.js"
},
Expand Down Expand Up @@ -63,8 +62,7 @@
"async-retry": "^1.3.3",
"is-buffer": "^2.0.5",
"is-node-process": "^1.2.0",
"throttleit": "^2.1.0",
"undici": "^5.28.4"
"throttleit": "^2.1.0"
},
"devDependencies": {
"@edge-runtime/jest-environment": "2.3.10",
Expand Down
53 changes: 24 additions & 29 deletions packages/blob/src/api.node.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import undici from 'undici';
import {
BlobAccessError,
BlobNotFoundError,
Expand All @@ -12,6 +11,8 @@ import {
import { BlobError } from './helpers';

describe('api', () => {
const fetchMock = jest.fn();

describe('request api', () => {
const OLD_ENV = process.env;

Expand All @@ -21,16 +22,16 @@ describe('api', () => {
jest.restoreAllMocks();

process.env = { ...OLD_ENV };

globalThis.fetch = fetchMock;
});

it('should throw if no token is provided', async () => {
const fetchMock = jest.spyOn(undici, 'fetch').mockImplementation(
jest.fn().mockResolvedValue({
status: 200,
ok: true,
json: () => Promise.resolve({ success: true }),
}),
);
fetchMock.mockResolvedValueOnce({
status: 200,
ok: true,
json: () => Promise.resolve({ success: true }),
});

process.env.BLOB_READ_WRITE_TOKEN = undefined;

Expand All @@ -42,13 +43,11 @@ describe('api', () => {
});

it('should not retry successful request', async () => {
const fetchMock = jest.spyOn(undici, 'fetch').mockImplementation(
jest.fn().mockResolvedValue({
status: 200,
ok: true,
json: () => Promise.resolve({ success: true }),
}),
);
fetchMock.mockResolvedValueOnce({
status: 200,
ok: true,
json: () => Promise.resolve({ success: true }),
});

const res = await requestApi<{ success: boolean }>(
'/method',
Expand Down Expand Up @@ -81,13 +80,11 @@ describe('api', () => {
])(
`should retry '%s %s' error response`,
async (status, code, error) => {
const fetchMock = jest.spyOn(undici, 'fetch').mockImplementation(
jest.fn().mockResolvedValue({
status,
ok: false,
json: () => Promise.resolve({ error: { code } }),
}),
);
fetchMock.mockResolvedValue({
status,
ok: false,
json: () => Promise.resolve({ error: { code } }),
});

process.env.VERCEL_BLOB_RETRIES = '1';
process.env.BLOB_READ_WRITE_TOKEN = 'test-token';
Expand Down Expand Up @@ -118,13 +115,11 @@ describe('api', () => {
])(
`should not retry '%s %s' response error response`,
async (status, code, error, message = '') => {
const fetchMock = jest.spyOn(undici, 'fetch').mockImplementation(
jest.fn().mockResolvedValue({
status,
ok: false,
json: () => Promise.resolve({ error: { code, message } }),
}),
);
fetchMock.mockResolvedValue({
status,
ok: false,
json: () => Promise.resolve({ error: { code, message } }),
});

await expect(
requestApi('/api', { method: 'GET' }, { token: '123' }),
Expand Down
1 change: 0 additions & 1 deletion packages/blob/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { Response } from 'undici';
import retry from 'async-retry';
import isNetworkError from './is-network-error';
import { debug } from './debug';
Expand Down
181 changes: 87 additions & 94 deletions packages/blob/src/client.browser.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import undici from 'undici';
import {
completeMultipartUpload,
createMultipartUpload,
Expand All @@ -9,6 +8,7 @@ import {
} from './client';

describe('client', () => {
const fetchMock = jest.fn();
let requestId = '';

beforeEach(() => {
Expand All @@ -24,39 +24,39 @@ describe('client', () => {

jest.spyOn(global.Math, 'random').mockReturnValue(Math.random());
requestId = Math.random().toString(16).slice(2);

global.fetch = fetchMock;
});

afterEach(() => {
jest.spyOn(global.Math, 'random').mockRestore();
});

describe('upload()', () => {
it('should upload a file from the client', async () => {
const fetchMock = jest.spyOn(undici, 'fetch').mockImplementation(
jest
.fn()
.mockResolvedValueOnce({
status: 200,
ok: true,
json: () =>
Promise.resolve({
type: 'blob.generate-client-token',
clientToken: 'vercel_blob_client_fake_123',
}),
})
.mockResolvedValueOnce({
status: 200,
ok: true,
json: () =>
Promise.resolve({
url: `https://storeId.public.blob.vercel-storage.com/superfoo.txt`,
downloadUrl: `https://storeId.public.blob.vercel-storage.com/superfoo.txt?download=1`,
pathname: 'foo.txt',
contentType: 'text/plain',
contentDisposition: 'attachment; filename="foo.txt"',
}),
}),
);
it.only('should upload a file from the client', async () => {
fetchMock
.mockResolvedValueOnce({
status: 200,
ok: true,
json: () =>
Promise.resolve({
type: 'blob.generate-client-token',
clientToken: 'vercel_blob_client_fake_123',
}),
})
.mockResolvedValueOnce({
status: 200,
ok: true,
json: () =>
Promise.resolve({
url: 'https://storeId.public.blob.vercel-storage.com/superfoo.txt',
downloadUrl:
'https://storeId.public.blob.vercel-storage.com/superfoo.txt?download=1',
pathname: 'foo.txt',
contentType: 'text/plain',
contentDisposition: 'attachment; filename="foo.txt"',
}),
});

await expect(
upload('foo.txt', 'Test file data', {
Expand Down Expand Up @@ -118,36 +118,33 @@ describe('client', () => {
});

it('should upload a file using the manual functions', async () => {
const fetchMock = jest.spyOn(undici, 'fetch').mockImplementation(
jest
.fn()
.mockResolvedValueOnce({
status: 200,
ok: true,
json: () => Promise.resolve({ key: 'key', uploadId: 'uploadId' }),
})
.mockResolvedValueOnce({
status: 200,
ok: true,
json: () => Promise.resolve({ etag: 'etag1' }),
})
.mockResolvedValueOnce({
status: 200,
ok: true,
json: () => Promise.resolve({ etag: 'etag2' }),
})
.mockResolvedValueOnce({
status: 200,
ok: true,
json: () =>
Promise.resolve({
url: `https://storeId.public.blob.vercel-storage.com/foo.txt`,
pathname: 'foo.txt',
contentType: 'text/plain',
contentDisposition: 'attachment; filename="foo.txt"',
}),
}),
);
fetchMock
.mockResolvedValueOnce({
status: 200,
ok: true,
json: () => Promise.resolve({ key: 'key', uploadId: 'uploadId' }),
})
.mockResolvedValueOnce({
status: 200,
ok: true,
json: () => Promise.resolve({ etag: 'etag1' }),
})
.mockResolvedValueOnce({
status: 200,
ok: true,
json: () => Promise.resolve({ etag: 'etag2' }),
})
.mockResolvedValueOnce({
status: 200,
ok: true,
json: () =>
Promise.resolve({
url: 'https://storeId.public.blob.vercel-storage.com/foo.txt',
pathname: 'foo.txt',
contentType: 'text/plain',
contentDisposition: 'attachment; filename="foo.txt"',
}),
});

const pathname = 'foo.txt';
const token = 'vercel_blob_client_fake_token_for_test';
Expand Down Expand Up @@ -278,36 +275,33 @@ describe('client', () => {
});

it('should upload a file using the uploader', async () => {
const fetchMock = jest.spyOn(undici, 'fetch').mockImplementation(
jest
.fn()
.mockResolvedValueOnce({
status: 200,
ok: true,
json: () => Promise.resolve({ key: 'key', uploadId: 'uploadId' }),
})
.mockResolvedValueOnce({
status: 200,
ok: true,
json: () => Promise.resolve({ etag: 'etag1' }),
})
.mockResolvedValueOnce({
status: 200,
ok: true,
json: () => Promise.resolve({ etag: 'etag2' }),
})
.mockResolvedValueOnce({
status: 200,
ok: true,
json: () =>
Promise.resolve({
url: `https://storeId.public.blob.vercel-storage.com/foo.txt`,
pathname: 'foo.txt',
contentType: 'text/plain',
contentDisposition: 'attachment; filename="foo.txt"',
}),
}),
);
fetchMock
.mockResolvedValueOnce({
status: 200,
ok: true,
json: () => Promise.resolve({ key: 'key', uploadId: 'uploadId' }),
})
.mockResolvedValueOnce({
status: 200,
ok: true,
json: () => Promise.resolve({ etag: 'etag1' }),
})
.mockResolvedValueOnce({
status: 200,
ok: true,
json: () => Promise.resolve({ etag: 'etag2' }),
})
.mockResolvedValueOnce({
status: 200,
ok: true,
json: () =>
Promise.resolve({
url: 'https://storeId.public.blob.vercel-storage.com/foo.txt',
pathname: 'foo.txt',
contentType: 'text/plain',
contentDisposition: 'attachment; filename="foo.txt"',
}),
});

const pathname = 'foo.txt';
const token = 'vercel_blob_client_fake_token_for_test';
Expand Down Expand Up @@ -421,13 +415,11 @@ describe('client', () => {

it('should reject incorrect body in uploader.uploadPart()', async () => {
// Mock the createMultipartUploader to return a minimal uploader object
jest.spyOn(undici, 'fetch').mockImplementation(
jest.fn().mockResolvedValueOnce({
status: 200,
ok: true,
json: () => Promise.resolve({ key: 'key', uploadId: 'uploadId' }),
}),
);
fetchMock.mockResolvedValueOnce({
status: 200,
ok: true,
json: () => Promise.resolve({ key: 'key', uploadId: 'uploadId' }),
});

const uploader = await createMultipartUploader('foo.txt', {
access: 'public',
Expand Down Expand Up @@ -469,6 +461,7 @@ describe('client', () => {
{
access: 'public',
multipart: true,
token: '123',
},
),
],
Expand Down
6 changes: 1 addition & 5 deletions packages/blob/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
// eslint-disable-next-line unicorn/prefer-node-protocol -- node:crypto does not resolve correctly in browser and edge runtime
import * as crypto from 'crypto';
import type { IncomingMessage } from 'node:http';
// When bundled via a bundler supporting the `browser` field, then
// the `undici` module will be replaced with https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
// for browser contexts. See ./undici-browser.js and ./package.json
import { fetch } from 'undici';
import type { BlobCommandOptions, WithUploadProgress } from './helpers';
import { BlobError, getTokenFromOptionsOrEnv } from './helpers';
import { createPutMethod } from './put';
Expand Down Expand Up @@ -384,7 +380,7 @@ function hexToArrayByte(input: string): Buffer {
const view = new Uint8Array(input.length / 2);

for (let i = 0; i < input.length; i += 2) {
view[i / 2] = parseInt(input.substring(i, i + 2), 16);
view[i / 2] = Number.parseInt(input.substring(i, i + 2), 16);
}

return Buffer.from(view);
Expand Down
Loading