diff --git a/browser/data-browser/src/routes/PruneTestsRoute.tsx b/browser/data-browser/src/routes/PruneTestsRoute.tsx new file mode 100644 index 000000000..53043363c --- /dev/null +++ b/browser/data-browser/src/routes/PruneTestsRoute.tsx @@ -0,0 +1,40 @@ +import { Resource, Server, useStore } from '@tomic/react'; +import React, { useState } from 'react'; +import { Button } from '../components/Button'; +import { ContainerFull } from '../components/Containers'; +import { Column } from '../components/Row'; + +export function PruneTestsRoute(): JSX.Element { + const store = useStore(); + const [result, setResult] = useState>(); + const [isWaiting, setIsWaiting] = useState(false); + + const postPruneTest = async () => { + setIsWaiting(true); + const url = new URL('/prunetests', store.getServerUrl()); + const res = await store.postToServer(url.toString()); + setIsWaiting(false); + setResult(res); + }; + + return ( +
+ +

Prune Test Data

+

+ Pruning test data will delete all drives on the server that have + ’testdrive’ in their name. +

+ + + {isWaiting &&

Pruning, this might take a while...

} +

+ {result && `✅ ${result.props.responseMessage}`} +

+
+
+
+ ); +} diff --git a/browser/data-browser/src/routes/Routes.tsx b/browser/data-browser/src/routes/Routes.tsx index 6e5ba16d6..5b8f9a6f1 100644 --- a/browser/data-browser/src/routes/Routes.tsx +++ b/browser/data-browser/src/routes/Routes.tsx @@ -20,6 +20,7 @@ import { Sandbox } from './Sandbox'; import { TokenRoute } from './TokenRoute'; import { ImporterPage } from '../views/ImporterPage'; import { History } from './History'; +import { PruneTestsRoute } from './PruneTestsRoute'; const homeURL = window.location.origin; @@ -48,6 +49,7 @@ export function AppRoutes(): JSX.Element { } /> } /> } /> + {isDev && } />} {isDev && } />} } /> } /> diff --git a/browser/data-browser/src/routes/paths.tsx b/browser/data-browser/src/routes/paths.tsx index 28b3fe05c..66531d2a7 100644 --- a/browser/data-browser/src/routes/paths.tsx +++ b/browser/data-browser/src/routes/paths.tsx @@ -16,4 +16,5 @@ export const paths = { allVersions: '/all-versions', sandbox: '/sandbox', fetchBookmark: '/fetch-bookmark', + pruneTests: '/prunetests', }; diff --git a/browser/data-browser/tests/global.setup.ts b/browser/data-browser/tests/global.setup.ts new file mode 100644 index 000000000..a8daa89b3 --- /dev/null +++ b/browser/data-browser/tests/global.setup.ts @@ -0,0 +1,12 @@ +import { test as setup, expect } from '@playwright/test'; +import { before, FRONTEND_URL, signIn } from './test-utils'; + +setup('delete previous test data', async ({ page }) => { + await before({ page }); + await signIn(page); + await page.goto(`${FRONTEND_URL}/prunetests`); + await expect(page.getByText('Prune Test Data')).toBeVisible(); + await page.getByRole('button', { name: 'Prune' }).click(); + + await expect(page.getByTestId('prune-result')).toBeVisible(); +}); diff --git a/browser/data-browser/tests/playwright.config.ts b/browser/data-browser/tests/playwright.config.ts index 4ab353986..0cdf78204 100644 --- a/browser/data-browser/tests/playwright.config.ts +++ b/browser/data-browser/tests/playwright.config.ts @@ -11,9 +11,14 @@ const config: PlaywrightTestConfig = { retries: 3, // timeout: 1000 * 120, // 2 minutes projects: [ + { + name: 'setup', + testMatch: /global.setup\.ts/, + }, { name: 'chromium', use: { ...devices['Desktop Chrome'] }, + dependencies: ['setup'], }, ], // projects: [ diff --git a/browser/lib/src/client.ts b/browser/lib/src/client.ts index 55763cbb6..c4e2881e2 100644 --- a/browser/lib/src/client.ts +++ b/browser/lib/src/client.ts @@ -128,7 +128,7 @@ export class Client { subject: string, opts: FetchResourceOptions = {}, ): Promise { - const { signInfo, from, body: bodyReq } = opts; + const { signInfo, from, body: bodyReq, method } = opts; let createdResources: Resource[] = []; const parser = new JSONADParser(); let resource = new Resource(subject); @@ -160,7 +160,7 @@ export class Client { const response = await this.fetch(url, { headers: requestHeaders, - method: bodyReq ? 'POST' : 'GET', + method: method ?? 'GET', body: bodyReq, }); const body = await response.text(); diff --git a/browser/lib/src/ontologies/server.ts b/browser/lib/src/ontologies/server.ts index ca64536ca..384c51785 100644 --- a/browser/lib/src/ontologies/server.ts +++ b/browser/lib/src/ontologies/server.ts @@ -12,6 +12,8 @@ export const server = { redirect: 'https://atomicdata.dev/classes/Redirect', file: 'https://atomicdata.dev/classes/File', invite: 'https://atomicdata.dev/classes/Invite', + endpointResponse: + 'https://atomicdata.dev/ontology/server/class/endpoint-response', }, properties: { drives: 'https://atomicdata.dev/properties/drives', @@ -35,6 +37,9 @@ export const server = { children: 'https://atomicdata.dev/properties/children', parameters: 'https://atomicdata.dev/properties/endpoint/parameters', destination: 'https://atomicdata.dev/properties/destination', + status: 'https://atomicdata.dev/ontology/server/property/status', + responseMessage: + 'https://atomicdata.dev/ontology/server/property/response-message', }, } as const; @@ -46,6 +51,7 @@ export namespace Server { export type Redirect = typeof server.classes.redirect; export type File = typeof server.classes.file; export type Invite = typeof server.classes.invite; + export type EndpointResponse = typeof server.classes.endpointResponse; } declare module '../index.js' { @@ -92,6 +98,13 @@ declare module '../index.js' { | typeof server.properties.users | typeof server.properties.usagesLeft; }; + [server.classes.endpointResponse]: { + requires: + | BaseProps + | typeof server.properties.status + | typeof server.properties.responseMessage; + recommends: never; + }; } interface PropTypeMapping { @@ -116,6 +129,8 @@ declare module '../index.js' { [server.properties.children]: string[]; [server.properties.parameters]: string[]; [server.properties.destination]: string; + [server.properties.status]: number; + [server.properties.responseMessage]: string; } interface PropSubjectToNameMapping { @@ -140,5 +155,7 @@ declare module '../index.js' { [server.properties.children]: 'children'; [server.properties.parameters]: 'parameters'; [server.properties.destination]: 'destination'; + [server.properties.status]: 'status'; + [server.properties.responseMessage]: 'responseMessage'; } } diff --git a/browser/lib/src/store.ts b/browser/lib/src/store.ts index b5c186be2..6a6020cbb 100644 --- a/browser/lib/src/store.ts +++ b/browser/lib/src/store.ts @@ -17,6 +17,7 @@ import { FileOrFileLike, OptionalClass, UnknownClass, + Server, } from './index.js'; import { authenticate, fetchWebSocket, startWebsocket } from './websockets.js'; @@ -533,10 +534,10 @@ export class Store { } /** Sends an HTTP POST request to the server to the Subject. Parses the returned Resource and adds it to the store. */ - public async postToServer( + public async postToServer( url: string, - data: ArrayBuffer | string, - ): Promise { + data?: ArrayBuffer | string, + ): Promise> { return this.fetchResourceFromServer(url, { body: data, noWebSocket: true, diff --git a/lib/src/endpoints.rs b/lib/src/endpoints.rs index 40735fbed..61f760532 100644 --- a/lib/src/endpoints.rs +++ b/lib/src/endpoints.rs @@ -85,5 +85,7 @@ pub fn default_endpoints() -> Vec { plugins::bookmark::bookmark_endpoint(), plugins::importer::import_endpoint(), plugins::query::query_endpoint(), + #[cfg(debug_assertions)] + plugins::prunetests::prune_tests_endpoint(), ] } diff --git a/lib/src/plugins/mod.rs b/lib/src/plugins/mod.rs index a02b34634..31a2bdcf1 100644 --- a/lib/src/plugins/mod.rs +++ b/lib/src/plugins/mod.rs @@ -43,6 +43,7 @@ pub mod invite; pub mod bookmark; pub mod files; pub mod path; +pub mod prunetests; pub mod query; pub mod search; pub mod versioning; diff --git a/lib/src/plugins/prunetests.rs b/lib/src/plugins/prunetests.rs new file mode 100644 index 000000000..d58d91aea --- /dev/null +++ b/lib/src/plugins/prunetests.rs @@ -0,0 +1,70 @@ +use crate::{ + endpoints::{Endpoint, HandleGetContext, HandlePostContext}, + errors::AtomicResult, + storelike::Query, + urls, Resource, Storelike, Value, +}; + +pub fn prune_tests_endpoint() -> Endpoint { + Endpoint { + path: urls::PATH_PRUNE_TESTS.into(), + params: [].into(), + description: "Deletes all drives with 'testdrive-' in their name.".to_string(), + shortname: "prunetests".to_string(), + handle: Some(handle_get), + handle_post: Some(handle_prune_tests_request), + } +} + +pub fn handle_get(context: HandleGetContext) -> AtomicResult { + prune_tests_endpoint().to_resource(context.store) +} + +// Delete all drives with 'testdrive-' in their name. (These drive are generated with each e2e test run) +fn handle_prune_tests_request(context: HandlePostContext) -> AtomicResult { + let HandlePostContext { store, .. } = context; + + let mut query = Query::new_class(urls::DRIVE); + query.for_agent = context.for_agent.clone(); + let mut deleted_drives = 0; + + if let Ok(mut query_result) = store.query(&query) { + println!( + "Received prune request, deleting {} drives", + query_result.resources.len() + ); + + let total_drives = query_result.resources.len(); + + for resource in query_result.resources.iter_mut() { + if let Value::String(name) = resource + .get(urls::NAME) + .unwrap_or(&Value::String("".to_string())) + { + if name.contains("testdrive-") { + resource.destroy(store)?; + deleted_drives += 1; + + if (deleted_drives % 10) == 0 { + println!("Deleted {} of {} drives", deleted_drives, total_drives); + } + } + } + } + + println!("Done pruning drives"); + } else { + println!("Received prune request but there are no drives to prune"); + } + + let resource = build_response(store, 200, format!("Deleted {} drives", deleted_drives)); + Ok(resource) +} + +fn build_response(store: &impl Storelike, status: i32, message: String) -> Resource { + let mut resource = Resource::new_generate_subject(store); + resource.set_class(urls::ENDPOINT_RESPONSE); + resource.set_propval_unsafe(urls::STATUS.to_string(), status.into()); + resource.set_propval_unsafe(urls::RESPONSE_MESSAGE.to_string(), message.into()); + resource +} diff --git a/lib/src/urls.rs b/lib/src/urls.rs index 39f051e7f..68279b899 100644 --- a/lib/src/urls.rs +++ b/lib/src/urls.rs @@ -20,6 +20,8 @@ pub const IMPORTER: &str = "https://atomicdata.dev/classes/Importer"; pub const ERROR: &str = "https://atomicdata.dev/classes/Error"; pub const BOOKMARK: &str = "https://atomicdata.dev/class/Bookmark"; pub const ONTOLOGY: &str = "https://atomicdata.dev/class/ontology"; +pub const ENDPOINT_RESPONSE: &str = + "https://atomicdata.dev/ontology/server/class/endpoint-response"; // Properties pub const SHORTNAME: &str = "https://atomicdata.dev/properties/shortname"; @@ -119,7 +121,10 @@ pub const LOCAL_ID: &str = "https://atomicdata.dev/properties/localId"; pub const PROPERTIES: &str = "https://atomicdata.dev/properties/properties"; pub const CLASSES: &str = "https://atomicdata.dev/properties/classes"; pub const INSTANCES: &str = "https://atomicdata.dev/properties/instances"; - +// ... for Endpoint-Response +pub const STATUS: &str = "https://atomicdata.dev/ontology/server/property/status"; +pub const RESPONSE_MESSAGE: &str = + "https://atomicdata.dev/ontology/server/property/response-message"; // Datatypes pub const STRING: &str = "https://atomicdata.dev/datatypes/string"; pub const MARKDOWN: &str = "https://atomicdata.dev/datatypes/markdown"; @@ -150,3 +155,4 @@ pub fn construct_path_import(base: &str) -> String { pub const PATH_IMPORT: &str = "/import"; pub const PATH_FETCH_BOOKMARK: &str = "/fetch-bookmark"; pub const PATH_QUERY: &str = "/query"; +pub const PATH_PRUNE_TESTS: &str = "/prunetests";