You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Deno+Sinon测试Supabase Edge函数:Stub Storage.from未调用问题

问题排查与解决:Deno + Sinon测试Supabase Edge函数Stub未触发

核心问题根源

你的测试代码存在两个关键问题:

  1. 实例不共享:测试中创建的supabaseAdmin实例,和Edge函数里从../_shared/supabaseAdmin.ts导入的实例完全独立。你Stub的是测试本地实例,但通过supabaseAdmin.functions.invoke调用的是部署后的Edge函数,它使用自身的supabaseAdmin实例,自然不会触发你的Stub。
  2. Stub层级错误:storage.from()返回的是Bucket对象,真正调用createSignedUrl的是这个Bucket,而非from方法本身。直接Stubfrom的reject不符合函数调用逻辑。

修复步骤

1. 重构Edge函数为可测试的Handler

把Edge函数拆分为业务逻辑函数和入口函数,让业务逻辑支持依赖注入,方便测试时传入Stub实例:

import 'https://deno.land/x/xhr@0.3.0/mod.ts';
import { corsHeaders } from '../_shared/cors.ts';

// 导出可测试的handler函数,接受请求对象和supabase客户端作为参数
export async function handler(req, supabaseAdmin) {
    const { campaign_id, call_id, speaker, name } = await req.json()

    const { data, error } = await supabaseAdmin
        .storage
        .from('campaigns')
        .createSignedUrl(`${campaign_id}/calls/${call_id}/audios/${name}`, 60);
    
    if (error) {
        return new Response(JSON.stringify({ error: error.message }), {
            status: error.status,
            headers: { ...corsHeaders, 'Content-Type': 'application/json' }
        })
    } else if (data) {
        return new Response(JSON.stringify({ data }), {
            status: 200,
            headers: { ...corsHeaders, 'Content-Type': 'application/json' }
        })
    }
}

// 生产环境入口,使用原有的supabaseAdmin实例
import { supabaseAdmin } from '../_shared/supabaseAdmin.ts';
Deno.serve(async (req) => handler(req, supabaseAdmin));

2. 修改测试代码,直接测试Handler逻辑

不再通过functions.invoke调用远程函数,而是直接调用handler,传入Mock请求和Stub过的supabase实例:

import 'https://deno.land/x/xhr@0.3.0/mod.ts';
import sinon from "npm:sinon";
import { assert, assertEquals } from "https://deno.land/std@0.108.0/testing/asserts.ts";
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.23.0'
import { StorageApiError } from 'https://esm.sh/@supabase/storage-js@2.5.4';
import { handler } from './path-to-your-edge-function.ts'; // 替换为Edge函数的实际路径

const testStorageObjectNotFound = async () => {
    // 创建测试用的supabase实例
    const testSupabaseAdmin = createClient(
        Deno.env.get('SUPABASE_URL') ?? '',
        Deno.env.get('SUPABASE_ANON_KEY') ?? '',
        {
            auth: {
                autoRefreshToken: false,
                persistSession: false,
                detectSessionInUrl: false,
            },
        }
    );

    // 构建Mock Bucket对象,Stub createSignedUrl方法使其抛出错误
    const mockBucket = {
        createSignedUrl: sinon.stub().rejects(new StorageApiError('Object not found', 404))
    };
    // Stub storage.from方法,让它返回Mock Bucket
    const stubFrom = sinon.stub(testSupabaseAdmin.storage, "from").returns(mockBucket);

    // 模拟请求对象
    const mockReq = {
        json: async () => ({
            campaign_id: "123",
            call_id: "456",
            speaker: "John Doe",
            name: "audio-file.mp3",
        })
    };

    // 直接调用handler
    const response = await handler(mockReq, testSupabaseAdmin);

    // 断言响应结果
    const errorResponse = await response.json();
    assertEquals(response.status, 404);
    assertEquals(errorResponse.error, 'Object not found');
    
    // 断言Stub被正确调用
    assert(stubFrom.withArgs('campaigns').calledOnce);
    assert(mockBucket.createSignedUrl.calledOnceWith('123/calls/456/audios/audio-file.mp3', 60));
}

Deno.test("should return 404 when storage object not found", testStorageObjectNotFound);

扩展测试场景:createSignedUrl抛出StorageApiError

通过修改StorageApiError的参数,可测试不同错误情况:

const testStoragePermissionError = async () => {
    const testSupabaseAdmin = createClient(/* 配置同前 */);

    const mockBucket = {
        createSignedUrl: sinon.stub().rejects(new StorageApiError('Permission denied', 403))
    };
    const stubFrom = sinon.stub(testSupabaseAdmin.storage, "from").returns(mockBucket);

    const mockReq = {
        json: async () => ({ /* 请求体同前 */ })
    };

    const response = await handler(mockReq, testSupabaseAdmin);

    const errorResponse = await response.json();
    assertEquals(response.status, 403);
    assertEquals(errorResponse.error, 'Permission denied');
    
    assert(stubFrom.calledOnce);
    assert(mockBucket.createSignedUrl.calledOnce);
}

Deno.test("should return 403 when storage permission denied", testStoragePermissionError);

内容的提问来源于stack exchange,提问作者Bloodbee

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 17:25:55