Skip to content

Commit

Permalink
Support MSW
Browse files Browse the repository at this point in the history
  • Loading branch information
Cito committed Dec 4, 2024
1 parent 7d80580 commit cabc6c9
Show file tree
Hide file tree
Showing 12 changed files with 929 additions and 132 deletions.
10 changes: 10 additions & 0 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@
"glob": "**/*",
"input": "public"
},
{
"glob": "mockServiceWorker.js",
"input": "src/mocks",
"output": "./"
},
"src/assets"
],
"styles": ["src/styles.scss"],
Expand Down Expand Up @@ -87,6 +92,11 @@
{
"glob": "**/*",
"input": "public"
},
{
"glob": "mockServiceWorker.js",
"input": "src/mocks",
"output": "./"
}
],
"styles": ["src/styles.scss"],
Expand Down
11 changes: 11 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,12 @@ export default [
from: ['features', 'service', 'models'],
allow: [['model', { context: 'auth' }]],
},
// Mock module may only import models
{
from: ['mock'],
disallow: [['*', { context: '!model' }]],
message: 'Mock modules can only import models',
},
],
},
],
Expand Down Expand Up @@ -254,6 +260,11 @@ export default [
mode: 'folder',
capture: ['context'],
},
{
type: 'mock',
mode: 'folder',
pattern: 'src/mocks',
},
],
},
},
Expand Down
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,17 @@
"karma-coverage": "~2.2.1",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"msw": "^2.6.6",
"postcss": "^8.4.49",
"prettier": "3.3.3",
"prettier-plugin-tailwindcss": "^0.6.9",
"tailwindcss": "^3.4.16",
"typescript": "~5.6.3",
"typescript-eslint": "^8.17.0"
},
"msw": {
"workerDirectory": [
"src/mocks"
]
}
}
418 changes: 290 additions & 128 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions src/app/portal/features/home-page/home-page.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@
Infrastructure initiative (NFDI) and by the contributing institutions.<br />More at
www.ghga.de.
</p>
<!-- TODO: this is just for testing MSW, remove it later -->
<pre style="white-space: pre-wrap">
{{ stats() }}
</pre>
16 changes: 14 additions & 2 deletions src/app/portal/features/home-page/home-page.component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Component, inject, signal } from '@angular/core';

/**
* This is the home page component
Expand All @@ -8,4 +9,15 @@ import { Component } from '@angular/core';
imports: [],
templateUrl: './home-page.component.html',
})
export class HomePageComponent {}
export class HomePageComponent {
// TODO: this is just for testing MSW, remove it later
#http = inject(HttpClient);
stats = signal<unknown>({});

constructor() {
// TODO: this is just for testing MSW, remove it later
this.#http.get('api/metldata/stats').subscribe((data) => {
this.stats.set(JSON.stringify(data));
});
}
}
21 changes: 19 additions & 2 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
/**
* The main module
*/

import { isDevMode } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';

/**
* Start the application
*/
async function startApp() {
if (isDevMode()) {
const { worker } = await import('./mocks/setup');
await worker.start({ waitUntilReady: true });
}
}

bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err));
startApp().then(() =>
bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err)),
);
112 changes: 112 additions & 0 deletions src/mocks/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/**
* Mock data to be used in MSW responses
*/

/** TODO: Add type annotations using the models that are also used in the application code. */

/**
* Metldata API
*/

export const metadataGlobalSummary = {
resource_stats: {
SequencingProcessFile: {
count: 532,
stats: {
format: [
{ value: 'fastq', count: 124 },
{ value: 'bam', count: 408 },
],
},
},
Individual: {
count: 5432,
stats: {
sex: [
{ value: 'Female', count: 1935 },
{ value: 'Male', count: 2358 },
],
},
},
ExperimentMethod: {
count: 1400,
stats: {
instrument_model: [
{
value: 'Ilumina_test',
count: 700,
},
{
value: 'HiSeq_test',
count: 700,
},
],
},
},
Dataset: {
count: 252,
stats: {},
},
},
};

/**
* MASS API
*/

export const searchResults = {
facets: [
{
key: 'studies.type',
name: 'Study Type',
options: [
{ value: 'Option 1', count: 62 },
{ value: 'Option 2', count: 37 },
],
},
{
key: 'type',
name: 'Dataset Type',
options: [
{ value: 'Test dataset type 1', count: 12 },
{ value: 'Test dataset type 2', count: 87 },
],
},
],
count: 25, // just to test the paginator
hits: [
{
id_: 'GHGAD588887987',
content: {
accession: 'GHGAD588887987',
ega_accession: 'EGAD588887987',
title: 'Test dataset for details',
description:
'Test dataset for Metadata Repository get dataset details call. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vel risus commodo viverra maecenas accumsan lacus vel. Sit amet risus nullam eget felis eget nunc lobortis mattis. Iaculis at erat pellentesque adipiscing commodo. Volutpat consequat mauris nunc congue. At lectus urna duis convallis convallis tellus id interdum velit. Gravida cum sociis natoque penatibus et. Mauris in aliquam sem fringilla ut morbi. Ultrices gravida dictum fusce ut. At consectetur lorem donec massa sapien faucibus et molestie.',
types: ['Test dataset type 1'],
},
},
{
id_: 'GHGAD588887988',
content: {
accession: 'GHGAD588887988',
ega_accession: 'EGAD588887988',
title: 'Test dataset for details',
description:
'Test dataset for Metadata Repository get dataset details call. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vel risus commodo viverra maecenas accumsan lacus vel. Sit amet risus nullam eget felis eget nunc lobortis mattis. Iaculis at erat pellentesque adipiscing commodo. Volutpat consequat mauris nunc congue. At lectus urna duis convallis convallis tellus id interdum velit. Gravida cum sociis natoque penatibus et. Mauris in aliquam sem fringilla ut morbi. Ultrices gravida dictum fusce ut. At consectetur lorem donec massa sapien faucibus et molestie.',
types: ['Test dataset type 1'],
},
},
{
id_: 'GHGAD588887989',
content: {
accession: 'GHGAD588887989',
ega_accession: 'EGAD588887989',
title: 'Test dataset for details',
description:
'Test dataset for Metadata Repository get dataset details call. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vel risus commodo viverra maecenas accumsan lacus vel. Sit amet risus nullam eget felis eget nunc lobortis mattis. Iaculis at erat pellentesque adipiscing commodo. Volutpat consequat mauris nunc congue. At lectus urna duis convallis convallis tellus id interdum velit. Gravida cum sociis natoque penatibus et. Mauris in aliquam sem fringilla ut morbi. Ultrices gravida dictum fusce ut. At consectetur lorem donec massa sapien faucibus et molestie.',
types: ['Test dataset type 1'],
},
},
],
};
124 changes: 124 additions & 0 deletions src/mocks/handlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/**
* Create response handlers for MSW
*
* This module takes a list of static responses for different endpoints and
* converts it into a list of response handlers that can be used to* setup MSW.
*/

import { http, HttpResponse, RequestHandler } from 'msw';
import { responses, ResponseValue } from './responses';

/**
* List of handlers for REST endpoints
*/
export const handlers: RequestHandler[] = [];

type ResponseMap = { [params: string]: ResponseValue };

const groupedResponses: { [endpoint: string]: ResponseMap } = {};

/**
* Collect responses with different query parameters for the same endpoint
*/
Object.keys(responses).forEach((endpoint) => {
let method, url, params;
[method, url] = endpoint.split(' ');
method = method.toLowerCase();
if (!/^(get|post|patch|put|delete)$/.test(method)) {
console.error('Invalid endpoint in fake data:', endpoint);
return;
}
[url, params] = url.split('?');
let bareEndpoint = `${method} ${url}`;
let responseMap = groupedResponses[bareEndpoint];
if (!responseMap) {
groupedResponses[bareEndpoint] = responseMap = {};
}
responseMap[params || '*'] = responses[endpoint];
});

/**
Find the response with the most matching parameters
@param request - the request that should be matched
@param responseMap - the map of responses to choose from
@returns the query string that matches the most parameters
*/
async function getMatchingParamString(request: Request, responseMap: ResponseMap) {
const paramStrings = Object.keys(responseMap);
if (paramStrings.length < 2) {
return paramStrings[0];
}
// combine parameters from query string and body
const requestParams = new URL(request.url).searchParams;
const method = request.method.toLowerCase();
if (/post|patch|put|delete/.test(method)) {
try {
const bodyParams = await request.json();
Object.entries(bodyParams).forEach(([key, value]) => {
const paramValue = typeof value === 'string' ? value : JSON.stringify(value);
requestParams.set(key, paramValue);
});
} catch {}
}
// find the response with the most matching parameters
let bestParamString: string | null = null;
let bestNumParams = 0;
let bestStringLen = 0;
Object.keys(responseMap).forEach((paramString) => {
const params = new URLSearchParams(paramString);
const numParams = Array.from(requestParams.keys()).reduce(
(num, param) => num + (params.get(param) === requestParams.get(param) ? 1 : 0),
0,
);
if (
bestParamString === null ||
numParams > bestNumParams ||
(numParams === bestNumParams && paramString.length < bestStringLen)
) {
bestParamString = paramString;
bestNumParams = numParams;
bestStringLen = paramString.length;
}
});
return bestParamString;
}

// create request handlers for the different endpoints
Object.keys(groupedResponses).forEach((endpoint) => {
let method, url;
[method, url] = endpoint.split(' ');
const responseMap = groupedResponses[endpoint];
/**
* Resolver for the given endpoint
* @param options - an options object containing the request
* @param options.request - the request object
* @returns - a response
*/
const resolver = async ({ request }: { request: Request }) => {
const paramString = await getMatchingParamString(request, responseMap);
let response = responseMap[paramString || '*'];
if (response === undefined) {
console.debug('Not mocking', url);
return;
}
if (Object.keys(responseMap).length > 1) {
console.debug('Using mock data for params', paramString);
}
let status = 200;
if (typeof response === 'number') {
status = response;
response = undefined;
} else if (/post/.test(method)) {
status = 201;
} else if (/patch|put|delete/.test(method)) {
status = 204;
}
return HttpResponse.json(response || undefined, { status });
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const handler = (http as any)[method];
if (!handler) {
console.error('Unsupported method:', method);
}
handlers.push(handler.call(http, url, resolver));
});
Loading

0 comments on commit cabc6c9

Please sign in to comment.