mirror of
https://github.com/oven-sh/bun
synced 2026-02-04 16:08:53 +00:00
Compare commits
8 Commits
claude/imp
...
fix-covera
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c0c35529a | ||
|
|
f0d4838e9f | ||
|
|
48ebc15e63 | ||
|
|
2e8e7a000c | ||
|
|
c1584b8a35 | ||
|
|
a0f13ea5bb | ||
|
|
c2bd4095eb | ||
|
|
0a7313e66c |
@@ -667,6 +667,7 @@ src/http/ETag.zig
|
||||
src/http/FetchRedirect.zig
|
||||
src/http/HeaderBuilder.zig
|
||||
src/http/Headers.zig
|
||||
src/http/HeaderValueIterator.zig
|
||||
src/http/HTTPCertError.zig
|
||||
src/http/HTTPContext.zig
|
||||
src/http/HTTPRequestBody.zig
|
||||
|
||||
30
packages/bun-types/test.d.ts
vendored
30
packages/bun-types/test.d.ts
vendored
@@ -152,11 +152,41 @@ declare module "bun:test" {
|
||||
type SpiedSetter<T> = JestMock.SpiedSetter<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a spy on an object property or method
|
||||
*/
|
||||
export function spyOn<T extends object, K extends keyof T>(
|
||||
obj: T,
|
||||
methodOrPropertyValue: K,
|
||||
): Mock<Extract<T[K], (...args: any[]) => any>>;
|
||||
|
||||
/**
|
||||
* Vitest-compatible mocking utilities
|
||||
* Provides Vitest-style mocking API for easier migration from Vitest to Bun
|
||||
*/
|
||||
export const vi: {
|
||||
/**
|
||||
* Create a mock function
|
||||
*/
|
||||
fn: typeof jest.fn;
|
||||
/**
|
||||
* Create a spy on an object property or method
|
||||
*/
|
||||
spyOn: typeof spyOn;
|
||||
/**
|
||||
* Mock a module
|
||||
*/
|
||||
module: typeof mock.module;
|
||||
/**
|
||||
* Restore all mocks to their original implementation
|
||||
*/
|
||||
restoreAllMocks: typeof jest.restoreAllMocks;
|
||||
/**
|
||||
* Clear all mock state (calls, results, etc.) without restoring original implementation
|
||||
*/
|
||||
clearAllMocks: typeof jest.clearAllMocks;
|
||||
};
|
||||
|
||||
interface FunctionLike {
|
||||
readonly name: string;
|
||||
}
|
||||
|
||||
@@ -2050,6 +2050,12 @@ pub const Formatter = struct {
|
||||
return error.JSError;
|
||||
}
|
||||
|
||||
// If we call
|
||||
// `return try this.printAs`
|
||||
//
|
||||
// Then we can get a spurious `[Circular]` due to the value already being present in the map.
|
||||
var remove_before_recurse = false;
|
||||
|
||||
var writer = WrappedWriter(Writer){ .ctx = writer_, .estimated_line_length = &this.estimated_line_length };
|
||||
defer {
|
||||
if (writer.failed) {
|
||||
@@ -2075,12 +2081,16 @@ pub const Formatter = struct {
|
||||
if (entry.found_existing) {
|
||||
writer.writeAll(comptime Output.prettyFmt("<r><cyan>[Circular]<r>", enable_ansi_colors));
|
||||
return;
|
||||
} else {
|
||||
remove_before_recurse = true;
|
||||
}
|
||||
}
|
||||
|
||||
defer {
|
||||
if (comptime Format.canHaveCircularReferences()) {
|
||||
_ = this.map.remove(value);
|
||||
if (remove_before_recurse) {
|
||||
_ = this.map.remove(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2617,12 +2627,25 @@ pub const Formatter = struct {
|
||||
} else if (try JestPrettyFormat.printAsymmetricMatcher(this, Format, &writer, writer_, name_buf, value, enable_ansi_colors)) {
|
||||
return;
|
||||
} else if (jsType != .DOMWrapper) {
|
||||
if (remove_before_recurse) {
|
||||
remove_before_recurse = false;
|
||||
_ = this.map.remove(value);
|
||||
}
|
||||
|
||||
if (value.isCallable()) {
|
||||
remove_before_recurse = true;
|
||||
return try this.printAs(.Function, Writer, writer_, value, jsType, enable_ansi_colors);
|
||||
}
|
||||
|
||||
remove_before_recurse = true;
|
||||
return try this.printAs(.Object, Writer, writer_, value, jsType, enable_ansi_colors);
|
||||
}
|
||||
if (remove_before_recurse) {
|
||||
remove_before_recurse = false;
|
||||
_ = this.map.remove(value);
|
||||
}
|
||||
|
||||
remove_before_recurse = true;
|
||||
return try this.printAs(.Object, Writer, writer_, value, .Event, enable_ansi_colors);
|
||||
},
|
||||
.NativeCode => {
|
||||
@@ -2887,6 +2910,12 @@ pub const Formatter = struct {
|
||||
const event_type = switch (try EventType.map.fromJS(this.globalThis, event_type_value) orelse .unknown) {
|
||||
.MessageEvent, .ErrorEvent => |evt| evt,
|
||||
else => {
|
||||
if (remove_before_recurse) {
|
||||
_ = this.map.remove(value);
|
||||
}
|
||||
|
||||
// We must potentially remove it again.
|
||||
remove_before_recurse = true;
|
||||
return try this.printAs(.Object, Writer, writer_, value, .Event, enable_ansi_colors);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -178,7 +178,10 @@ pub const HTMLRewriter = struct {
|
||||
return global.throwInvalidArguments("Response body already used", .{});
|
||||
}
|
||||
const out = try this.beginTransform(global, response);
|
||||
if (out.toError()) |err| return global.throwValue(err);
|
||||
// Check if the returned value is an error and throw it properly
|
||||
if (out.toError()) |err| {
|
||||
return global.throwValue(err);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -203,6 +206,10 @@ pub const HTMLRewriter = struct {
|
||||
});
|
||||
defer resp.finalize();
|
||||
const out_response_value = try this.beginTransform(global, resp);
|
||||
// Check if the returned value is an error and throw it properly
|
||||
if (out_response_value.toError()) |err| {
|
||||
return global.throwValue(err);
|
||||
}
|
||||
out_response_value.ensureStillAlive();
|
||||
var out_response = out_response_value.as(Response) orelse return out_response_value;
|
||||
var blob = out_response.body.value.useAsAnyBlobAllowNonUTF8String();
|
||||
|
||||
@@ -1352,39 +1352,39 @@ void WebSocket::didFailWithErrorCode(Bun::WebSocketErrorCode code)
|
||||
break;
|
||||
}
|
||||
case Bun::WebSocketErrorCode::invalid_response: {
|
||||
didReceiveClose(CleanStatus::NotClean, 1002, "Invalid response"_s);
|
||||
didReceiveClose(CleanStatus::NotClean, 1002, "Invalid response"_s, true);
|
||||
break;
|
||||
}
|
||||
case Bun::WebSocketErrorCode::expected_101_status_code: {
|
||||
didReceiveClose(CleanStatus::NotClean, 1002, "Expected 101 status code"_s);
|
||||
didReceiveClose(CleanStatus::NotClean, 1002, "Expected 101 status code"_s, true);
|
||||
break;
|
||||
}
|
||||
case Bun::WebSocketErrorCode::missing_upgrade_header: {
|
||||
didReceiveClose(CleanStatus::NotClean, 1002, "Missing upgrade header"_s);
|
||||
didReceiveClose(CleanStatus::NotClean, 1002, "Missing upgrade header"_s, true);
|
||||
break;
|
||||
}
|
||||
case Bun::WebSocketErrorCode::missing_connection_header: {
|
||||
didReceiveClose(CleanStatus::NotClean, 1002, "Missing connection header"_s);
|
||||
didReceiveClose(CleanStatus::NotClean, 1002, "Missing connection header"_s, true);
|
||||
break;
|
||||
}
|
||||
case Bun::WebSocketErrorCode::missing_websocket_accept_header: {
|
||||
didReceiveClose(CleanStatus::NotClean, 1002, "Missing websocket accept header"_s);
|
||||
didReceiveClose(CleanStatus::NotClean, 1002, "Missing websocket accept header"_s, true);
|
||||
break;
|
||||
}
|
||||
case Bun::WebSocketErrorCode::invalid_upgrade_header: {
|
||||
didReceiveClose(CleanStatus::NotClean, 1002, "Invalid upgrade header"_s);
|
||||
didReceiveClose(CleanStatus::NotClean, 1002, "Invalid upgrade header"_s, true);
|
||||
break;
|
||||
}
|
||||
case Bun::WebSocketErrorCode::invalid_connection_header: {
|
||||
didReceiveClose(CleanStatus::NotClean, 1002, "Invalid connection header"_s);
|
||||
didReceiveClose(CleanStatus::NotClean, 1002, "Invalid connection header"_s, true);
|
||||
break;
|
||||
}
|
||||
case Bun::WebSocketErrorCode::invalid_websocket_version: {
|
||||
didReceiveClose(CleanStatus::NotClean, 1002, "Invalid websocket version"_s);
|
||||
didReceiveClose(CleanStatus::NotClean, 1002, "Invalid websocket version"_s, true);
|
||||
break;
|
||||
}
|
||||
case Bun::WebSocketErrorCode::mismatch_websocket_accept_header: {
|
||||
didReceiveClose(CleanStatus::NotClean, 1002, "Mismatch websocket accept header"_s);
|
||||
didReceiveClose(CleanStatus::NotClean, 1002, "Mismatch websocket accept header"_s, true);
|
||||
break;
|
||||
}
|
||||
case Bun::WebSocketErrorCode::missing_client_protocol: {
|
||||
@@ -1412,11 +1412,11 @@ void WebSocket::didFailWithErrorCode(Bun::WebSocketErrorCode code)
|
||||
break;
|
||||
}
|
||||
case Bun::WebSocketErrorCode::headers_too_large: {
|
||||
didReceiveClose(CleanStatus::NotClean, 1007, "Headers too large"_s);
|
||||
didReceiveClose(CleanStatus::NotClean, 1007, "Headers too large"_s, true);
|
||||
break;
|
||||
}
|
||||
case Bun::WebSocketErrorCode::ended: {
|
||||
didReceiveClose(CleanStatus::NotClean, 1006, "Connection ended"_s);
|
||||
didReceiveClose(CleanStatus::NotClean, 1006, "Connection ended"_s, true);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1457,7 +1457,7 @@ void WebSocket::didFailWithErrorCode(Bun::WebSocketErrorCode code)
|
||||
break;
|
||||
}
|
||||
case Bun::WebSocketErrorCode::tls_handshake_failed: {
|
||||
didReceiveClose(CleanStatus::NotClean, 1015, "TLS handshake failed"_s);
|
||||
didReceiveClose(CleanStatus::NotClean, 1015, "TLS handshake failed"_s, true);
|
||||
break;
|
||||
}
|
||||
case Bun::WebSocketErrorCode::message_too_big: {
|
||||
@@ -1596,3 +1596,13 @@ WebCore::ExceptionOr<void> WebCore::WebSocket::pong(WebCore::JSBlob* blob)
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void WebCore::WebSocket::setProtocol(const String& protocol)
|
||||
{
|
||||
m_subprotocol = protocol;
|
||||
}
|
||||
|
||||
extern "C" void WebSocket__setProtocol(WebCore::WebSocket* webSocket, BunString* protocol)
|
||||
{
|
||||
webSocket->setProtocol(protocol->transferToWTFString());
|
||||
}
|
||||
|
||||
@@ -116,6 +116,8 @@ public:
|
||||
ExceptionOr<void> close(std::optional<unsigned short> code, const String& reason);
|
||||
ExceptionOr<void> terminate();
|
||||
|
||||
void setProtocol(const String& protocol);
|
||||
|
||||
const URL& url() const;
|
||||
State readyState() const;
|
||||
unsigned bufferedAmount() const;
|
||||
|
||||
@@ -498,11 +498,13 @@ pub const Jest = struct {
|
||||
mockFn.put(globalObject, ZigString.static("restore"), restoreAllMocks);
|
||||
mockFn.put(globalObject, ZigString.static("clearAllMocks"), clearAllMocks);
|
||||
|
||||
const jest = JSValue.createEmptyObject(globalObject, 8);
|
||||
const jest = JSValue.createEmptyObject(globalObject, 10);
|
||||
jest.put(globalObject, ZigString.static("fn"), mockFn);
|
||||
jest.put(globalObject, ZigString.static("mock"), mockModuleFn);
|
||||
jest.put(globalObject, ZigString.static("spyOn"), spyOn);
|
||||
jest.put(globalObject, ZigString.static("restoreAllMocks"), restoreAllMocks);
|
||||
jest.put(globalObject, ZigString.static("clearAllMocks"), clearAllMocks);
|
||||
jest.put(globalObject, ZigString.static("resetAllMocks"), clearAllMocks);
|
||||
jest.put(
|
||||
globalObject,
|
||||
ZigString.static("setSystemTime"),
|
||||
@@ -529,10 +531,10 @@ pub const Jest = struct {
|
||||
Expect.js.getConstructor(globalObject),
|
||||
);
|
||||
|
||||
const vi = JSValue.createEmptyObject(globalObject, 3);
|
||||
const vi = JSValue.createEmptyObject(globalObject, 5);
|
||||
vi.put(globalObject, ZigString.static("fn"), mockFn);
|
||||
vi.put(globalObject, ZigString.static("mock"), mockModuleFn);
|
||||
vi.put(globalObject, ZigString.static("spyOn"), spyOn);
|
||||
vi.put(globalObject, ZigString.static("module"), mockModuleFn);
|
||||
vi.put(globalObject, ZigString.static("restoreAllMocks"), restoreAllMocks);
|
||||
vi.put(globalObject, ZigString.static("clearAllMocks"), clearAllMocks);
|
||||
module.put(globalObject, ZigString.static("vi"), vi);
|
||||
|
||||
19
src/bun.zig
19
src/bun.zig
@@ -1734,7 +1734,7 @@ pub const StringSet = struct {
|
||||
|
||||
pub const Map = StringArrayHashMap(void);
|
||||
|
||||
pub fn clone(self: StringSet) !StringSet {
|
||||
pub fn clone(self: *const StringSet) !StringSet {
|
||||
var new_map = Map.init(self.map.allocator);
|
||||
try new_map.ensureTotalCapacity(self.map.count());
|
||||
for (self.map.keys()) |key| {
|
||||
@@ -1751,7 +1751,15 @@ pub const StringSet = struct {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn keys(self: StringSet) []const []const u8 {
|
||||
pub fn isEmpty(self: *const StringSet) bool {
|
||||
return self.count() == 0;
|
||||
}
|
||||
|
||||
pub fn count(self: *const StringSet) usize {
|
||||
return self.map.count();
|
||||
}
|
||||
|
||||
pub fn keys(self: *const StringSet) []const []const u8 {
|
||||
return self.map.keys();
|
||||
}
|
||||
|
||||
@@ -1770,6 +1778,13 @@ pub const StringSet = struct {
|
||||
return self.map.swapRemove(key);
|
||||
}
|
||||
|
||||
pub fn clearAndFree(self: *StringSet) void {
|
||||
for (self.map.keys()) |key| {
|
||||
self.map.allocator.free(key);
|
||||
}
|
||||
self.map.clearAndFree();
|
||||
}
|
||||
|
||||
pub fn deinit(self: *StringSet) void {
|
||||
for (self.map.keys()) |key| {
|
||||
self.map.allocator.free(key);
|
||||
|
||||
@@ -2449,6 +2449,7 @@ pub const FetchRedirect = @import("./http/FetchRedirect.zig").FetchRedirect;
|
||||
pub const InitError = @import("./http/InitError.zig").InitError;
|
||||
pub const HTTPRequestBody = @import("./http/HTTPRequestBody.zig").HTTPRequestBody;
|
||||
pub const SendFile = @import("./http/SendFile.zig");
|
||||
pub const HeaderValueIterator = @import("./http/HeaderValueIterator.zig");
|
||||
|
||||
const string = []const u8;
|
||||
|
||||
|
||||
18
src/http/HeaderValueIterator.zig
Normal file
18
src/http/HeaderValueIterator.zig
Normal file
@@ -0,0 +1,18 @@
|
||||
const HeaderValueIterator = @This();
|
||||
|
||||
iterator: std.mem.TokenIterator(u8, .scalar),
|
||||
|
||||
pub fn init(input: []const u8) HeaderValueIterator {
|
||||
return HeaderValueIterator{
|
||||
.iterator = std.mem.tokenizeScalar(u8, std.mem.trim(u8, input, " \t"), ','),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn next(self: *HeaderValueIterator) ?[]const u8 {
|
||||
const slice = std.mem.trim(u8, self.iterator.next() orelse return null, " \t");
|
||||
if (slice.len == 0) return self.next();
|
||||
|
||||
return slice;
|
||||
}
|
||||
|
||||
const std = @import("std");
|
||||
@@ -58,6 +58,7 @@ pub const CppWebSocket = opaque {
|
||||
}
|
||||
extern fn WebSocket__incrementPendingActivity(websocket_context: *CppWebSocket) void;
|
||||
extern fn WebSocket__decrementPendingActivity(websocket_context: *CppWebSocket) void;
|
||||
extern fn WebSocket__setProtocol(websocket_context: *CppWebSocket, protocol: *bun.String) void;
|
||||
pub fn ref(this: *CppWebSocket) void {
|
||||
jsc.markBinding(@src());
|
||||
WebSocket__incrementPendingActivity(this);
|
||||
@@ -67,6 +68,10 @@ pub const CppWebSocket = opaque {
|
||||
jsc.markBinding(@src());
|
||||
WebSocket__decrementPendingActivity(this);
|
||||
}
|
||||
pub fn setProtocol(this: *CppWebSocket, protocol: *bun.String) void {
|
||||
jsc.markBinding(@src());
|
||||
WebSocket__setProtocol(this, protocol);
|
||||
}
|
||||
};
|
||||
|
||||
const WebSocketDeflate = @import("./WebSocketDeflate.zig");
|
||||
|
||||
@@ -34,15 +34,14 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type {
|
||||
tcp: Socket,
|
||||
outgoing_websocket: ?*CppWebSocket,
|
||||
input_body_buf: []u8 = &[_]u8{},
|
||||
client_protocol: []const u8 = "",
|
||||
to_send: []const u8 = "",
|
||||
read_length: usize = 0,
|
||||
headers_buf: [128]PicoHTTP.Header = undefined,
|
||||
body: std.ArrayListUnmanaged(u8) = .{},
|
||||
websocket_protocol: u64 = 0,
|
||||
hostname: [:0]const u8 = "",
|
||||
poll_ref: Async.KeepAlive = Async.KeepAlive.init(),
|
||||
state: State = .initializing,
|
||||
subprotocols: bun.StringSet,
|
||||
|
||||
const State = enum { initializing, reading, failed };
|
||||
|
||||
@@ -90,7 +89,6 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type {
|
||||
|
||||
bun.assert(vm.event_loop_handle != null);
|
||||
|
||||
var client_protocol_hash: u64 = 0;
|
||||
const body = buildRequestBody(
|
||||
vm,
|
||||
pathname,
|
||||
@@ -98,7 +96,6 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type {
|
||||
host,
|
||||
port,
|
||||
client_protocol,
|
||||
&client_protocol_hash,
|
||||
NonUTF8Headers.init(header_names, header_values, header_count),
|
||||
) catch return null;
|
||||
|
||||
@@ -107,8 +104,15 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type {
|
||||
.tcp = .{ .socket = .{ .detached = {} } },
|
||||
.outgoing_websocket = websocket,
|
||||
.input_body_buf = body,
|
||||
.websocket_protocol = client_protocol_hash,
|
||||
.state = .initializing,
|
||||
.subprotocols = brk: {
|
||||
var subprotocols = bun.StringSet.init(bun.default_allocator);
|
||||
var it = bun.http.HeaderValueIterator.init(client_protocol.slice());
|
||||
while (it.next()) |protocol| {
|
||||
subprotocols.insert(protocol) catch |e| bun.handleOom(e);
|
||||
}
|
||||
break :brk subprotocols;
|
||||
},
|
||||
});
|
||||
|
||||
var host_ = host.toSlice(bun.default_allocator);
|
||||
@@ -162,6 +166,7 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type {
|
||||
pub fn clearData(this: *HTTPClient) void {
|
||||
this.poll_ref.unref(jsc.VirtualMachine.get());
|
||||
|
||||
this.subprotocols.clearAndFree();
|
||||
this.clearInput();
|
||||
this.body.clearAndFree(bun.default_allocator);
|
||||
}
|
||||
@@ -346,7 +351,8 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type {
|
||||
var upgrade_header = PicoHTTP.Header{ .name = "", .value = "" };
|
||||
var connection_header = PicoHTTP.Header{ .name = "", .value = "" };
|
||||
var websocket_accept_header = PicoHTTP.Header{ .name = "", .value = "" };
|
||||
var visited_protocol = this.websocket_protocol == 0;
|
||||
var protocol_header_seen = false;
|
||||
|
||||
// var visited_version = false;
|
||||
var deflate_result = DeflateNegotiationResult{};
|
||||
|
||||
@@ -382,11 +388,36 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type {
|
||||
},
|
||||
"Sec-WebSocket-Protocol".len => {
|
||||
if (strings.eqlCaseInsensitiveASCII(header.name, "Sec-WebSocket-Protocol", false)) {
|
||||
if (this.websocket_protocol == 0 or bun.hash(header.value) != this.websocket_protocol) {
|
||||
const valid = brk: {
|
||||
// Can't have multiple protocol headers in the response.
|
||||
if (protocol_header_seen) break :brk false;
|
||||
|
||||
protocol_header_seen = true;
|
||||
|
||||
var iterator = bun.http.HeaderValueIterator.init(header.value);
|
||||
|
||||
const protocol = iterator.next()
|
||||
// Can't be empty.
|
||||
orelse break :brk false;
|
||||
|
||||
// Can't have multiple protocols.
|
||||
if (iterator.next() != null) break :brk false;
|
||||
|
||||
// Protocol must be in the list of allowed protocols.
|
||||
if (!this.subprotocols.contains(protocol)) break :brk false;
|
||||
|
||||
if (this.outgoing_websocket) |ws| {
|
||||
var protocol_str = bun.String.init(protocol);
|
||||
defer protocol_str.deref();
|
||||
ws.setProtocol(&protocol_str);
|
||||
}
|
||||
break :brk true;
|
||||
};
|
||||
|
||||
if (!valid) {
|
||||
this.terminate(ErrorCode.mismatch_client_protocol);
|
||||
return;
|
||||
}
|
||||
visited_protocol = true;
|
||||
}
|
||||
},
|
||||
"Sec-WebSocket-Extensions".len => {
|
||||
@@ -469,11 +500,6 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!visited_protocol) {
|
||||
this.terminate(ErrorCode.mismatch_client_protocol);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!strings.eqlCaseInsensitiveASCII(connection_header.value, "Upgrade", true)) {
|
||||
this.terminate(ErrorCode.invalid_connection_header);
|
||||
return;
|
||||
@@ -622,7 +648,6 @@ fn buildRequestBody(
|
||||
host: *const jsc.ZigString,
|
||||
port: u16,
|
||||
client_protocol: *const jsc.ZigString,
|
||||
client_protocol_hash: *u64,
|
||||
extra_headers: NonUTF8Headers,
|
||||
) std.mem.Allocator.Error![]u8 {
|
||||
const allocator = vm.allocator;
|
||||
@@ -642,9 +667,6 @@ fn buildRequestBody(
|
||||
},
|
||||
};
|
||||
|
||||
if (client_protocol.len > 0)
|
||||
client_protocol_hash.* = bun.hash(static_headers[1].value);
|
||||
|
||||
const pathname_ = pathname.toSlice(allocator);
|
||||
const host_ = host.toSlice(allocator);
|
||||
defer {
|
||||
|
||||
@@ -550,11 +550,16 @@ function spawnSync(file, args, options) {
|
||||
stderr = null;
|
||||
}
|
||||
|
||||
// When stdio is redirected to a file descriptor, Bun.spawnSync returns the fd number
|
||||
// instead of the actual output. We should treat this as no output available.
|
||||
const outputStdout = typeof stdout === "number" ? null : stdout;
|
||||
const outputStderr = typeof stderr === "number" ? null : stderr;
|
||||
|
||||
const result = {
|
||||
signal: signalCode ?? null,
|
||||
status: exitCode,
|
||||
// TODO: Need to expose extra pipes from Bun.spawnSync to child_process
|
||||
output: [null, stdout, stderr],
|
||||
output: [null, outputStdout, outputStderr],
|
||||
pid,
|
||||
};
|
||||
|
||||
@@ -562,11 +567,11 @@ function spawnSync(file, args, options) {
|
||||
result.error = error;
|
||||
}
|
||||
|
||||
if (stdout && encoding && encoding !== "buffer") {
|
||||
if (outputStdout && encoding && encoding !== "buffer") {
|
||||
result.output[1] = result.output[1]?.toString(encoding);
|
||||
}
|
||||
|
||||
if (stderr && encoding && encoding !== "buffer") {
|
||||
if (outputStderr && encoding && encoding !== "buffer") {
|
||||
result.output[2] = result.output[2]?.toString(encoding);
|
||||
}
|
||||
|
||||
|
||||
@@ -457,7 +457,7 @@ pub const ByteRangeMapping = struct {
|
||||
|
||||
const has_executed = block.hasExecuted or block.executionCount > 0;
|
||||
|
||||
for (min..max) |byte_offset| {
|
||||
for (min..(max + 1)) |byte_offset| {
|
||||
const new_line_index = LineOffsetTable.findIndex(line_starts, .{ .start = @intCast(byte_offset) }) orelse continue;
|
||||
const line_start_byte_offset = line_starts[new_line_index];
|
||||
if (line_start_byte_offset >= byte_offset) {
|
||||
@@ -494,7 +494,7 @@ pub const ByteRangeMapping = struct {
|
||||
var min_line: u32 = std.math.maxInt(u32);
|
||||
var max_line: u32 = 0;
|
||||
|
||||
for (min..max) |byte_offset| {
|
||||
for (min..(max + 1)) |byte_offset| {
|
||||
const new_line_index = LineOffsetTable.findIndex(line_starts, .{ .start = @intCast(byte_offset) }) orelse continue;
|
||||
const line_start_byte_offset = line_starts[new_line_index];
|
||||
if (line_start_byte_offset >= byte_offset) {
|
||||
@@ -546,7 +546,7 @@ pub const ByteRangeMapping = struct {
|
||||
var max_line: u32 = 0;
|
||||
const has_executed = block.hasExecuted or block.executionCount > 0;
|
||||
|
||||
for (min..max) |byte_offset| {
|
||||
for (min..(max + 1)) |byte_offset| {
|
||||
const new_line_index = LineOffsetTable.findIndex(line_starts, .{ .start = @intCast(byte_offset) }) orelse continue;
|
||||
const line_start_byte_offset = line_starts[new_line_index];
|
||||
if (line_start_byte_offset >= byte_offset) {
|
||||
@@ -589,7 +589,7 @@ pub const ByteRangeMapping = struct {
|
||||
var min_line: u32 = std.math.maxInt(u32);
|
||||
var max_line: u32 = 0;
|
||||
|
||||
for (min..max) |byte_offset| {
|
||||
for (min..(max + 1)) |byte_offset| {
|
||||
const new_line_index = LineOffsetTable.findIndex(line_starts, .{ .start = @intCast(byte_offset) }) orelse continue;
|
||||
const line_start_byte_offset = line_starts[new_line_index];
|
||||
if (line_start_byte_offset >= byte_offset) {
|
||||
|
||||
@@ -589,3 +589,99 @@ Ran 1 test across 1 file."
|
||||
`);
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("coverage accuracy - switch statement", () => {
|
||||
const dir = tempDirWithFiles("cov", {
|
||||
"switch-func.ts": `
|
||||
export function func(value: string): string {
|
||||
switch (value) {
|
||||
case "A":
|
||||
return "Alpha";
|
||||
case "B":
|
||||
return "Beta";
|
||||
case "C":
|
||||
return "Charlie";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
`,
|
||||
"switch-test.test.ts": `
|
||||
import { test, expect } from "bun:test";
|
||||
import { func } from "./switch-func";
|
||||
|
||||
test("switch test - case A only", () => {
|
||||
expect(func("A")).toBe("Alpha");
|
||||
});
|
||||
`,
|
||||
});
|
||||
|
||||
const result = Bun.spawnSync([bunExe(), "test", "--coverage"], {
|
||||
cwd: dir,
|
||||
env: {
|
||||
...bunEnv,
|
||||
},
|
||||
stdio: [null, null, "pipe"],
|
||||
});
|
||||
|
||||
let stderr = result.stderr.toString("utf-8");
|
||||
// Normalize output for cross-platform consistency
|
||||
stderr = normalizeBunSnapshot(stderr, dir);
|
||||
|
||||
// Lines 6-9 (case "B", "C", and default) should be uncovered
|
||||
expect(stderr).toContain("55.56");
|
||||
expect(stderr).toContain("6-9");
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("coverage accuracy - try-catch statement", () => {
|
||||
const dir = tempDirWithFiles("cov", {
|
||||
"try-catch-func.ts": `
|
||||
export function func(value: number): string | number {
|
||||
try {
|
||||
if (value > 100) {
|
||||
throw new Error("Too large");
|
||||
}
|
||||
if (value < 0) {
|
||||
throw new Error("Negative");
|
||||
}
|
||||
return value * 2;
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
if (e.message === "Too large") {
|
||||
return "error: large";
|
||||
} else if (e.message === "Negative") {
|
||||
return "error: negative";
|
||||
}
|
||||
}
|
||||
return "unknown error";
|
||||
}
|
||||
}
|
||||
`,
|
||||
"try-catch-test.test.ts": `
|
||||
import { test, expect } from "bun:test";
|
||||
import { func } from "./try-catch-func";
|
||||
|
||||
test("try-catch test - normal value", () => {
|
||||
expect(func(50)).toBe(100);
|
||||
});
|
||||
`,
|
||||
});
|
||||
|
||||
const result = Bun.spawnSync([bunExe(), "test", "--coverage"], {
|
||||
cwd: dir,
|
||||
env: {
|
||||
...bunEnv,
|
||||
},
|
||||
stdio: [null, null, "pipe"],
|
||||
});
|
||||
|
||||
let stderr = result.stderr.toString("utf-8");
|
||||
// Normalize output for cross-platform consistency
|
||||
stderr = normalizeBunSnapshot(stderr, dir);
|
||||
|
||||
// Lines 5,8,11-18 (error conditions and catch block) should be uncovered
|
||||
expect(stderr).toContain("44.44");
|
||||
expect(stderr).toContain("5,8,11-18");
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
@@ -1231,15 +1231,25 @@ describe("websocket and routes test", () => {
|
||||
resolve(event.data);
|
||||
ws.close();
|
||||
};
|
||||
ws.onerror = reject;
|
||||
let errorFired = false;
|
||||
ws.onerror = e => {
|
||||
errorFired = true;
|
||||
// Don't reject on error, we expect both error and close for failed upgrade
|
||||
};
|
||||
ws.onclose = event => {
|
||||
reject(event.code);
|
||||
if (!shouldBeUpgraded) {
|
||||
// For failed upgrade, resolve with the close code
|
||||
resolve(event.code);
|
||||
} else {
|
||||
reject(event.code);
|
||||
}
|
||||
};
|
||||
if (shouldBeUpgraded) {
|
||||
const result = await promise;
|
||||
expect(result).toBe("recv: Hello server");
|
||||
} else {
|
||||
const result = await promise.catch(e => e);
|
||||
const result = await promise;
|
||||
expect(errorFired).toBe(true); // Error event should fire for failed upgrade
|
||||
expect(result).toBe(1002);
|
||||
}
|
||||
if (hasPOST) {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { tmpdirSync } from "harness";
|
||||
import { normalizeBunSnapshot, tmpdirSync } from "harness";
|
||||
import { join } from "path";
|
||||
import util from "util";
|
||||
|
||||
it("prototype", () => {
|
||||
const prototypes = [
|
||||
Request.prototype,
|
||||
@@ -607,3 +606,120 @@ it("Symbol", () => {
|
||||
expect(Bun.inspect(Symbol())).toBe("Symbol()");
|
||||
expect(Bun.inspect(Symbol(""))).toBe("Symbol()");
|
||||
});
|
||||
|
||||
it("CloseEvent", () => {
|
||||
const closeEvent = new CloseEvent("close", {
|
||||
code: 1000,
|
||||
reason: "Normal",
|
||||
});
|
||||
expect(Bun.inspect(closeEvent)).toMatchInlineSnapshot(`
|
||||
"CloseEvent {
|
||||
isTrusted: false,
|
||||
wasClean: false,
|
||||
code: 1000,
|
||||
reason: "Normal",
|
||||
type: "close",
|
||||
target: null,
|
||||
currentTarget: null,
|
||||
eventPhase: 0,
|
||||
cancelBubble: false,
|
||||
bubbles: false,
|
||||
cancelable: false,
|
||||
defaultPrevented: false,
|
||||
composed: false,
|
||||
timeStamp: 0,
|
||||
srcElement: null,
|
||||
returnValue: true,
|
||||
composedPath: [Function: composedPath],
|
||||
stopPropagation: [Function: stopPropagation],
|
||||
stopImmediatePropagation: [Function: stopImmediatePropagation],
|
||||
preventDefault: [Function: preventDefault],
|
||||
initEvent: [Function: initEvent],
|
||||
NONE: 0,
|
||||
CAPTURING_PHASE: 1,
|
||||
AT_TARGET: 2,
|
||||
BUBBLING_PHASE: 3,
|
||||
}"
|
||||
`);
|
||||
});
|
||||
|
||||
it("ErrorEvent", () => {
|
||||
const errorEvent = new ErrorEvent("error", {
|
||||
message: "Something went wrong",
|
||||
filename: "script.js",
|
||||
lineno: 42,
|
||||
colno: 10,
|
||||
error: new Error("Test error"),
|
||||
});
|
||||
expect(normalizeBunSnapshot(Bun.inspect(errorEvent)).replace(/\d+ \| /gim, "NNN |")).toMatchInlineSnapshot(`
|
||||
"ErrorEvent {
|
||||
type: "error",
|
||||
message: "Something went wrong",
|
||||
error: NNN | const errorEvent = new ErrorEvent("error", {
|
||||
NNN | message: "Something went wrong",
|
||||
NNN | filename: "script.js",
|
||||
NNN | lineno: 42,
|
||||
NNN | colno: 10,
|
||||
NNN | error: new Error("Test error"),
|
||||
^
|
||||
error: Test error
|
||||
at <anonymous> (file:NN:NN)
|
||||
,
|
||||
}"
|
||||
`);
|
||||
});
|
||||
|
||||
it("MessageEvent", () => {
|
||||
const messageEvent = new MessageEvent("message", {
|
||||
data: "Hello, world!",
|
||||
origin: "https://example.com",
|
||||
lastEventId: "123",
|
||||
source: null,
|
||||
ports: [],
|
||||
});
|
||||
expect(Bun.inspect(messageEvent)).toMatchInlineSnapshot(`
|
||||
"MessageEvent {
|
||||
type: "message",
|
||||
data: "Hello, world!",
|
||||
}"
|
||||
`);
|
||||
});
|
||||
|
||||
it("CustomEvent", () => {
|
||||
const customEvent = new CustomEvent("custom", {
|
||||
detail: { value: 42, name: "test" },
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
expect(Bun.inspect(customEvent)).toMatchInlineSnapshot(`
|
||||
"CustomEvent {
|
||||
isTrusted: false,
|
||||
detail: {
|
||||
value: 42,
|
||||
name: "test",
|
||||
},
|
||||
initCustomEvent: [Function: initCustomEvent],
|
||||
type: "custom",
|
||||
target: null,
|
||||
currentTarget: null,
|
||||
eventPhase: 0,
|
||||
cancelBubble: false,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
defaultPrevented: false,
|
||||
composed: false,
|
||||
timeStamp: 0,
|
||||
srcElement: null,
|
||||
returnValue: true,
|
||||
composedPath: [Function: composedPath],
|
||||
stopPropagation: [Function: stopPropagation],
|
||||
stopImmediatePropagation: [Function: stopImmediatePropagation],
|
||||
preventDefault: [Function: preventDefault],
|
||||
initEvent: [Function: initEvent],
|
||||
NONE: 0,
|
||||
CAPTURING_PHASE: 1,
|
||||
AT_TARGET: 2,
|
||||
BUBBLING_PHASE: 3,
|
||||
}"
|
||||
`);
|
||||
});
|
||||
|
||||
191
test/js/web/websocket/websocket-subprotocol-strict.test.ts
Normal file
191
test/js/web/websocket/websocket-subprotocol-strict.test.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import { describe, expect, it, mock } from "bun:test";
|
||||
import crypto from "node:crypto";
|
||||
import net from "node:net";
|
||||
|
||||
describe("WebSocket strict RFC 6455 subprotocol handling", () => {
|
||||
async function createTestServer(
|
||||
responseHeaders: string[],
|
||||
): Promise<{ port: number; [Symbol.asyncDispose]: () => Promise<void> }> {
|
||||
const server = net.createServer();
|
||||
let port: number;
|
||||
|
||||
await new Promise<void>(resolve => {
|
||||
server.listen(0, () => {
|
||||
port = (server.address() as any).port;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
server.on("connection", socket => {
|
||||
let requestData = "";
|
||||
|
||||
socket.on("data", data => {
|
||||
requestData += data.toString();
|
||||
|
||||
if (requestData.includes("\r\n\r\n")) {
|
||||
const lines = requestData.split("\r\n");
|
||||
let websocketKey = "";
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("Sec-WebSocket-Key:")) {
|
||||
websocketKey = line.split(":")[1].trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const acceptKey = crypto
|
||||
.createHash("sha1")
|
||||
.update(websocketKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
|
||||
.digest("base64");
|
||||
|
||||
const response = [
|
||||
"HTTP/1.1 101 Switching Protocols",
|
||||
"Upgrade: websocket",
|
||||
"Connection: Upgrade",
|
||||
`Sec-WebSocket-Accept: ${acceptKey}`,
|
||||
...responseHeaders,
|
||||
"\r\n",
|
||||
].join("\r\n");
|
||||
|
||||
socket.write(response);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
port: port!,
|
||||
[Symbol.asyncDispose]: async () => {
|
||||
server.close();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function expectConnectionFailure(port: number, protocols: string[], expectedCode = 1002) {
|
||||
const { promise: closePromise, resolve: resolveClose } = Promise.withResolvers();
|
||||
|
||||
const ws = new WebSocket(`ws://localhost:${port}`, protocols);
|
||||
const onopenMock = mock(() => {});
|
||||
ws.onopen = onopenMock;
|
||||
|
||||
ws.onclose = close => {
|
||||
expect(close.code).toBe(expectedCode);
|
||||
expect(close.reason).toBe("Mismatch client protocol");
|
||||
resolveClose();
|
||||
};
|
||||
|
||||
await closePromise;
|
||||
expect(onopenMock).not.toHaveBeenCalled();
|
||||
}
|
||||
|
||||
async function expectConnectionSuccess(port: number, protocols: string[], expectedProtocol: string) {
|
||||
const { promise: openPromise, resolve: resolveOpen, reject } = Promise.withResolvers();
|
||||
const ws = new WebSocket(`ws://localhost:${port}`, protocols);
|
||||
try {
|
||||
ws.onopen = () => resolveOpen();
|
||||
ws.onerror = reject;
|
||||
await openPromise;
|
||||
expect(ws.protocol).toBe(expectedProtocol);
|
||||
} finally {
|
||||
ws.terminate();
|
||||
}
|
||||
}
|
||||
// Multiple protocols in single header (comma-separated) - should fail
|
||||
it("should reject multiple comma-separated protocols", async () => {
|
||||
await using server = await createTestServer(["Sec-WebSocket-Protocol: chat, echo"]);
|
||||
await expectConnectionFailure(server.port, ["chat", "echo"]);
|
||||
});
|
||||
|
||||
it("should reject multiple comma-separated protocols with spaces", async () => {
|
||||
await using server = await createTestServer(["Sec-WebSocket-Protocol: chat , echo , binary"]);
|
||||
await expectConnectionFailure(server.port, ["chat", "echo", "binary"]);
|
||||
});
|
||||
|
||||
it("should reject multiple comma-separated protocols (3 protocols)", async () => {
|
||||
await using server = await createTestServer(["Sec-WebSocket-Protocol: a,b,c"]);
|
||||
await expectConnectionFailure(server.port, ["a", "b", "c"]);
|
||||
});
|
||||
|
||||
// Multiple headers - should fail
|
||||
it("should reject duplicate Sec-WebSocket-Protocol headers (same value)", async () => {
|
||||
await using server = await createTestServer(["Sec-WebSocket-Protocol: chat", "Sec-WebSocket-Protocol: chat"]);
|
||||
await expectConnectionFailure(server.port, ["chat", "echo"]);
|
||||
});
|
||||
|
||||
it("should reject duplicate Sec-WebSocket-Protocol headers (different values)", async () => {
|
||||
await using server = await createTestServer(["Sec-WebSocket-Protocol: chat", "Sec-WebSocket-Protocol: echo"]);
|
||||
await expectConnectionFailure(server.port, ["chat", "echo"]);
|
||||
});
|
||||
|
||||
it("should reject three Sec-WebSocket-Protocol headers", async () => {
|
||||
await using server = await createTestServer([
|
||||
"Sec-WebSocket-Protocol: a",
|
||||
"Sec-WebSocket-Protocol: b",
|
||||
"Sec-WebSocket-Protocol: c",
|
||||
]);
|
||||
await expectConnectionFailure(server.port, ["a", "b", "c"]);
|
||||
});
|
||||
|
||||
// Empty values - should fail
|
||||
it("should reject empty Sec-WebSocket-Protocol header", async () => {
|
||||
await using server = await createTestServer(["Sec-WebSocket-Protocol: "]);
|
||||
await expectConnectionFailure(server.port, ["chat", "echo"]);
|
||||
});
|
||||
|
||||
it("should reject Sec-WebSocket-Protocol with only comma", async () => {
|
||||
await using server = await createTestServer(["Sec-WebSocket-Protocol: ,"]);
|
||||
await expectConnectionFailure(server.port, ["chat", "echo"]);
|
||||
});
|
||||
|
||||
it("should reject Sec-WebSocket-Protocol with only spaces", async () => {
|
||||
await using server = await createTestServer(["Sec-WebSocket-Protocol: "]);
|
||||
await expectConnectionFailure(server.port, ["chat", "echo"]);
|
||||
});
|
||||
|
||||
// Unknown protocols - should fail
|
||||
it("should reject unknown single protocol", async () => {
|
||||
await using server = await createTestServer(["Sec-WebSocket-Protocol: unknown"]);
|
||||
await expectConnectionFailure(server.port, ["chat", "echo"]);
|
||||
});
|
||||
|
||||
it("should reject unknown protocol (not in client list)", async () => {
|
||||
await using server = await createTestServer(["Sec-WebSocket-Protocol: binary"]);
|
||||
await expectConnectionFailure(server.port, ["chat", "echo"]);
|
||||
});
|
||||
|
||||
// Valid cases - should succeed
|
||||
it("should accept single valid protocol (first in client list)", async () => {
|
||||
await using server = await createTestServer(["Sec-WebSocket-Protocol: chat"]);
|
||||
await expectConnectionSuccess(server.port, ["chat", "echo", "binary"], "chat");
|
||||
});
|
||||
|
||||
it("should accept single valid protocol (middle in client list)", async () => {
|
||||
await using server = await createTestServer(["Sec-WebSocket-Protocol: echo"]);
|
||||
await expectConnectionSuccess(server.port, ["chat", "echo", "binary"], "echo");
|
||||
});
|
||||
|
||||
it("should accept single valid protocol (last in client list)", async () => {
|
||||
await using server = await createTestServer(["Sec-WebSocket-Protocol: binary"]);
|
||||
await expectConnectionSuccess(server.port, ["chat", "echo", "binary"], "binary");
|
||||
});
|
||||
|
||||
it("should accept single protocol with extra whitespace", async () => {
|
||||
await using server = await createTestServer(["Sec-WebSocket-Protocol: echo "]);
|
||||
await expectConnectionSuccess(server.port, ["chat", "echo"], "echo");
|
||||
});
|
||||
|
||||
it("should accept single protocol with single character", async () => {
|
||||
await using server = await createTestServer(["Sec-WebSocket-Protocol: a"]);
|
||||
await expectConnectionSuccess(server.port, ["a", "b"], "a");
|
||||
});
|
||||
|
||||
// Edge cases with special characters
|
||||
it("should handle protocol with special characters", async () => {
|
||||
await using server = await createTestServer(["Sec-WebSocket-Protocol: chat-v2.0"]);
|
||||
await expectConnectionSuccess(server.port, ["chat-v1.0", "chat-v2.0"], "chat-v2.0");
|
||||
});
|
||||
|
||||
it("should handle protocol with dots", async () => {
|
||||
await using server = await createTestServer(["Sec-WebSocket-Protocol: com.example.chat"]);
|
||||
await expectConnectionSuccess(server.port, ["com.example.chat", "other"], "com.example.chat");
|
||||
});
|
||||
});
|
||||
@@ -131,15 +131,19 @@ describe("WebSocket", () => {
|
||||
function testClient(client) {
|
||||
const { promise, resolve, reject } = Promise.withResolvers();
|
||||
let messages = [];
|
||||
let errorFired = false;
|
||||
client.onopen = () => {
|
||||
client.send("Hello from client!");
|
||||
};
|
||||
client.onmessage = e => {
|
||||
messages.push(e.data);
|
||||
};
|
||||
client.onerror = reject;
|
||||
client.onerror = e => {
|
||||
errorFired = true;
|
||||
// Don't reject, we expect both error and close events
|
||||
};
|
||||
client.onclose = e => {
|
||||
resolve({ result: e, messages });
|
||||
resolve({ result: e, messages, errorFired });
|
||||
};
|
||||
return promise;
|
||||
}
|
||||
@@ -147,7 +151,8 @@ describe("WebSocket", () => {
|
||||
{
|
||||
// by default rejectUnauthorized is true
|
||||
const client = new WebSocket(url);
|
||||
const { result, messages } = await testClient(client);
|
||||
const { result, messages, errorFired } = await testClient(client);
|
||||
expect(errorFired).toBe(true); // Error event should fire
|
||||
expect(["Hello from Bun!", "Hello from client!"]).not.toEqual(messages);
|
||||
expect(result.code).toBe(1015);
|
||||
expect(result.reason).toBe("TLS handshake failed");
|
||||
@@ -156,7 +161,8 @@ describe("WebSocket", () => {
|
||||
{
|
||||
// just in case we change the default to true and test
|
||||
const client = new WebSocket(url, { tls: { rejectUnauthorized: true } });
|
||||
const { result, messages } = await testClient(client);
|
||||
const { result, messages, errorFired } = await testClient(client);
|
||||
expect(errorFired).toBe(true); // Error event should fire
|
||||
expect(["Hello from Bun!", "Hello from client!"]).not.toEqual(messages);
|
||||
expect(result.code).toBe(1015);
|
||||
expect(result.reason).toBe("TLS handshake failed");
|
||||
@@ -248,22 +254,27 @@ describe("WebSocket", () => {
|
||||
function testClient(client) {
|
||||
const { promise, resolve, reject } = Promise.withResolvers();
|
||||
let messages = [];
|
||||
let errorFired = false;
|
||||
client.onopen = () => {
|
||||
client.send("Hello from client!");
|
||||
};
|
||||
client.onmessage = e => {
|
||||
messages.push(e.data);
|
||||
};
|
||||
client.onerror = reject;
|
||||
client.onerror = e => {
|
||||
errorFired = true;
|
||||
// Don't reject, we expect both error and close events
|
||||
};
|
||||
client.onclose = e => {
|
||||
resolve({ result: e, messages });
|
||||
resolve({ result: e, messages, errorFired });
|
||||
};
|
||||
return promise;
|
||||
}
|
||||
const url = `wss://localhost:${server.address.port}`;
|
||||
{
|
||||
const client = new WebSocket(url);
|
||||
const { result, messages } = await testClient(client);
|
||||
const { result, messages, errorFired } = await testClient(client);
|
||||
expect(errorFired).toBe(true); // Error event should fire
|
||||
expect(["Hello from Bun!", "Hello from client!"]).not.toEqual(messages);
|
||||
expect(result.code).toBe(1015);
|
||||
expect(result.reason).toBe("TLS handshake failed");
|
||||
|
||||
150
test/regression/issue/14338.test.ts
Normal file
150
test/regression/issue/14338.test.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { expect, test } from "bun:test";
|
||||
|
||||
test("WebSocket should emit error event before close event on handshake failure (issue #14338)", async () => {
|
||||
const { promise: errorPromise, resolve: resolveError } = Promise.withResolvers<Event>();
|
||||
const { promise: closePromise, resolve: resolveClose } = Promise.withResolvers<CloseEvent>();
|
||||
const events: string[] = [];
|
||||
|
||||
// Create a server that returns a 302 redirect response instead of a WebSocket upgrade
|
||||
await using server = Bun.serve({
|
||||
port: 0,
|
||||
fetch(req) {
|
||||
// Return a 302 redirect response to simulate handshake failure
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
Location: "http://example.com",
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const ws = new WebSocket(`ws://localhost:${server.port}`);
|
||||
|
||||
ws.addEventListener("error", event => {
|
||||
events.push("error");
|
||||
resolveError(event);
|
||||
});
|
||||
|
||||
ws.addEventListener("close", event => {
|
||||
events.push("close");
|
||||
resolveClose(event);
|
||||
});
|
||||
|
||||
ws.addEventListener("open", () => {
|
||||
events.push("open");
|
||||
});
|
||||
|
||||
// Wait for close event (which should always fire)
|
||||
await closePromise;
|
||||
|
||||
// After the fix, both error and close events should be emitted
|
||||
// The error event should come before the close event
|
||||
expect(events).toEqual(["error", "close"]);
|
||||
});
|
||||
|
||||
test("WebSocket successful connection should NOT emit error event", async () => {
|
||||
const { promise: openPromise, resolve: resolveOpen } = Promise.withResolvers<Event>();
|
||||
const { promise: messagePromise, resolve: resolveMessage } = Promise.withResolvers<MessageEvent>();
|
||||
const { promise: closePromise, resolve: resolveClose } = Promise.withResolvers<CloseEvent>();
|
||||
const events: string[] = [];
|
||||
|
||||
// Create a proper WebSocket server
|
||||
await using server = Bun.serve({
|
||||
port: 0,
|
||||
websocket: {
|
||||
message(ws, message) {
|
||||
ws.send(message);
|
||||
},
|
||||
},
|
||||
fetch(req, server) {
|
||||
if (server.upgrade(req)) {
|
||||
return;
|
||||
}
|
||||
return new Response("Not found", { status: 404 });
|
||||
},
|
||||
});
|
||||
|
||||
const ws = new WebSocket(`ws://localhost:${server.port}`);
|
||||
|
||||
ws.addEventListener("error", event => {
|
||||
events.push("error");
|
||||
});
|
||||
|
||||
ws.addEventListener("open", event => {
|
||||
events.push("open");
|
||||
resolveOpen(event);
|
||||
});
|
||||
|
||||
ws.addEventListener("message", event => {
|
||||
events.push("message");
|
||||
resolveMessage(event);
|
||||
});
|
||||
|
||||
ws.addEventListener("close", event => {
|
||||
events.push("close");
|
||||
resolveClose(event);
|
||||
});
|
||||
|
||||
// Wait for connection to open
|
||||
await openPromise;
|
||||
|
||||
// Send a test message
|
||||
ws.send("test");
|
||||
|
||||
// Wait for echo
|
||||
const msg = await messagePromise;
|
||||
expect(msg.data).toBe("test");
|
||||
|
||||
// Close the connection normally
|
||||
ws.close();
|
||||
|
||||
// Wait for close event
|
||||
await closePromise;
|
||||
|
||||
// Should have open, message, and close events, but NO error event
|
||||
expect(events).toContain("open");
|
||||
expect(events).toContain("message");
|
||||
expect(events).toContain("close");
|
||||
expect(events).not.toContain("error");
|
||||
});
|
||||
|
||||
test("WebSocket should emit error and close events on connection to non-WebSocket server", async () => {
|
||||
const { promise: closePromise, resolve: resolveClose } = Promise.withResolvers<CloseEvent>();
|
||||
const events: string[] = [];
|
||||
|
||||
// Create a regular HTTP server (not WebSocket)
|
||||
await using server = Bun.serve({
|
||||
port: 0,
|
||||
fetch(req) {
|
||||
// Return a normal HTTP response
|
||||
return new Response("Not a WebSocket server", {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "text/plain",
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const ws = new WebSocket(`ws://localhost:${server.port}`);
|
||||
|
||||
ws.addEventListener("error", event => {
|
||||
events.push("error");
|
||||
});
|
||||
|
||||
ws.addEventListener("close", event => {
|
||||
events.push("close");
|
||||
resolveClose(event);
|
||||
});
|
||||
|
||||
ws.addEventListener("open", () => {
|
||||
events.push("open");
|
||||
});
|
||||
|
||||
// Wait for close event
|
||||
await closePromise;
|
||||
|
||||
// After the fix, both error and close events should be emitted
|
||||
expect(events).toEqual(["error", "close"]);
|
||||
});
|
||||
58
test/regression/issue/19219.test.ts
Normal file
58
test/regression/issue/19219.test.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { expect, test } from "bun:test";
|
||||
|
||||
// https://github.com/oven-sh/bun/issues/19219
|
||||
test("HTMLRewriter should throw proper errors instead of [native code: Exception]", () => {
|
||||
const rewriter = new HTMLRewriter().on("p", {
|
||||
element(element) {
|
||||
// This will cause an error by trying to call a non-existent method
|
||||
(element as any).nonExistentMethod();
|
||||
},
|
||||
});
|
||||
|
||||
const html = "<html><body><p>Hello</p></body></html>";
|
||||
|
||||
// Should throw a proper TypeError, not [native code: Exception]
|
||||
expect(() => {
|
||||
rewriter.transform(html);
|
||||
}).toThrow(TypeError);
|
||||
|
||||
// Verify the error message is descriptive
|
||||
try {
|
||||
rewriter.transform(html);
|
||||
} catch (error: any) {
|
||||
expect(error).toBeInstanceOf(TypeError);
|
||||
expect(error.message).toContain("nonExistentMethod");
|
||||
expect(error.message).toContain("is not a function");
|
||||
// Make sure it's not the generic [native code: Exception] message
|
||||
expect(error.toString()).not.toContain("[native code: Exception]");
|
||||
}
|
||||
});
|
||||
|
||||
test("HTMLRewriter should propagate errors from handlers correctly", () => {
|
||||
const rewriter = new HTMLRewriter().on("div", {
|
||||
element() {
|
||||
throw new Error("Custom error from handler");
|
||||
},
|
||||
});
|
||||
|
||||
const html = "<div>test</div>";
|
||||
|
||||
expect(() => {
|
||||
rewriter.transform(html);
|
||||
}).toThrow("Custom error from handler");
|
||||
});
|
||||
|
||||
test("HTMLRewriter should handle errors in async handlers", async () => {
|
||||
const rewriter = new HTMLRewriter().on("div", {
|
||||
async element() {
|
||||
throw new Error("Async handler error");
|
||||
},
|
||||
});
|
||||
|
||||
const html = "<div>test</div>";
|
||||
const response = new Response(html);
|
||||
|
||||
expect(() => {
|
||||
rewriter.transform(response);
|
||||
}).toThrow("Async handler error");
|
||||
});
|
||||
95
test/regression/issue/20321.test.ts
Normal file
95
test/regression/issue/20321.test.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { spawnSync } from "child_process";
|
||||
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
|
||||
|
||||
test("spawnSync should not crash when stdout is set to process.stderr (issue #20321)", () => {
|
||||
// Test with process.stderr as stdout
|
||||
const proc1 = spawnSync(bunExe(), ["-e", 'console.log("hello")'], {
|
||||
encoding: "utf-8",
|
||||
stdio: ["ignore", process.stderr, "inherit"],
|
||||
env: bunEnv,
|
||||
});
|
||||
|
||||
expect(proc1.error).toBeUndefined();
|
||||
expect(proc1.status).toBe(0);
|
||||
// When redirecting to a file descriptor, we don't capture the output
|
||||
expect(proc1.stdout).toBeNull();
|
||||
});
|
||||
|
||||
test("spawnSync should not crash when stderr is set to process.stdout", () => {
|
||||
// Test with process.stdout as stderr
|
||||
const proc2 = spawnSync(bunExe(), ["-e", 'console.log("hello")'], {
|
||||
encoding: "utf-8",
|
||||
stdio: ["ignore", "pipe", process.stdout],
|
||||
env: bunEnv,
|
||||
});
|
||||
|
||||
expect(proc2.error).toBeUndefined();
|
||||
expect(proc2.status).toBe(0);
|
||||
expect(proc2.stdout).toBe("hello\n");
|
||||
// When redirecting to a file descriptor, we don't capture the output
|
||||
expect(proc2.stderr).toBeNull();
|
||||
});
|
||||
|
||||
test("spawnSync should handle process.stdin/stdout/stderr in stdio array", () => {
|
||||
// Test with all process streams
|
||||
const proc3 = spawnSync(bunExe(), ["-e", 'console.log("test")'], {
|
||||
encoding: "utf-8",
|
||||
stdio: [process.stdin, process.stdout, process.stderr],
|
||||
env: bunEnv,
|
||||
});
|
||||
|
||||
expect(proc3.error).toBeUndefined();
|
||||
expect(proc3.status).toBe(0);
|
||||
// When redirecting to file descriptors, we don't capture the output
|
||||
expect(proc3.stdout).toBeNull();
|
||||
expect(proc3.stderr).toBeNull();
|
||||
});
|
||||
|
||||
test("spawnSync with mixed stdio options including process streams", () => {
|
||||
// Mix of different stdio options
|
||||
const proc4 = spawnSync(bunExe(), ["-e", 'console.log("mixed")'], {
|
||||
encoding: "utf-8",
|
||||
stdio: ["pipe", process.stderr, "pipe"],
|
||||
env: bunEnv,
|
||||
});
|
||||
|
||||
expect(proc4.error).toBeUndefined();
|
||||
expect(proc4.status).toBe(0);
|
||||
// stdout redirected to stderr fd, so no capture
|
||||
expect(proc4.stdout).toBeNull();
|
||||
// stderr is piped, should be empty for echo
|
||||
expect(proc4.stderr).toBe("");
|
||||
});
|
||||
|
||||
test("spawnSync should work with file descriptors directly", () => {
|
||||
// Test with raw file descriptors (same as what process.stderr resolves to)
|
||||
const proc5 = spawnSync(bunExe(), ["-e", 'console.log("fd-test")'], {
|
||||
encoding: "utf-8",
|
||||
stdio: ["ignore", 2, "inherit"], // 2 is stderr fd
|
||||
env: bunEnv,
|
||||
});
|
||||
|
||||
expect(proc5.error).toBeUndefined();
|
||||
expect(proc5.status).toBe(0);
|
||||
expect(proc5.stdout).toBeNull();
|
||||
});
|
||||
|
||||
test("spawnSync should handle the AWS CDK use case", () => {
|
||||
// This is the exact use case from AWS CDK that was failing
|
||||
const dir = tempDirWithFiles("spawnsync-cdk", {
|
||||
"test.js": `console.log("CDK output");`,
|
||||
});
|
||||
|
||||
const proc = spawnSync(bunExe(), ["test.js"], {
|
||||
encoding: "utf-8",
|
||||
stdio: ["ignore", process.stderr, "inherit"],
|
||||
cwd: dir,
|
||||
env: bunEnv,
|
||||
});
|
||||
|
||||
expect(proc.error).toBeUndefined();
|
||||
expect(proc.status).toBe(0);
|
||||
// Output goes to stderr, not captured
|
||||
expect(proc.stdout).toBeNull();
|
||||
});
|
||||
29
test/regression/issue/issue-1825-jest-mock-functions.test.ts
Normal file
29
test/regression/issue/issue-1825-jest-mock-functions.test.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, jest, test } from "bun:test";
|
||||
|
||||
describe("Jest mock functions from issue #1825", () => {
|
||||
test("jest.mock should be available and work with factory function", () => {
|
||||
// Should not throw - jest.mock should be available
|
||||
expect(() => {
|
||||
jest.mock("fs", () => ({ readFile: jest.fn() }));
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
test("jest.resetAllMocks should be available and not throw", () => {
|
||||
const mockFn = jest.fn();
|
||||
mockFn();
|
||||
expect(mockFn).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Should not throw - jest.resetAllMocks should be available
|
||||
expect(() => {
|
||||
jest.resetAllMocks();
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
test("mockReturnThis should return the mock function itself", () => {
|
||||
const mockFn = jest.fn();
|
||||
const result = mockFn.mockReturnThis();
|
||||
|
||||
// mockReturnThis should return the mock function itself
|
||||
expect(result).toBe(mockFn);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user