@opencloning/opencloningdb 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/package.json +30 -0
- package/src/GetPrimerComponent.cy.jsx +57 -0
- package/src/GetPrimerComponent.jsx +43 -0
- package/src/GetSequenceFileAndDatabaseIdComponent.cy.jsx +102 -0
- package/src/GetSequenceFileAndDatabaseIdComponent.jsx +62 -0
- package/src/LoadHistoryComponent.cy.jsx +95 -0
- package/src/LoadHistoryComponent.jsx +35 -0
- package/src/OpenCloningDBInterface.js +141 -0
- package/src/OpenCloningDBInterface.multipart.test.js +65 -0
- package/src/OpenCloningDBInterface.test.js +156 -0
- package/src/PrimerSelect.cy.jsx +51 -0
- package/src/PrimerSelect.jsx +40 -0
- package/src/PrimersNotInDatabaseComponent.cy.jsx +57 -0
- package/src/PrimersNotInDatabaseComponent.jsx +40 -0
- package/src/SequenceSelect.cy.jsx +32 -0
- package/src/SequenceSelect.jsx +44 -0
- package/src/SubmitToDatabaseComponent.cy.jsx +74 -0
- package/src/SubmitToDatabaseComponent.jsx +44 -0
- package/src/common.js +60 -0
- package/src/common.test.js +108 -0
- package/src/endpoints.js +46 -0
- package/src/index.js +4 -0
- package/src/testUtils.test.js +90 -0
- package/vitest.config.js +8 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
baseUrl,
|
|
5
|
+
clearWorkspaceHeader,
|
|
6
|
+
openCloningDBHttpClient,
|
|
7
|
+
setUnauthorizedHandler,
|
|
8
|
+
setWorkspaceHeader,
|
|
9
|
+
} from './common';
|
|
10
|
+
|
|
11
|
+
const requestFulfilled = openCloningDBHttpClient.interceptors.request.handlers[0].fulfilled;
|
|
12
|
+
const responseFulfilled = openCloningDBHttpClient.interceptors.response.handlers[0].fulfilled;
|
|
13
|
+
const responseRejected = openCloningDBHttpClient.interceptors.response.handlers[0].rejected;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
localStorage.clear();
|
|
17
|
+
clearWorkspaceHeader();
|
|
18
|
+
setUnauthorizedHandler(null);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
localStorage.clear();
|
|
23
|
+
clearWorkspaceHeader();
|
|
24
|
+
setUnauthorizedHandler(null);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('common', () => {
|
|
28
|
+
it('exports the expected base URL', () => {
|
|
29
|
+
expect(baseUrl).toBe('http://localhost:8001');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('sets and clears the workspace header', () => {
|
|
33
|
+
setWorkspaceHeader(12);
|
|
34
|
+
|
|
35
|
+
expect(openCloningDBHttpClient.defaults.headers.common['X-Workspace-Id']).toBe(12);
|
|
36
|
+
|
|
37
|
+
clearWorkspaceHeader();
|
|
38
|
+
|
|
39
|
+
expect(openCloningDBHttpClient.defaults.headers.common['X-Workspace-Id']).toBeUndefined();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('serializes scalars and arrays while skipping nullish values', () => {
|
|
43
|
+
const serialized = openCloningDBHttpClient.defaults.paramsSerializer({
|
|
44
|
+
single: 'value',
|
|
45
|
+
number: 3,
|
|
46
|
+
ignored: undefined,
|
|
47
|
+
ignoredToo: null,
|
|
48
|
+
list: ['a', undefined, 'b', null, 4],
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(serialized).toBe('single=value&number=3&list=a&list=b&list=4');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('returns an empty query string when params are missing', () => {
|
|
55
|
+
expect(openCloningDBHttpClient.defaults.paramsSerializer()).toBe('');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('adds the bearer token to request headers when present', () => {
|
|
59
|
+
localStorage.setItem('token', '__TEST_TOKEN__');
|
|
60
|
+
const config = { headers: {} };
|
|
61
|
+
|
|
62
|
+
const result = requestFulfilled(config);
|
|
63
|
+
|
|
64
|
+
expect(result).toBe(config);
|
|
65
|
+
expect(config.headers.Authorization).toBe('Bearer __TEST_TOKEN__');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('leaves request headers unchanged when there is no token', () => {
|
|
69
|
+
const config = { headers: {} };
|
|
70
|
+
|
|
71
|
+
const result = requestFulfilled(config);
|
|
72
|
+
|
|
73
|
+
expect(result).toBe(config);
|
|
74
|
+
expect(config.headers.Authorization).toBeUndefined();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('passes successful responses through unchanged', () => {
|
|
78
|
+
const response = { data: { ok: true } };
|
|
79
|
+
|
|
80
|
+
expect(responseFulfilled(response)).toBe(response);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('calls the unauthorized handler for 401 responses', async () => {
|
|
84
|
+
const handler = vi.fn();
|
|
85
|
+
const error = { response: { status: 401 } };
|
|
86
|
+
setUnauthorizedHandler(handler);
|
|
87
|
+
|
|
88
|
+
await expect(responseRejected(error)).rejects.toBe(error);
|
|
89
|
+
|
|
90
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('does not call the unauthorized handler for non-401 responses', async () => {
|
|
94
|
+
const handler = vi.fn();
|
|
95
|
+
const error = { response: { status: 500 } };
|
|
96
|
+
setUnauthorizedHandler(handler);
|
|
97
|
+
|
|
98
|
+
await expect(responseRejected(error)).rejects.toBe(error);
|
|
99
|
+
|
|
100
|
+
expect(handler).not.toHaveBeenCalled();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('rejects 401 responses even when no unauthorized handler is set', async () => {
|
|
104
|
+
const error = { response: { status: 401 } };
|
|
105
|
+
|
|
106
|
+
await expect(responseRejected(error)).rejects.toBe(error);
|
|
107
|
+
});
|
|
108
|
+
});
|
package/src/endpoints.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const endpoints = {
|
|
2
|
+
sequences: '/sequences',
|
|
3
|
+
sequence: (id) => `/sequences/${id}`,
|
|
4
|
+
sequenceTextFile: (id) => `/sequences/${id}/text_file_sequence`,
|
|
5
|
+
sequenceChangeAnnotation: (id) => `/sequences/${id}/change_annotation`,
|
|
6
|
+
sequenceCloningStrategy: (id) => `/sequences/${id}/cloning_strategy`,
|
|
7
|
+
sequenceChildren: (id) => `/sequences/${id}/children`,
|
|
8
|
+
sequenceLines: (id) => `/sequences/${id}/lines`,
|
|
9
|
+
sequenceChangeCircularity: (id) => `/sequences/${id}/change_circularity`,
|
|
10
|
+
sequencePrimers: (id) => `/sequences/${id}/primers`,
|
|
11
|
+
sequenceSequencingFiles: (id) => `/sequences/${id}/sequencing_files`,
|
|
12
|
+
sequenceSearch: '/sequences/search',
|
|
13
|
+
sequencesValidateUpload: '/sequences/validate-upload',
|
|
14
|
+
sequencesBulk: '/sequences/bulk',
|
|
15
|
+
sequencingFileDownload: (id) => `/sequencing_files/${id}/download`,
|
|
16
|
+
sequenceSequencingFileDelete: (sequenceId, fileId) => `/sequences/${sequenceId}/sequencing_files/${fileId}`,
|
|
17
|
+
postSequence: '/sequences',
|
|
18
|
+
primers: '/primers',
|
|
19
|
+
primersValidateUpload: '/primers/validate-upload',
|
|
20
|
+
primersBulk: '/primers/bulk',
|
|
21
|
+
primer: (id) => `/primers/${id}`,
|
|
22
|
+
primerTemplateSequences: (id) => `/primers/${id}/sequences`,
|
|
23
|
+
postPrimer: '/primers',
|
|
24
|
+
lines: '/lines',
|
|
25
|
+
line: (id) => `/lines/${id}`,
|
|
26
|
+
lineChildren: (id) => `/lines/${id}/children`,
|
|
27
|
+
lineTags: (id) => `/lines/${id}/tags`,
|
|
28
|
+
inputEntityTags: (id) => `/input_entities/${id}/tags`,
|
|
29
|
+
postLine: '/lines',
|
|
30
|
+
tags: '/tags',
|
|
31
|
+
tagPost: '/tags',
|
|
32
|
+
tagUnlinkLine: (lineId, tagId) => `/lines/${lineId}/tags/${tagId}`,
|
|
33
|
+
tagUnlinkInputEntity: (inputEntityId, tagId) => `/input_entities/${inputEntityId}/tags/${tagId}`,
|
|
34
|
+
sequenceSamples: '/sequence_samples',
|
|
35
|
+
postSequenceSample: '/sequence_samples',
|
|
36
|
+
sequenceSample: (uid) => `/sequence_samples/${uid}`,
|
|
37
|
+
authToken: '/auth/token',
|
|
38
|
+
authRegister: '/auth/register',
|
|
39
|
+
authMe: '/auth/me',
|
|
40
|
+
workspaces: '/workspaces',
|
|
41
|
+
postWorkspace: '/workspaces',
|
|
42
|
+
workspace: (id) => `/workspaces/${id}`,
|
|
43
|
+
templateSequences: '/template_sequences',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export default endpoints;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { default as OpenCloningDBInterface } from './OpenCloningDBInterface.js';
|
|
2
|
+
export { openCloningDBHttpClient, setUnauthorizedHandler, setWorkspaceHeader, clearWorkspaceHeader } from './common.js';
|
|
3
|
+
export { default as endpoints } from './endpoints.js';
|
|
4
|
+
export { default as SequenceSelect } from './SequenceSelect.jsx';
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { http, HttpResponse } from 'msw';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { baseUrl } from './common';
|
|
5
|
+
|
|
6
|
+
const STUB_FOLDER = `${__dirname}/../../../OpenCloning_backend/stubs/db`;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {Object} DbStub
|
|
10
|
+
* @property {unknown} body
|
|
11
|
+
* @property {string} endpoint
|
|
12
|
+
* @property {Record<string, string>} headers
|
|
13
|
+
* @property {string} method
|
|
14
|
+
* @property {unknown} params
|
|
15
|
+
* @property {{
|
|
16
|
+
* body: unknown,
|
|
17
|
+
* headers: Record<string, string>,
|
|
18
|
+
* status_code: number
|
|
19
|
+
* }} response
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {string} stubName
|
|
24
|
+
* @returns {DbStub}
|
|
25
|
+
*/
|
|
26
|
+
export function getStub(stubName) {
|
|
27
|
+
if (!fs.existsSync(`${STUB_FOLDER}/${stubName}.json`)) {
|
|
28
|
+
throw new Error(`Stub ${stubName} not found`);
|
|
29
|
+
}
|
|
30
|
+
return /** @type {DbStub} */ (
|
|
31
|
+
JSON.parse(fs.readFileSync(`${STUB_FOLDER}/${stubName}.json`, 'utf8'))
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {unknown} server
|
|
37
|
+
* @param {DbStub} stub
|
|
38
|
+
*/
|
|
39
|
+
export function addStubToServer(server, stub) {
|
|
40
|
+
const url = new URL(stub.endpoint, baseUrl);
|
|
41
|
+
server.use(
|
|
42
|
+
http[stub.method.toLowerCase()](url.toString(), async ({ request, params }) => {
|
|
43
|
+
const plainParams = Object.fromEntries(Object.entries(params ?? {}));
|
|
44
|
+
const actualParams = Object.keys(plainParams).length === 0 ? null : plainParams;
|
|
45
|
+
let actualBody;
|
|
46
|
+
if (request.headers.get('content-type')?.includes('multipart/form-data')) {
|
|
47
|
+
throw new Error('Multipart form data is not supported');
|
|
48
|
+
} else {
|
|
49
|
+
actualBody = await request
|
|
50
|
+
.clone()
|
|
51
|
+
.json()
|
|
52
|
+
.catch(() => null);
|
|
53
|
+
}
|
|
54
|
+
expect(actualParams).toEqual(stub.params);
|
|
55
|
+
expect(actualBody).toEqual(stub.body);
|
|
56
|
+
|
|
57
|
+
const actualHeaders = Object.fromEntries(request.headers.entries());
|
|
58
|
+
delete actualHeaders['content-type'];
|
|
59
|
+
delete actualHeaders['accept'];
|
|
60
|
+
expect(actualHeaders).toMatchObject(stub.headers);
|
|
61
|
+
|
|
62
|
+
if (stub.response.headers['content-disposition']) {
|
|
63
|
+
return new HttpResponse(Buffer.from(stub.response.body, 'base64'), {
|
|
64
|
+
status: stub.response.status_code,
|
|
65
|
+
headers: stub.response.headers,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return HttpResponse.json(stub.response.body, {
|
|
70
|
+
status: stub.response.status_code,
|
|
71
|
+
headers: stub.response.headers,
|
|
72
|
+
});
|
|
73
|
+
}),
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function setupToken() {
|
|
78
|
+
localStorage.setItem('token', '__TEST_TOKEN__');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function clearToken() {
|
|
82
|
+
localStorage.removeItem('token');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
describe('test utility module', () => {
|
|
86
|
+
it('exports helpers for other test files', () => {
|
|
87
|
+
expect(typeof getStub).toBe('function');
|
|
88
|
+
expect(typeof addStubToServer).toBe('function');
|
|
89
|
+
});
|
|
90
|
+
});
|