From 5f1227fce3461bba7767a94d9f7e5957713fa8fc Mon Sep 17 00:00:00 2001 From: Arnaud Date: Fri, 30 May 2025 11:32:00 +0200 Subject: [PATCH] Improve tests --- .github/workflows/ci.yaml | 9 + src/data/data.spec.ts | 124 ++++++ src/debug/debug.test.ts | 53 +-- src/marketplace/marketplace.test.ts | 617 ++++++++++++---------------- src/marketplace/marketplace.ts | 29 +- src/marketplace/types.ts | 34 +- src/node/node.spec.ts | 36 ++ 7 files changed, 483 insertions(+), 419 deletions(-) create mode 100644 src/data/data.spec.ts create mode 100644 src/node/node.spec.ts diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 21aa846..ac480d0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -26,4 +26,13 @@ jobs: - run: npm ci + - name: Start codex-factory + run: npx codex-factory start latest & + + - name: Wait for client node to be started + run: npx wait-on tcp:8080 --timeout=300000 + + - name: Wait for first storage provider to be started + run: npx wait-on tcp:8081 --timeout=300000 + - run: npm test diff --git a/src/data/data.spec.ts b/src/data/data.spec.ts new file mode 100644 index 0000000..0e7e084 --- /dev/null +++ b/src/data/data.spec.ts @@ -0,0 +1,124 @@ +import { assert, describe, it } from "vitest"; +import { CodexData } from "./data"; +import { NodeUploadStategy } from "./node-upload"; +import crypto from "crypto"; + +describe("data", () => { + const data = new CodexData( + process.env["CLIENT_URL"] || "http://localhost:8080" + ); + const spData = new CodexData( + process.env["SP_URL"] || "http://localhost:8081" + ); + + it("uploads a file a download it locally", async () => { + const content = crypto.randomBytes(16).toString("hex"); + + const strategy = new NodeUploadStategy(content); + const res = data.upload(strategy); + const cid = await res.result; + assert.ok(cid.error == false); + assert.ok(cid.data); + + const cids = await data.cids(); + assert.ok(cids.error == false); + assert.ok(cids.data.content.find((c) => c.cid == cid.data)); + + const localDownload = await data.localDownload(cid.data); + assert.ok(localDownload.error == false); + assert.strictEqual(await localDownload.data.text(), content); + + const manifest = await data.fetchManifest(cid.data); + assert.ok(manifest.error == false); + assert.strictEqual(manifest.data.cid, cid.data); + + const { blockSize, datasetSize, treeCid } = manifest.data.manifest; + assert.ok(blockSize); + assert.ok(datasetSize); + assert.ok(treeCid); + }); + + it("updates the space available when storing data", async () => { + const content = crypto.randomBytes(16).toString("hex"); + + let space = await data.space(); + assert.ok(space.error == false); + assert.ok(space.data.quotaMaxBytes); + + const usedBytes = space.data.quotaUsedBytes; + + const strategy = new NodeUploadStategy(content); + const res = data.upload(strategy); + const cid = await res.result; + assert.ok(cid.error == false); + assert.ok(cid.data); + + space = await data.space(); + assert.ok(space.error == false); + assert.ok(space.data.quotaMaxBytes); + assert.ok(space.data.quotaUsedBytes > usedBytes); + }); + + it("stream downloads a file on the network", async () => { + const content = crypto.randomBytes(16).toString("hex"); + + const strategy = new NodeUploadStategy(content); + const res = spData.upload(strategy); + const cid = await res.result; + assert.ok(cid.error == false); + assert.ok(cid.data); + + const networkDownload = await data.networkDownloadStream(cid.data); + assert.ok(networkDownload.error == false); + assert.strictEqual(await networkDownload.data.text(), content); + }); + + it("downloads a file on the network", async () => { + const content = crypto.randomBytes(16).toString("hex"); + + const strategy = new NodeUploadStategy(content); + const res = spData.upload(strategy); + const cid = await res.result; + assert.ok(cid.error == false); + assert.ok(cid.data); + + const networkDownload = await data.networkDownload(cid.data); + assert.ok(networkDownload.error == false); + + const cids = await data.cids(); + assert.ok(cids.error == false); + assert.ok(cids.data.content.find((c) => c.cid == cid.data)); + }); + + it("returns an error when trying to stream download a not existing file on the network", async () => { + const cid = crypto.randomBytes(16).toString("hex"); + + const networkDownload = await data.networkDownloadStream(cid); + assert.ok(networkDownload.error); + assert.strictEqual(networkDownload.data.message, "Incorrect Cid"); + }); + + it("returns an error when trying to download a not existing file on the network", async () => { + const cid = crypto.randomBytes(16).toString("hex"); + + const networkDownload = await data.networkDownload(cid); + assert.ok(networkDownload.error); + assert.strictEqual(networkDownload.data.message, "Incorrect Cid"); + }); + + it("returns an error when trying to download a not existing file locally", async () => { + const cid = crypto.randomBytes(16).toString("hex"); + + const networkDownload = await data.localDownload(cid); + assert.ok(networkDownload.error); + assert.strictEqual(networkDownload.data.message, "Incorrect Cid"); + }); + + it("returns an error when trying to fetch a not existing manifest", async () => { + const cid = crypto.randomBytes(16).toString("hex"); + + const fetchManifest = await data.fetchManifest(cid); + assert.ok(fetchManifest.error); + assert.strictEqual(fetchManifest.data.message, "Incorrect Cid"); + }); +}); diff --git a/src/debug/debug.test.ts b/src/debug/debug.test.ts index e298bc9..66671fd 100644 --- a/src/debug/debug.test.ts +++ b/src/debug/debug.test.ts @@ -1,45 +1,26 @@ -import { afterEach, assert, describe, it, vi } from "vitest"; +import { assert, describe, it } from "vitest"; import { CodexDebug } from "./debug"; -import type { CodexLogLevel } from "./types"; -import { CodexError } from "../errors/errors"; describe("debug", () => { - afterEach(() => { - vi.restoreAllMocks(); + const debug = new CodexDebug( + process.env["CLIENT_URL"] || "http://localhost:8080" + ); + + it("changes the log level", async () => { + const logLevel = await debug.setLogLevel("NOTICE"); + assert.ok(logLevel.error == false); }); - const debug = new CodexDebug("http://localhost:3000"); - - it("returns an error when trying to setup the log level with a bad value", async () => { - const response = await debug.setLogLevel("TEST" as CodexLogLevel); - - assert.deepStrictEqual(response, { - error: true, - data: new CodexError("Cannot validate the input", { - errors: [ - { - expected: - '"TRACE" | "DEBUG" | "INFO" | "NOTICE" | "WARN" | "ERROR" | "FATAL"', - message: - 'Invalid type: Expected "TRACE" | "DEBUG" | "INFO" | "NOTICE" | "WARN" | "ERROR" | "FATAL" but received "TEST"', - path: undefined, - received: '"TEST"', - }, - ], - }), - }); + it("gets the debug info", async () => { + const info = await debug.info(); + assert.ok(info.error == false); + assert.ok(info.data.spr); + assert.ok(info.data.announceAddresses.length > 0); }); - it("returns a success when trying to setup the log level with a correct value", async () => { - const mockResponse = { - ok: true, - status: 200, - text: async () => "", - } as Response; - globalThis.fetch = vi.fn().mockResolvedValue(mockResponse); - - const response = await debug.setLogLevel("ERROR"); - - assert.deepStrictEqual(response, { error: false, data: "" }); + it("returns error when changing the log level with wrong value", async () => { + const logLevel = await debug.setLogLevel("HELLO"); + assert.ok(logLevel.error); + assert.strictEqual(logLevel.data.message, "Cannot validate the input"); }); }); diff --git a/src/marketplace/marketplace.test.ts b/src/marketplace/marketplace.test.ts index bf7c7b8..47f3ee0 100644 --- a/src/marketplace/marketplace.test.ts +++ b/src/marketplace/marketplace.test.ts @@ -1,381 +1,280 @@ -import { afterEach, assert, describe, it, vi } from "vitest"; -import { Fetch } from "../fetch-safe/fetch-safe"; +import { assert, describe, it } from "vitest"; import { CodexMarketplace } from "./marketplace"; -import { - randomEthereumAddress, - randomInt, - randomString, -} from "../tests/tests.util"; -import { CodexError } from "../errors/errors"; +import { CodexData } from "../data/data"; +import { NodeUploadStategy } from "../data/node-upload"; +import type { + CodexAvailabilityPatchInput, + CodexCreateAvailabilityInput, + CodexCreateStorageRequestInput, +} from "./types"; -function createStorageRequest() { - return { - cid: randomString(64), - duration: randomInt(1, 64000), - pricePerBytePerSecond: randomInt(1, 100), - proofProbability: randomInt(1, 100), - nodes: randomInt(1, 5), - tolerance: randomInt(1, 100), - expiry: randomInt(1, 100), - collateralPerByte: randomInt(1, 100), - }; -} - -function missingNumberValidationError(field: string) { - return { - error: true as any, - data: new CodexError("Cannot validate the input", { - errors: [ - { - path: field, - expected: "number", - message: "Invalid type: Expected number but received undefined", - received: "undefined", - }, - ], - }), - }; -} - -function extraValidationError(field: string, value: unknown) { - return { - error: true as any, - data: new CodexError("Cannot validate the input", { - errors: [ - { - path: field, - expected: "never", - message: `Invalid type: Expected never but received "${value}"`, - received: `"${value}"`, - }, - ], - }), - }; -} - -function missingStringValidationError(field: string) { - return { - error: true as any, - data: new CodexError("Cannot validate the input", { - errors: [ - { - path: field, - expected: "string", - message: "Invalid type: Expected string but received undefined", - received: "undefined", - }, - ], - }), - }; -} - -function mistypeNumberValidationError(field: string, value: string) { - return { - error: true as any, - data: new CodexError("Cannot validate the input", { - errors: [ - { - path: field, - expected: "number", - message: `Invalid type: Expected number but received "${value}"`, - received: `"${value}"`, - }, - ], - }), - }; -} - -function minNumberValidationError(field: string, min: number) { - return { - error: true as any, - data: new CodexError("Cannot validate the input", { - errors: [ - { - path: field, - expected: ">=" + min, - message: "Invalid value: Expected >=1 but received 0", - received: "0", - }, - ], - }), - }; -} - -function createAvailability() { - return { - id: randomEthereumAddress(), - totalSize: randomInt(0, 9), - duration: randomInt(0, 9), - minPrice: randomInt(0, 9), - minPricePerBytePerSecond: randomInt(0, 9), - totalCollateral: randomInt(0, 900), - totalRemainingCollateral: randomInt(0, 900), - }; -} - -describe("marketplace", () => { - const marketplace = new CodexMarketplace("http://localhost:3000"); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - it("returns an error when trying to create an availability without total size", async () => { - const response = await marketplace.createAvailability({ - duration: 3000, - totalCollateral: 1, - minPricePerBytePerSecond: 100, - } as any); - - assert.deepStrictEqual(response, missingNumberValidationError("totalSize")); - }); - - it("returns an error when trying to create an availability with an invalid number valid", async () => { - const response = await marketplace.createAvailability({ - duration: 3000, - totalCollateral: 1, - minPricePerBytePerSecond: 100, - totalSize: "abc", - } as any); - - assert.deepStrictEqual( - response, - mistypeNumberValidationError("totalSize", "abc") +describe("marketplace", async () => { + describe("availability", async () => { + const spMarketplace = new CodexMarketplace( + process.env["SP_URL"] || "http://localhost:8081" ); - }); + const totalSize = 1_000_000; + const duration = 3000; + const minPricePerBytePerSecond = 1000; + const totalCollateral = 1_000_000_000; - it("returns an error when trying to create an availability with zero total size", async () => { - const response = await marketplace.createAvailability({ - duration: 3000, - totalCollateral: 1, - minPricePerBytePerSecond: 100, - totalSize: 0, + const body = { + duration, + totalCollateral, + minPricePerBytePerSecond, + totalSize, + }; + + const result = await spMarketplace.createAvailability(body); + assert.ok(result.error == false); + + const availability = result.data; + + describe("create", async () => { + it("verifies that the availability was created successfully", async () => { + assert.ok(availability.id); + assert.strictEqual(availability.duration, duration); + assert.strictEqual(availability.freeSize, totalSize); + assert.strictEqual( + availability.minPricePerBytePerSecond, + minPricePerBytePerSecond + ); + assert.strictEqual(availability.totalCollateral, totalCollateral); + assert.strictEqual( + availability.totalRemainingCollateral, + totalCollateral + ); + assert.strictEqual(availability.totalSize, totalSize); + assert.strictEqual(availability.until, 0); + assert.ok(availability.enabled); + }); + + const errors: Partial[] = [ + { duration: 0 }, + { totalSize: 0 }, + { totalCollateral: -1 }, + { minPricePerBytePerSecond: -1 }, + ]; + + for (const err of errors) { + const field = Object.keys(err)[0] as keyof typeof err; + assert.ok(field); + + it(`fails to create availability with wrong ${field}`, async () => { + const response = await spMarketplace.createAvailability({ + ...body, + [field]: err[field], + }); + + assert.ok(response.error); + assert.ok(response.data.errors?.length); + assert.equal(response.data.errors[0]?.path, field); + assert.equal( + response.data.errors[0]?.received, + err[field]?.toString() + ); + assert.ok( + response.data.errors[0]?.message.startsWith("Invalid value:") + ); + }); + } }); - assert.deepStrictEqual(response, minNumberValidationError("totalSize", 1)); + describe("update", async () => { + async function getUpdatedAvailability() { + const availabilities = await spMarketplace.availabilities(); + assert.ok(availabilities.error == false); + return availabilities.data.find((a) => a.id == availability.id); + } + + const updates: Omit[] = [ + { enabled: false }, + { duration: 3000 }, + { minPricePerBytePerSecond: 1 }, + { totalSize: 3000 }, + { totalCollateral: 3000 }, + { until: 5000 }, + ]; + + for (const usecase of updates) { + const field = Object.keys(usecase)[0] as keyof typeof usecase; + assert.ok(field); + + it(`updates availability's ${field}`, async () => { + const response = await spMarketplace.updateAvailability({ + id: availability.id, + ...usecase, + }); + assert.ok(response.error == false); + + const updated = await getUpdatedAvailability(); + assert.ok(updated?.[field] == usecase[field]); + }); + } + + const errors: Omit[] = [ + { duration: 0 }, + { totalSize: 0 }, + { totalCollateral: -1 }, + { minPricePerBytePerSecond: -1 }, + { until: -1 }, + ]; + + for (const err of errors) { + const field = Object.keys(err)[0] as keyof typeof err; + assert.ok(field); + + it(`fails to update availability with wrong ${field}`, async () => { + const response = await spMarketplace.updateAvailability({ + id: availability.id, + ...err, + }); + + assert.ok(response.error); + assert.ok(response.data.errors?.length); + assert.equal(response.data.errors[0]?.path, field); + assert.equal( + response.data.errors[0]?.received, + err[field]?.toString() + ); + assert.ok( + response.data.errors[0]?.message.startsWith("Invalid value:") + ); + }); + } + }); }); - it("returns an error when trying to create an availability without duration", async () => { - const response = await marketplace.createAvailability({ - totalSize: 3000, - totalCollateral: 1, - minPricePerBytePerSecond: 100, - } as any); + const data = new CodexData( + process.env["CLIENT_URL"] || "http://localhost:8080" + ); + const marketplace = new CodexMarketplace( + process.env["CLIENT_URL"] || "http://localhost:8080" + ); - assert.deepStrictEqual(response, missingNumberValidationError("duration")); - }); + async function uploadContent(sizeInBytes: number) { + const content = "a".repeat(sizeInBytes); + const strategy = new NodeUploadStategy(content); + const res = data.upload(strategy); + const cid = await res.result; + assert.ok(cid.error == false); + assert.ok(cid.data); + return cid.data; + } - it("returns an error when trying to create an availability with zero duration", async () => { - const response = await marketplace.createAvailability({ - duration: 0, - totalCollateral: 1, - minPricePerBytePerSecond: 100, - totalSize: 3000, + async function createStorageRequestBody(targetSizeInBytes = 131072) { + return { + cid: await uploadContent(targetSizeInBytes), + duration: 1000, + pricePerBytePerSecond: 1, + proofProbability: 1, + expiry: 900, + collateralPerByte: 1, + nodes: 3, + tolerance: 1, + }; + } + + describe("storage request", async () => { + const body = await createStorageRequestBody(); + + it("creates successfully", async () => { + const request = await marketplace.createStorageRequest(body); + assert.ok(request.error == false); + assert.ok(request.data); }); - assert.deepStrictEqual(response, minNumberValidationError("duration", 1)); + const errors: { + request: Partial; + message: string; + }[] = [ + { request: { cid: "" }, message: "Incorrect Cid" }, + { + request: { duration: 0 }, + message: "Cannot validate the input", + }, + { + request: { pricePerBytePerSecond: 0 }, + message: "Cannot validate the input", + }, + { + request: { proofProbability: 0 }, + message: "Cannot validate the input", + }, + { + request: { expiry: 0 }, + message: "Cannot validate the input", + }, + { + request: { collateralPerByte: 0 }, + message: "Cannot validate the input", + }, + { + request: { tolerance: 0 }, + message: "Cannot validate the input", + }, + { + request: { cid: await uploadContent(1) }, + message: + "Dataset too small for erasure parameters, need at least 131072 bytes", + }, + { + request: { duration: 3000, expiry: 4000 }, + message: + "Expiry must be greater than zero and less than the request's duration", + }, + { + request: { nodes: 2, tolerance: 1 }, + message: + "Invalid parameters: parameters must satify `1 < (nodes - tolerance) ≥ tolerance`", + }, + ]; + + for (const err of errors) { + it(`fails to create storage request with wrong ${JSON.stringify(err.request)}`, async () => { + const request = await marketplace.createStorageRequest({ + ...body, + ...err.request, + }); + + assert.ok(request.error); + assert.ok(request.data.message.includes(err.message)); + + if (request.data.errors?.length) { + const keys = Object.keys(err.request); + for (const e of request.data.errors) { + assert.ok(e.path); + assert.ok(keys.includes(e.path)); + } + } + }); + } }); - it("returns an error when trying to create an availability without min price", async () => { - const response = await marketplace.createAvailability({ - totalSize: 3000, - totalCollateral: 1, - duration: 100, - } as any); + describe("purchases", async () => { + const body = await createStorageRequestBody(); - assert.deepStrictEqual(response, missingNumberValidationError("minPrice")); - }); + const request = await marketplace.createStorageRequest(body); + assert.ok(request.error == false); + assert.ok(request.data); - it("returns an error when trying to create an availability without max collateral", async () => { - const response = await marketplace.createAvailability({ - totalSize: 3000, - minPricePerBytePerSecond: 100, - duration: 100, - } as any); + it("lists successfully", async () => { + const ids = await marketplace.purchaseIds(); - assert.deepStrictEqual( - response, - missingNumberValidationError("maxCollateral") - ); - }); + assert.ok(ids.error == false); + assert.ok(ids.data.length); + assert.ok(ids.data[0]); - it("returns an error when trying to create an availability with an extra field", async () => { - const response = await marketplace.createAvailability({ - maxCollateral: 1, - totalSize: 3000, - minPricePerBytePerSecond: 100, - duration: 100, - hello: "world", - } as any); + const purchase = await marketplace.purchaseDetail(ids.data[0]); + assert.ok(purchase.error == false); + assert.ok(purchase.data.requestId); + assert.ok(purchase.data.state); - assert.deepStrictEqual(response, extraValidationError("hello", "world")); - }); - - it("returns a response when the request succeed", async () => { - const data = { ...createAvailability(), freeSize: 1000 }; - - const spy = vi.spyOn(Fetch, "safeJson"); - spy.mockImplementationOnce(() => Promise.resolve({ error: false, data })); - - const response = await marketplace.createAvailability({ - totalCollateral: 1, - totalSize: 3000, - minPricePerBytePerSecond: 100, - duration: 100, + const purchases = await marketplace.purchases(); + assert.ok(purchases.error == false); + assert.ok(purchases.data.length); + assert.ok(purchases.data[0]?.requestId); + assert.ok(purchases.data[0]?.state); }); - - assert.ok(!response.error); - // @ts-ignore - assert.deepEqual(response.data, data); - }); - - it("returns a response when the create availability succeed", async () => { - const data = { ...createAvailability(), freeSize: 1000 }; - - const spy = vi.spyOn(Fetch, "safeJson"); - spy.mockImplementationOnce(() => Promise.resolve({ error: false, data })); - - const response = await marketplace.createAvailability({ - totalCollateral: data.totalCollateral, - totalSize: data.totalSize, - minPricePerBytePerSecond: data.minPricePerBytePerSecond, - duration: data.duration, - }); - - assert.ok(!response.error); - // @ts-ignore - assert.deepEqual(response.data, data); - }); - - it("returns an error when trying to update an availability without id", async () => { - const response = await marketplace.updateAvailability({ - totalCollateral: 1, - totalSize: 3000, - minPricePerBytePerSecond: 100, - duration: 100, - } as any); - - assert.deepStrictEqual(response, missingStringValidationError("id")); - }); - - it("returns an error when trying to update an availability with zero total size", async () => { - const response = await marketplace.updateAvailability({ - id: randomString(64), - totalSize: 0, - minPricePerBytePerSecond: 100, - duration: 100, - totalCollateral: 100, - }); - - assert.deepStrictEqual(response, minNumberValidationError("totalSize", 1)); - }); - - it("returns an error when trying to update an availability with zero duration", async () => { - const response = await marketplace.updateAvailability({ - id: randomString(64), - totalSize: 100, - duration: 0, - minPricePerBytePerSecond: 100, - totalCollateral: 100, - }); - - assert.deepStrictEqual(response, minNumberValidationError("duration", 1)); - }); - - it("returns a response when the update availability succeed", async () => { - const mockResponse = { - ok: true, - status: 200, - } as any; - globalThis.fetch = vi.fn().mockResolvedValue(mockResponse); - - const response = await marketplace.updateAvailability({ - id: randomString(64), - totalSize: 3000, - duration: 10, - minPricePerBytePerSecond: 100, - totalCollateral: 100, - }); - - assert.ok(!response.error); - }); - - it("returns an error when trying to create a storage request without cid", async () => { - const { cid, ...rest } = createStorageRequest(); - - const response = await marketplace.createStorageRequest(rest as any); - - assert.deepStrictEqual(response, missingStringValidationError("cid")); - }); - - it("returns an error when trying to create a storage request without duration", async () => { - const { duration, ...rest } = createStorageRequest(); - - const response = await marketplace.createStorageRequest(rest as any); - - assert.deepStrictEqual(response, missingNumberValidationError("duration")); - }); - - it("returns an error when trying to create a storage request with zero duration", async () => { - const { duration, ...rest } = createStorageRequest(); - - const response = await marketplace.createStorageRequest({ - ...rest, - duration: 0, - }); - - assert.deepStrictEqual(response, minNumberValidationError("duration", 1)); - }); - - it("returns an error when trying to create a storage request without pricePerBytePerSecond", async () => { - const { pricePerBytePerSecond, ...rest } = createStorageRequest(); - - const response = await marketplace.createStorageRequest(rest as any); - - assert.deepStrictEqual( - response, - missingNumberValidationError("pricePerBytePerSecond") - ); - }); - - it("returns an error when trying to create a storage request without proof probability", async () => { - const { proofProbability, ...rest } = createStorageRequest(); - - const response = await marketplace.createStorageRequest(rest as any); - - assert.deepStrictEqual( - response, - missingNumberValidationError("proofProbability") - ); - }); - - it("returns an error when trying to create a storage request without expiry", async () => { - const { expiry, ...rest } = createStorageRequest(); - - const response = await marketplace.createStorageRequest(rest as any); - - assert.deepStrictEqual(response, missingNumberValidationError("expiry")); - }); - - it("returns an error when trying to create a storage request with zero expiry", async () => { - const { expiry, ...rest } = createStorageRequest(); - - const response = await marketplace.createStorageRequest({ - ...rest, - expiry: 0, - }); - - assert.deepStrictEqual(response, minNumberValidationError("expiry", 1)); - }); - - it("returns an error when trying to create a storage request without collateralPerByte", async () => { - const { collateralPerByte, ...rest } = createStorageRequest(); - - const response = await marketplace.createStorageRequest(rest as any); - - assert.deepStrictEqual( - response, - missingNumberValidationError("collateralPerByte") - ); }); }); diff --git a/src/marketplace/marketplace.ts b/src/marketplace/marketplace.ts index 04cd737..dcb2a3a 100644 --- a/src/marketplace/marketplace.ts +++ b/src/marketplace/marketplace.ts @@ -25,6 +25,7 @@ import { type CodexStorageRequestCreateBody, type CodexReservation, type CodexPurchaseWithoutTypes, + type CodexAvailabilityPatchBody, } from "./types"; import { CodexCreateAvailabilityInput, @@ -179,15 +180,27 @@ export class CodexMarketplace { const url = this.url + Api.config.prefix + "/sales/availability/" + result.output.id; - const body: CodexAvailabilityCreateBody = { - totalSize: result.output.totalSize, - duration: result.output.duration, - minPricePerBytePerSecond: - result.output.minPricePerBytePerSecond.toString(), - totalCollateral: result.output.totalCollateral.toString(), - }; + const { totalSize, duration, minPricePerBytePerSecond, totalCollateral } = + result.output; + let body: CodexAvailabilityPatchBody = {}; - if (result.output.enabled) { + if (totalSize) { + body.totalSize = totalSize; + } + + if (duration) { + body.duration = duration; + } + + if (minPricePerBytePerSecond) { + body.minPricePerBytePerSecond = minPricePerBytePerSecond.toString(); + } + + if (totalCollateral) { + body.totalCollateral = totalCollateral.toString(); + } + + if (result.output.enabled != undefined) { body.enabled = result.output.enabled; } diff --git a/src/marketplace/types.ts b/src/marketplace/types.ts index 35fa4e1..e736509 100644 --- a/src/marketplace/types.ts +++ b/src/marketplace/types.ts @@ -45,19 +45,21 @@ export type CodexAvailabilityCreateBody = Exclude< export const CodexCreateAvailabilityInput = v.strictObject({ totalSize: v.pipe(v.number(), v.minValue(1)), duration: v.pipe(v.number(), v.minValue(1)), - minPricePerBytePerSecond: v.number(), - totalCollateral: v.number(), + minPricePerBytePerSecond: v.pipe(v.number(), v.minValue(0)), + totalCollateral: v.pipe(v.number(), v.minValue(0)), enabled: v.optional(v.boolean()), - until: v.optional(v.number()), + until: v.optional(v.pipe(v.number(), v.minValue(0))), }); export type CodexAvailabilityPatchResponse = paths["/sales/availability/{id}"]["patch"]["responses"][204]["content"]; -export type CodexAvailabilityPatchBody = Exclude< - paths["/sales/availability"]["post"]["requestBody"], - undefined ->["content"]["application/json"]; +export type CodexAvailabilityPatchBody = Partial< + Exclude< + paths["/sales/availability"]["post"]["requestBody"], + undefined + >["content"]["application/json"] +>; export type CodexCreateAvailabilityInput = v.InferOutput< typeof CodexCreateAvailabilityInput @@ -65,12 +67,12 @@ export type CodexCreateAvailabilityInput = v.InferOutput< export const CodexAvailabilityPatchInput = v.strictObject({ id: v.string(), - totalSize: v.pipe(v.number(), v.minValue(1)), - duration: v.pipe(v.number(), v.minValue(1)), - minPricePerBytePerSecond: v.number(), - totalCollateral: v.number(), + totalSize: v.optional(v.pipe(v.number(), v.minValue(1))), + duration: v.optional(v.pipe(v.number(), v.minValue(1))), + minPricePerBytePerSecond: v.optional(v.pipe(v.number(), v.minValue(1))), + totalCollateral: v.optional(v.pipe(v.number(), v.minValue(0))), enabled: v.optional(v.boolean()), - until: v.optional(v.number()), + until: v.optional(v.pipe(v.number(), v.minValue(0))), }); export type CodexAvailabilityPatchInput = v.InferOutput< @@ -120,12 +122,12 @@ export type CodexStorageRequestCreateBody = Exclude< export const CodexCreateStorageRequestInput = v.strictObject({ cid: v.string(), duration: v.pipe(v.number(), v.minValue(1)), - pricePerBytePerSecond: v.number(), - proofProbability: v.number(), + pricePerBytePerSecond: v.pipe(v.number(), v.minValue(1)), + proofProbability: v.pipe(v.number(), v.minValue(1)), nodes: v.optional(v.number(), 1), - tolerance: v.optional(v.number(), 0), + tolerance: v.optional(v.pipe(v.number(), v.minValue(1)), 1), expiry: v.pipe(v.number(), v.minValue(1)), - collateralPerByte: v.number(), + collateralPerByte: v.pipe(v.number(), v.minValue(1)), }); export type CodexCreateStorageRequestInput = v.InferOutput< diff --git a/src/node/node.spec.ts b/src/node/node.spec.ts new file mode 100644 index 0000000..e4be4a0 --- /dev/null +++ b/src/node/node.spec.ts @@ -0,0 +1,36 @@ +import { assert, describe, expect, it, vi } from "vitest"; +import { CodexNode } from "./node"; +import { Fetch } from "../fetch-safe/fetch-safe"; + +describe("node", () => { + const clientUrl = process.env["CLIENT_URL"] || "http://localhost:8080"; + const node = new CodexNode(clientUrl); + + it("gets the json spr", async () => { + const spr = await node.spr("json"); + assert.ok(spr.error == false); + assert.ok(spr.data); + }); + + it("gets the text spr", async () => { + const spr = await node.spr("text"); + assert.ok(spr.error == false); + assert.ok(spr.data); + }); + + it("connects to a peer", async () => { + const spy = vi.spyOn(Fetch, "safeText"); + spy.mockImplementationOnce(() => + Promise.resolve({ error: false, data: "" }) + ); + + await node.connect("1234", ["5678"]); + expect(spy).toHaveBeenCalledWith( + clientUrl + "/api/codex/v1/connect/1234?addrs=5678", + { + headers: {}, + method: "GET", + } + ); + }); +});