| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522 |
- import { describe, expect, test, vi } from "vitest";
- function createThenableQuery<T>(result: T) {
- type Query = Promise<T> & {
- from: (...args: unknown[]) => Query;
- where: (...args: unknown[]) => Query;
- orderBy: (...args: unknown[]) => Query;
- limit: (...args: unknown[]) => Query;
- };
- const query = Promise.resolve(result) as unknown as Query;
- query.from = () => query;
- query.where = () => query;
- query.orderBy = () => query;
- query.limit = () => query;
- return query;
- }
- describe("provider-endpoints repository", () => {
- test("ensureProviderEndpointExistsForUrl: url 为空时抛错且不写 DB", async () => {
- vi.resetModules();
- const insertMock = vi.fn();
- vi.doMock("@/drizzle/db", () => ({
- db: {
- insert: insertMock,
- },
- }));
- const { ensureProviderEndpointExistsForUrl } = await import("@/repository/provider-endpoints");
- await expect(
- ensureProviderEndpointExistsForUrl({
- vendorId: 1,
- providerType: "claude",
- url: " ",
- })
- ).rejects.toThrow("[ProviderEndpointEnsure] url is required");
- expect(insertMock).not.toHaveBeenCalled();
- });
- test("ensureProviderEndpointExistsForUrl: url 非法时抛错且不写 DB", async () => {
- vi.resetModules();
- const insertMock = vi.fn();
- vi.doMock("@/drizzle/db", () => ({
- db: {
- insert: insertMock,
- },
- }));
- const { ensureProviderEndpointExistsForUrl } = await import("@/repository/provider-endpoints");
- await expect(
- ensureProviderEndpointExistsForUrl({
- vendorId: 1,
- providerType: "claude",
- url: "not a url",
- })
- ).rejects.toThrow("[ProviderEndpointEnsure] url must be a valid URL");
- expect(insertMock).not.toHaveBeenCalled();
- });
- test("ensureProviderEndpointExistsForUrl: 插入成功时返回 true(trim + label=null)", async () => {
- vi.resetModules();
- const state = { values: undefined as unknown };
- const returning = vi.fn(async () => [{ id: 1 }]);
- const onConflictDoNothing = vi.fn(() => ({ returning }));
- const values = vi.fn((payload: unknown) => {
- state.values = payload;
- return { onConflictDoNothing };
- });
- const insertMock = vi.fn(() => ({ values }));
- vi.doMock("@/drizzle/db", () => ({
- db: {
- insert: insertMock,
- },
- }));
- const { ensureProviderEndpointExistsForUrl } = await import("@/repository/provider-endpoints");
- const ok = await ensureProviderEndpointExistsForUrl({
- vendorId: 1,
- providerType: "claude",
- url: " https://api.example.com ",
- });
- expect(ok).toBe(true);
- expect(insertMock).toHaveBeenCalledTimes(1);
- expect(values).toHaveBeenCalledTimes(1);
- expect(state.values).toEqual(
- expect.objectContaining({
- vendorId: 1,
- providerType: "claude",
- url: "https://api.example.com",
- label: null,
- })
- );
- expect(onConflictDoNothing).toHaveBeenCalledWith(
- expect.objectContaining({
- target: expect.any(Array),
- where: expect.any(Object),
- })
- );
- });
- test("ensureProviderEndpointExistsForUrl: 冲突不插入时返回 false", async () => {
- vi.resetModules();
- const returning = vi.fn(async () => []);
- const onConflictDoNothing = vi.fn(() => ({ returning }));
- const values = vi.fn(() => ({ onConflictDoNothing }));
- const insertMock = vi.fn(() => ({ values }));
- vi.doMock("@/drizzle/db", () => ({
- db: {
- insert: insertMock,
- },
- }));
- const { ensureProviderEndpointExistsForUrl } = await import("@/repository/provider-endpoints");
- const ok = await ensureProviderEndpointExistsForUrl({
- vendorId: 1,
- providerType: "claude",
- url: "https://api.example.com",
- });
- expect(ok).toBe(false);
- });
- test("ensureProviderEndpointExistsForUrl: 非编辑路径保持 insert-only 语义(不触发 update/transaction)", async () => {
- vi.resetModules();
- const returning = vi.fn(async () => []);
- const onConflictDoNothing = vi.fn(() => ({ returning }));
- const values = vi.fn(() => ({ onConflictDoNothing }));
- const insertMock = vi.fn(() => ({ values }));
- const updateMock = vi.fn();
- const transactionMock = vi.fn();
- vi.doMock("@/drizzle/db", () => ({
- db: {
- insert: insertMock,
- update: updateMock,
- transaction: transactionMock,
- },
- }));
- const { ensureProviderEndpointExistsForUrl } = await import("@/repository/provider-endpoints");
- const ok = await ensureProviderEndpointExistsForUrl({
- vendorId: 1,
- providerType: "codex",
- url: "https://api.example.com/v1/responses",
- });
- expect(ok).toBe(false);
- expect(insertMock).toHaveBeenCalledTimes(1);
- expect(updateMock).not.toHaveBeenCalled();
- expect(transactionMock).not.toHaveBeenCalled();
- });
- test("backfillProviderEndpointsFromProviders: 全部无效时不写 DB", async () => {
- vi.resetModules();
- const selectPages = [
- [
- { id: 1, vendorId: 0, providerType: "claude", url: "https://ok.example.com" },
- { id: 2, vendorId: 1, providerType: "claude", url: " " },
- { id: 3, vendorId: 1, providerType: "claude", url: "not a url" },
- ],
- [],
- ];
- const selectMock = vi.fn(() => createThenableQuery(selectPages.shift() ?? []));
- const insertMock = vi.fn();
- vi.doMock("@/drizzle/db", () => ({
- db: {
- select: selectMock,
- insert: insertMock,
- },
- }));
- const { backfillProviderEndpointsFromProviders } = await import(
- "@/repository/provider-endpoints"
- );
- const result = await backfillProviderEndpointsFromProviders();
- expect(result).toEqual({ inserted: 0, uniqueCandidates: 0, skippedInvalid: 3 });
- expect(insertMock).not.toHaveBeenCalled();
- });
- test("backfillProviderEndpointsFromProviders: 去重 + trim + 统计 inserted/uniqueCandidates/skippedInvalid", async () => {
- vi.resetModules();
- const capturedValues: unknown[] = [];
- const insertState = { values: undefined as unknown };
- const returning = vi.fn(async () => {
- const values = insertState.values;
- if (!Array.isArray(values)) return [];
- return values.map((_, idx) => ({ id: idx + 1 }));
- });
- const onConflictDoNothing = vi.fn(() => ({ returning }));
- const values = vi.fn((payload: unknown) => {
- insertState.values = payload;
- if (Array.isArray(payload)) capturedValues.push(...payload);
- return { onConflictDoNothing };
- });
- const insertMock = vi.fn(() => ({ values }));
- const selectPages = [
- [
- { id: 1, vendorId: 1, providerType: "claude", url: " https://a.com " },
- { id: 2, vendorId: 1, providerType: "claude", url: "https://a.com" },
- { id: 3, vendorId: 1, providerType: "openai-compatible", url: "https://a.com" },
- ],
- [
- { id: 4, vendorId: 2, providerType: "claude", url: "https://a.com" },
- { id: 5, vendorId: 0, providerType: "claude", url: "https://bad-vendor.com" },
- { id: 6, vendorId: 1, providerType: "claude", url: "not a url" },
- ],
- [],
- ];
- const selectMock = vi.fn(() => createThenableQuery(selectPages.shift() ?? []));
- vi.doMock("@/drizzle/db", () => ({
- db: {
- select: selectMock,
- insert: insertMock,
- },
- }));
- const { backfillProviderEndpointsFromProviders } = await import(
- "@/repository/provider-endpoints"
- );
- const result = await backfillProviderEndpointsFromProviders();
- expect(result).toEqual({ inserted: 3, uniqueCandidates: 3, skippedInvalid: 2 });
- expect(capturedValues).toEqual(
- expect.arrayContaining([
- expect.objectContaining({ vendorId: 1, providerType: "claude", url: "https://a.com" }),
- expect.objectContaining({
- vendorId: 1,
- providerType: "openai-compatible",
- url: "https://a.com",
- }),
- expect.objectContaining({ vendorId: 2, providerType: "claude", url: "https://a.com" }),
- ])
- );
- });
- test("backfillProviderEndpointsFromProviders: 冲突不插入时 inserted=0 但 uniqueCandidates 仍统计", async () => {
- vi.resetModules();
- const insertState = { values: undefined as unknown };
- const returning = vi.fn(async () => []);
- const onConflictDoNothing = vi.fn(() => ({ returning }));
- const values = vi.fn((payload: unknown) => {
- insertState.values = payload;
- return { onConflictDoNothing };
- });
- const insertMock = vi.fn(() => ({ values }));
- const selectPages = [
- [
- { id: 1, vendorId: 1, providerType: "claude", url: "https://a.com" },
- { id: 2, vendorId: 1, providerType: "openai-compatible", url: "https://a.com" },
- ],
- [],
- ];
- const selectMock = vi.fn(() => createThenableQuery(selectPages.shift() ?? []));
- vi.doMock("@/drizzle/db", () => ({
- db: {
- select: selectMock,
- insert: insertMock,
- },
- }));
- const { backfillProviderEndpointsFromProviders } = await import(
- "@/repository/provider-endpoints"
- );
- const result = await backfillProviderEndpointsFromProviders();
- expect(result).toEqual({ inserted: 0, uniqueCandidates: 2, skippedInvalid: 0 });
- });
- test("tryDeleteProviderVendorIfEmpty: 有 active provider 时不删除", async () => {
- vi.resetModules();
- const selectPages = [[{ id: 1 }], []];
- const selectMock = vi.fn(() => createThenableQuery(selectPages.shift() ?? []));
- const deleteMock = vi.fn();
- const transactionMock = vi.fn(async (fn: (tx: any) => Promise<any>) => {
- return fn({
- select: selectMock,
- delete: deleteMock,
- });
- });
- vi.doMock("@/drizzle/db", () => ({
- db: {
- transaction: transactionMock,
- },
- }));
- const { tryDeleteProviderVendorIfEmpty } = await import("@/repository/provider-endpoints");
- const ok = await tryDeleteProviderVendorIfEmpty(123);
- expect(ok).toBe(false);
- expect(selectMock).toHaveBeenCalledTimes(1);
- expect(deleteMock).not.toHaveBeenCalled();
- });
- test("tryDeleteProviderVendorIfEmpty: 有 active endpoint 时不删除", async () => {
- vi.resetModules();
- const selectPages = [[], [{ id: 1 }]];
- const selectMock = vi.fn(() => createThenableQuery(selectPages.shift() ?? []));
- const deleteMock = vi.fn();
- const transactionMock = vi.fn(async (fn: (tx: any) => Promise<any>) => {
- return fn({
- select: selectMock,
- delete: deleteMock,
- });
- });
- vi.doMock("@/drizzle/db", () => ({
- db: {
- transaction: transactionMock,
- },
- }));
- const { tryDeleteProviderVendorIfEmpty } = await import("@/repository/provider-endpoints");
- const ok = await tryDeleteProviderVendorIfEmpty(123);
- expect(ok).toBe(false);
- expect(selectMock).toHaveBeenCalledTimes(2);
- expect(deleteMock).not.toHaveBeenCalled();
- });
- test("tryDeleteProviderVendorIfEmpty: 无 active provider/endpoint 时删除 vendor", async () => {
- vi.resetModules();
- const selectPages = [[], []];
- const selectMock = vi.fn(() => createThenableQuery(selectPages.shift() ?? []));
- let deleteCallIndex = 0;
- const deleteMock = vi.fn(() => {
- deleteCallIndex += 1;
- if (deleteCallIndex === 1) {
- return {
- where: vi.fn(async () => []),
- };
- }
- return {
- where: vi.fn(() => ({
- returning: vi.fn(async () => [{ id: 123 }]),
- })),
- };
- });
- const transactionMock = vi.fn(async (fn: (tx: any) => Promise<any>) => {
- return fn({
- select: selectMock,
- delete: deleteMock,
- });
- });
- vi.doMock("@/drizzle/db", () => ({
- db: {
- transaction: transactionMock,
- },
- }));
- const { tryDeleteProviderVendorIfEmpty } = await import("@/repository/provider-endpoints");
- const ok = await tryDeleteProviderVendorIfEmpty(123);
- expect(ok).toBe(true);
- expect(selectMock).toHaveBeenCalledTimes(2);
- expect(deleteMock).toHaveBeenCalledTimes(2);
- });
- test("tryDeleteProviderVendorIfEmpty: vendor 不存在时返回 false", async () => {
- vi.resetModules();
- const selectPages = [[], []];
- const selectMock = vi.fn(() => createThenableQuery(selectPages.shift() ?? []));
- let deleteCallIndex = 0;
- const deleteMock = vi.fn(() => {
- deleteCallIndex += 1;
- if (deleteCallIndex === 1) {
- return {
- where: vi.fn(async () => []),
- };
- }
- return {
- where: vi.fn(() => ({
- returning: vi.fn(async () => []),
- })),
- };
- });
- const transactionMock = vi.fn(async (fn: (tx: any) => Promise<any>) => {
- return fn({
- select: selectMock,
- delete: deleteMock,
- });
- });
- vi.doMock("@/drizzle/db", () => ({
- db: {
- transaction: transactionMock,
- },
- }));
- const { tryDeleteProviderVendorIfEmpty } = await import("@/repository/provider-endpoints");
- const ok = await tryDeleteProviderVendorIfEmpty(123);
- expect(ok).toBe(false);
- expect(selectMock).toHaveBeenCalledTimes(2);
- expect(deleteMock).toHaveBeenCalledTimes(2);
- });
- test("tryDeleteProviderVendorIfEmpty: transaction 抛错时抛出异常", async () => {
- vi.resetModules();
- const transactionMock = vi.fn(async () => {
- throw new Error("boom");
- });
- vi.doMock("@/drizzle/db", () => ({
- db: {
- transaction: transactionMock,
- },
- }));
- const { tryDeleteProviderVendorIfEmpty } = await import("@/repository/provider-endpoints");
- await expect(tryDeleteProviderVendorIfEmpty(123)).rejects.toThrow("boom");
- });
- test("deleteProviderVendor: vendor 存在时返回 true 且执行级联删除", async () => {
- vi.resetModules();
- let deleteCallIndex = 0;
- const deleteMock = vi.fn(() => {
- deleteCallIndex += 1;
- // 1) delete endpoints, 2) delete providers
- if (deleteCallIndex <= 2) {
- return {
- where: vi.fn(async () => []),
- };
- }
- // 3) delete vendor
- return {
- where: vi.fn(() => ({
- returning: vi.fn(async () => [{ id: 123 }]),
- })),
- };
- });
- const transactionMock = vi.fn(async (fn: (tx: any) => Promise<any>) => {
- return fn({
- delete: deleteMock,
- });
- });
- vi.doMock("@/drizzle/db", () => ({
- db: {
- transaction: transactionMock,
- },
- }));
- const { deleteProviderVendor } = await import("@/repository/provider-endpoints");
- const ok = await deleteProviderVendor(123);
- expect(ok).toBe(true);
- expect(deleteMock).toHaveBeenCalledTimes(3);
- });
- test("deleteProviderVendor: vendor 不存在时返回 false", async () => {
- vi.resetModules();
- let deleteCallIndex = 0;
- const deleteMock = vi.fn(() => {
- deleteCallIndex += 1;
- if (deleteCallIndex <= 2) {
- return {
- where: vi.fn(async () => []),
- };
- }
- return {
- where: vi.fn(() => ({
- returning: vi.fn(async () => []),
- })),
- };
- });
- const transactionMock = vi.fn(async (fn: (tx: any) => Promise<any>) => {
- return fn({
- delete: deleteMock,
- });
- });
- vi.doMock("@/drizzle/db", () => ({
- db: {
- transaction: transactionMock,
- },
- }));
- const { deleteProviderVendor } = await import("@/repository/provider-endpoints");
- const ok = await deleteProviderVendor(123);
- expect(ok).toBe(false);
- expect(deleteMock).toHaveBeenCalledTimes(3);
- });
- });
|