Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: add check that suins name has not changed to cache #256

Merged
merged 3 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
71 changes: 41 additions & 30 deletions portal/common/lib/page_fetching.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,44 +25,55 @@ import { HttpStatusCodes } from "./http/http_status_codes";

/**
* Resolves the subdomain to an object ID, and gets the corresponding resources.
*
* The `resolvedObjectId` variable is the object ID of the site that was previously resolved. If
* `null`, the object ID is resolved again.
*/
export async function resolveAndFetchPage(parsedUrl: DomainDetails): Promise<Response> {
export async function resolveAndFetchPage(
parsedUrl: DomainDetails,
resolvedObjectId: string | null,
): Promise<Response> {
const rpcUrl = getFullnodeUrl(NETWORK);
const client = new SuiClient({ url: rpcUrl });
const resolveObjectResult = await resolveObjectId(parsedUrl, client);
const isObjectId = typeof resolveObjectResult == "string";
if (isObjectId) {
console.log("Object ID: ", resolveObjectResult);
console.log("Base36 version of the object ID: ", HEXtoBase36(resolveObjectResult));
// Rerouting based on the contents of the routes object,
// constructed using the ws-resource.json.

// Initiate a fetch request to get the Routes object in case the request
// to the initial unfiltered path fails.
const routesPromise = getRoutes(client, resolveObjectResult);
if (!resolvedObjectId) {
const resolveObjectResult = await resolveObjectId(parsedUrl, client);
const isObjectId = typeof resolveObjectResult == "string";
if (!isObjectId) {
return resolveObjectResult;
}
resolvedObjectId = resolveObjectResult;
}

// Fetch the page using the initial path.
const fetchPromise = await fetchPage(client, resolveObjectResult, parsedUrl.path);
console.log("Object ID: ", resolvedObjectId);
console.log("Base36 version of the object ID: ", HEXtoBase36(resolvedObjectId));
// Rerouting based on the contents of the routes object,
// constructed using the ws-resource.json.

// If the fetch fails, check if the path can be matched using
// the Routes DF and fetch the redirected path.
if (fetchPromise.status == HttpStatusCodes.NOT_FOUND) {
const routes = await routesPromise;
if (!routes) {
console.warn("No routes found for the object ID");
return siteNotFound();
}
let matchingRoute: string | undefined;
matchingRoute = matchPathToRoute(parsedUrl.path, routes);
if (!matchingRoute) {
console.warn(`No matching route found for ${parsedUrl.path}`);
return siteNotFound();
}
return fetchPage(client, resolveObjectResult, matchingRoute);
// Initiate a fetch request to get the Routes object in case the request
// to the initial unfiltered path fails.
const routesPromise = getRoutes(client, resolvedObjectId);

// Fetch the page using the initial path.
const fetchPromise = await fetchPage(client, resolvedObjectId, parsedUrl.path);

// If the fetch fails, check if the path can be matched using
// the Routes DF and fetch the redirected path.
if (fetchPromise.status == HttpStatusCodes.NOT_FOUND) {
const routes = await routesPromise;
if (!routes) {
console.warn("No routes found for the object ID");
return siteNotFound();
}
let matchingRoute: string | undefined;
matchingRoute = matchPathToRoute(parsedUrl.path, routes);
if (!matchingRoute) {
console.warn(`No matching route found for ${parsedUrl.path}`);
return siteNotFound();
}
return fetchPromise;
return fetchPage(client, resolvedObjectId, matchingRoute);
}
return resolveObjectResult;
return fetchPromise;
}

export async function resolveObjectId(
Expand Down
126 changes: 56 additions & 70 deletions portal/common/lib/resource.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@
// SPDX-License-Identifier: Apache-2.0

// Import necessary functions and types
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { fetchResource } from './resource';
import { SuiClient } from '@mysten/sui/client';
import { HttpStatusCodes } from './http/http_status_codes';
import { checkRedirect } from './redirects';
import { fromBase64 } from '@mysten/bcs';
import { DynamicFieldStruct } from './bcs_data_parsing';
import { RESOURCE_PATH_MOVE_TYPE } from './constants';
import { beforeEach, describe, expect, test, vi } from "vitest";
import { fetchResource } from "./resource";
import { SuiClient } from "@mysten/sui/client";
import { HttpStatusCodes } from "./http/http_status_codes";
import { checkRedirect } from "./redirects";
import { fromBase64 } from "@mysten/bcs";
import { DynamicFieldStruct } from "./bcs_data_parsing";

// Mock SuiClient methods
const getObject = vi.fn();
Expand All @@ -18,94 +17,85 @@ const mockClient = {
} as unknown as SuiClient;

// Mock checkRedirect
vi.mock('./redirects', () => ({
vi.mock("./redirects", () => ({
checkRedirect: vi.fn(),
}));

// Mock fromBase64
vi.mock('@mysten/bcs', async () => {
const actual = await vi.importActual<typeof import('@mysten/bcs')>('@mysten/bcs');
vi.mock("@mysten/bcs", async () => {
const actual = await vi.importActual<typeof import("@mysten/bcs")>("@mysten/bcs");
return {
...actual,
fromBase64: vi.fn(),
};
});

vi.mock('./bcs_data_parsing', async (importOriginal) => {
const actual = await importOriginal() as typeof import('./bcs_data_parsing');
vi.mock("./bcs_data_parsing", async (importOriginal) => {
const actual = (await importOriginal()) as typeof import("./bcs_data_parsing");
return {
...actual,
DynamicFieldStruct: vi.fn(() => ({
parse: vi.fn(() => ({ value: { blob_id: '0xresourceBlobId' } })),
parse: vi.fn(() => ({ value: { blob_id: "0xresourceBlobId" } })),
})),
};
});

describe('fetchResource', () => {
describe("fetchResource", () => {
beforeEach(() => {
vi.clearAllMocks();
});

test('should return LOOP_DETECTED if objectId is already in seenResources', async () => {
const seenResources = new Set<string>(['0xParentId']);
test("should return LOOP_DETECTED if objectId is already in seenResources", async () => {
const seenResources = new Set<string>(["0xParentId"]);

const result = await fetchResource(
mockClient, '0xParentId', '/path', seenResources
);
const result = await fetchResource(mockClient, "0xParentId", "/path", seenResources);
expect(result).toBe(HttpStatusCodes.LOOP_DETECTED);
});

test('should return TOO_MANY_REDIRECTS if recursion depth exceeds MAX_REDIRECT_DEPTH',
async () => {
const seenResources = new Set<string>();
// Assuming MAX_REDIRECT_DEPTH is 3
const result = await fetchResource(
mockClient, '0xParentId', '/path', seenResources, 4
);
expect(result).toBe(HttpStatusCodes.TOO_MANY_REDIRECTS);
});
test("TOO_MANY_REDIRECTS if recursion depth exceeds MAX_REDIRECT_DEPTH", async () => {
const seenResources = new Set<string>();
// Assuming MAX_REDIRECT_DEPTH is 3
const result = await fetchResource(mockClient, "0xParentId", "/path", seenResources, 4);
expect(result).toBe(HttpStatusCodes.TOO_MANY_REDIRECTS);
});

test('should fetch resource without redirect', async () => {
test("should fetch resource without redirect", async () => {
// Mock object response
getObject.mockResolvedValueOnce({
data: {
bcs: {
dataType: 'moveObject',
bcsBytes: 'mockBcsBytes',
dataType: "moveObject",
bcsBytes: "mockBcsBytes",
},
},
});
(fromBase64 as any).mockReturnValueOnce('decodedBcsBytes');
(fromBase64 as any).mockReturnValueOnce("decodedBcsBytes");

const result = await fetchResource(mockClient, '0x1', '/path', new Set());
const result = await fetchResource(mockClient, "0x1", "/path", new Set());
expect(result).toEqual({
blob_id: '0xresourceBlobId',
objectId: '0x3cf9bff169db6f780a0a3cae7b3b770097c26342ad0c08604bc80728cfa37bdc',
version: undefined
blob_id: "0xresourceBlobId",
objectId: "0x3cf9bff169db6f780a0a3cae7b3b770097c26342ad0c08604bc80728cfa37bdc",
version: undefined,
});
expect(mockClient.getObject).toHaveBeenCalledWith({
id: '0x3cf9bff169db6f780a0a3cae7b3b770097c26342ad0c08604bc80728cfa37bdc',
id: "0x3cf9bff169db6f780a0a3cae7b3b770097c26342ad0c08604bc80728cfa37bdc",
options: { showBcs: true },
});
});

test('should follow redirect and recursively fetch resource', async () => {
test("should follow redirect and recursively fetch resource", async () => {
// Mock the redirect check to return a redirect ID on the first call
(checkRedirect as any)
.mockResolvedValueOnce(
'0x51813e7d4040265af8bd6c757f52accbe11e6df5b9cf3d6696a96e3f54fad096'
);
(checkRedirect as any)
.mockResolvedValueOnce(
undefined
(checkRedirect as any).mockResolvedValueOnce(
"0x51813e7d4040265af8bd6c757f52accbe11e6df5b9cf3d6696a96e3f54fad096",
);
(checkRedirect as any).mockResolvedValueOnce(undefined);

// Mock the first resource object response
getObject.mockResolvedValueOnce({
data: {
bcs: {
dataType: 'moveObject',
bcsBytes: 'mockBcsBytes',
dataType: "moveObject",
bcsBytes: "mockBcsBytes",
},
},
});
Expand All @@ -114,68 +104,64 @@ describe('fetchResource', () => {
getObject.mockResolvedValueOnce({
data: {
bcs: {
dataType: 'moveObject',
bcsBytes: 'mockBcsBytes',
dataType: "moveObject",
bcsBytes: "mockBcsBytes",
},
},
});

const result = await fetchResource(
mockClient, '0x1', '/path', new Set()
);
const result = await fetchResource(mockClient, "0x1", "/path", new Set());

// Verify the results
expect(result).toEqual({
blob_id: '0xresourceBlobId',
objectId: '0x3cf9bff169db6f780a0a3cae7b3b770097c26342ad0c08604bc80728cfa37bdc',
version: undefined
blob_id: "0xresourceBlobId",
objectId: "0x3cf9bff169db6f780a0a3cae7b3b770097c26342ad0c08604bc80728cfa37bdc",
version: undefined,
});
expect(checkRedirect).toHaveBeenCalledTimes(1);
});

test('should return NOT_FOUND if the resource does not contain a blob_id', async () => {
test("should return NOT_FOUND if the resource does not contain a blob_id", async () => {
const seenResources = new Set<string>();
const mockResource = {}; // No blob_id
const mockResource = {}; // No blob_id

(checkRedirect as any)
.mockResolvedValueOnce(
'0x51813e7d4040265af8bd6c757f52accbe11e6df5b9cf3d6696a96e3f54fad096'
(checkRedirect as any).mockResolvedValueOnce(
"0x51813e7d4040265af8bd6c757f52accbe11e6df5b9cf3d6696a96e3f54fad096",
);

// Mock getObject to return a valid BCS object
getObject.mockResolvedValueOnce({
data: {
bcs: {
dataType: 'moveObject',
bcsBytes: 'mockBcsBytes',
dataType: "moveObject",
bcsBytes: "mockBcsBytes",
},
},
});
getObject.mockResolvedValueOnce({
data: {
bcs: {
dataType: 'moveObject',
bcsBytes: 'mockBcsBytes',
dataType: "moveObject",
bcsBytes: "mockBcsBytes",
},
},
});

// Mock fromBase64 to simulate the decoding process
(fromBase64 as any).mockReturnValueOnce('decodedBcsBytes');
(fromBase64 as any).mockReturnValueOnce("decodedBcsBytes");

// Mock DynamicFieldStruct to return a resource without a blob_id
(DynamicFieldStruct as any).mockImplementation(() => ({
parse: () => ({ value: mockResource }),
}));

const result = await fetchResource(mockClient, '0x1', '/path', seenResources);
const result = await fetchResource(mockClient, "0x1", "/path", seenResources);

// Since the resource does not have a blob_id, the function should return NOT_FOUND
expect(result).toBe(HttpStatusCodes.NOT_FOUND);
});


test('should return NOT_FOUND if dynamic fields are not found', async () => {
test("should return NOT_FOUND if dynamic fields are not found", async () => {
const seenResources = new Set<string>();

// Mock to return no redirect
Expand All @@ -184,7 +170,7 @@ describe('fetchResource', () => {
// Mock to simulate that dynamic fields are not found
getObject.mockResolvedValueOnce(undefined);

const result = await fetchResource(mockClient, '0x1', '/path', seenResources);
const result = await fetchResource(mockClient, "0x1", "/path", seenResources);

// Check that the function returns NOT_FOUND
expect(result).toBe(HttpStatusCodes.NOT_FOUND);
Expand Down
8 changes: 4 additions & 4 deletions portal/common/package.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
{
"dependencies": {
"@mysten/bcs": "^1.0.2",
"@mysten/bcs": "^1.1.0",
"@mysten/sui": "^1.12.0",
"base-x": "^4.0.0",
"pako": "^2.1.0",
"parse-domain": "8.0.2",
"vite": "^5.4.2"
"vite": "^5.4.9"
},
"devDependencies": {
"@codspeed/vitest-plugin": "^3.1.1",
"@types/pako": "^2.0.3",
"vite-plugin-html": "^3.2.2",
"vitest": "^2.0.4",
"webpack": "^5.92.0"
"vitest": "^2.1.3",
"webpack": "^5.95.0"
},
"name": "common",
"description": "A common library that is shared among the portal implementations.",
Expand Down
Loading
Loading