Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,17 @@ export type CreateStorageObjectInput = {
file: File | Blob | Readable;
};

export type CreateSignedUrlInput = {
path: string;
expiresInSeconds?: number;
};

export type SignedUrl = {
url: string;
path: string;
expiresAt: string;
};

/**
* Auth
*/
Expand Down
6 changes: 6 additions & 0 deletions src/api/yepcodeApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import {
VersionedModuleAliasesPaginatedResult,
StorageObject,
CreateStorageObjectInput,
CreateSignedUrlInput,
SignedUrl,
Token,
Sandbox,
CreateSandboxInput,
Expand Down Expand Up @@ -796,6 +798,10 @@ export class YepCodeApi {
);
}

async createSignedUrl(data: CreateSignedUrlInput): Promise<SignedUrl> {
return this.request("POST", "/storage/signed-urls", { data });
}

// Auth endpoints
async getToken(apiToken: string): Promise<Token> {
return this.request("POST", "/auth/token", {
Expand Down
9 changes: 8 additions & 1 deletion src/storage/yepcodeStorage.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { YepCodeApi } from "../api";
import { StorageObject, YepCodeApiConfig } from "../api/types";
import { SignedUrl, StorageObject, YepCodeApiConfig } from "../api/types";
import { Readable } from "stream";

export class YepCodeStorage {
Expand Down Expand Up @@ -27,4 +27,11 @@ export class YepCodeStorage {
async delete(filename: string): Promise<void> {
await this.api.deleteObject(filename);
}

async createSignedUrl(
filename: string,
options: { expiresInSeconds?: number } = {}
): Promise<SignedUrl> {
return this.api.createSignedUrl({ path: filename, ...options });
}
}
68 changes: 66 additions & 2 deletions tests/api/yepcodeApi.storage.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { YepCodeApi } from "../../src/api/yepcodeApi";
import { StorageObject } from "../../src/api/types";
import { YepCodeApi, YepCodeApiError } from "../../src/api/yepcodeApi";
import { SignedUrl, StorageObject } from "../../src/api/types";
import fs, { createWriteStream, readFileSync } from "fs";
import path from "path";
import { Readable } from "stream";
Expand Down Expand Up @@ -113,4 +113,68 @@ describe.skip("YepCodeApi", () => {
await verifyDownloadedFile(result, downloadedFile, testFilePath);
});
});

describe("createSignedUrl", () => {
it("should return a signed url with the default expiry", async () => {
const file: File = new File([readFileSync(testFilePath)], testName);
await api.createObject({ name: testName, file });

const result: SignedUrl = await api.createSignedUrl({ path: testName });

expect(typeof result.url).toBe("string");
expect(result.url.length).toBeGreaterThan(0);
expect(result.path).toBe(testName);

const expiresAt = new Date(result.expiresAt).getTime();
const expectedExpiry = Date.now() + 3600 * 1000;
expect(Math.abs(expiresAt - expectedExpiry)).toBeLessThan(60 * 1000);
});

it("should return a signed url with a custom expiry", async () => {
const file: File = new File([readFileSync(testFilePath)], testName);
await api.createObject({ name: testName, file });

const result: SignedUrl = await api.createSignedUrl({
path: testName,
expiresInSeconds: 60,
});

const expiresAt = new Date(result.expiresAt).getTime();
const expectedExpiry = Date.now() + 60 * 1000;
expect(Math.abs(expiresAt - expectedExpiry)).toBeLessThan(30 * 1000);
});

it("should return content matching the original file when fetched", async () => {
const file: File = new File([readFileSync(testFilePath)], testName);
await api.createObject({ name: testName, file });

const { url } = await api.createSignedUrl({ path: testName });

const response = await fetch(url);
expect(response.ok).toBe(true);
const body = await response.text();
expect(body).toBe(readFileSync(testFilePath, "utf8"));
});

it("should throw a 404 when the file does not exist", async () => {
await expect(
api.createSignedUrl({ path: "does-not-exist.txt" })
).rejects.toMatchObject({
name: "YepCodeApiError",
status: 404,
} as Partial<YepCodeApiError>);
});

it("should throw a 400 when expiresInSeconds is out of range", async () => {
const file: File = new File([readFileSync(testFilePath)], testName);
await api.createObject({ name: testName, file });

await expect(
api.createSignedUrl({ path: testName, expiresInSeconds: 999999 })
).rejects.toMatchObject({
name: "YepCodeApiError",
status: 400,
} as Partial<YepCodeApiError>);
});
});
});
54 changes: 53 additions & 1 deletion tests/storage/yepcodeStorage.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { YepCodeStorage } from "../../src/storage";
import { StorageObject } from "../../src/api/types";
import { YepCodeApiError } from "../../src/api/yepcodeApi";
import { SignedUrl, StorageObject } from "../../src/api/types";
import fs, { createWriteStream, readFileSync } from "fs";
import path from "path";
import { Readable } from "stream";
Expand Down Expand Up @@ -117,4 +118,55 @@ describe.skip("YepCodeStorage", () => {
await verifyDownloadedFile(result, downloadedFile, testFilePath);
});
});

describe("createSignedUrl", () => {
it("should return a signed url with the default expiry", async () => {
const file: File = new File([readFileSync(testFilePath)], testName);
await storage.upload(testName, file);

const result: SignedUrl = await storage.createSignedUrl(testName);

expect(typeof result.url).toBe("string");
expect(result.url.length).toBeGreaterThan(0);
expect(result.path).toBe(testName);

const expiresAt = new Date(result.expiresAt).getTime();
const expectedExpiry = Date.now() + 3600 * 1000;
expect(Math.abs(expiresAt - expectedExpiry)).toBeLessThan(60 * 1000);
});

it("should return a signed url with a custom expiry", async () => {
const file: File = new File([readFileSync(testFilePath)], testName);
await storage.upload(testName, file);

const result: SignedUrl = await storage.createSignedUrl(testName, {
expiresInSeconds: 60,
});

const expiresAt = new Date(result.expiresAt).getTime();
const expectedExpiry = Date.now() + 60 * 1000;
expect(Math.abs(expiresAt - expectedExpiry)).toBeLessThan(30 * 1000);
});

it("should throw a 404 when the file does not exist", async () => {
await expect(
storage.createSignedUrl("does-not-exist.txt")
).rejects.toMatchObject({
name: "YepCodeApiError",
status: 404,
} as Partial<YepCodeApiError>);
});

it("should throw a 400 when expiresInSeconds is out of range", async () => {
const file: File = new File([readFileSync(testFilePath)], testName);
await storage.upload(testName, file);

await expect(
storage.createSignedUrl(testName, { expiresInSeconds: 999999 })
).rejects.toMatchObject({
name: "YepCodeApiError",
status: 400,
} as Partial<YepCodeApiError>);
});
});
});
Loading