Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.
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
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
{ "language": "typescript", "autoFix": true },
{ "language": "typescriptreact", "autoFix": true }
],
"eslint.workingDirectories": ["./Composer"],
"editor.formatOnSave": true,
"typescript.tsdk": "./Composer/node_modules/typescript/lib"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import React from 'react';
import { render } from 'react-testing-library';
import { DialogWrapper } from '@app/components/DialogWrapper';
import { DialogWrapper } from '@src/components/DialogWrapper';

describe('<DialogWrapper />', () => {
const props = {
Expand Down
12 changes: 12 additions & 0 deletions Composer/packages/client/__tests__/jest.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { ActionTypes } from '@src/constants';

declare global {
namespace jest {
interface Matchers<R> {
toBeDispatchedWith(type: ActionTypes, payload?: any, error?: any);
}
}
}
41 changes: 41 additions & 0 deletions Composer/packages/client/__tests__/setupTests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import formatMessage from 'format-message';
import { setIconOptions } from 'office-ui-fabric-react/lib/Styling';
import 'jest-dom/extend-expect';
import { cleanup } from 'react-testing-library';

// Suppress icon warnings.
setIconOptions({
disableWarnings: true,
});

formatMessage.setup({
missingTranslation: 'ignore',
});

expect.extend({
toBeDispatchedWith(dispatch: jest.Mock, type: string, payload: any, error?: any) {
if (this.isNot) {
expect(dispatch).not.toHaveBeenCalledWith({
type,
payload,
error,
});
} else {
expect(dispatch).toHaveBeenCalledWith({
type,
payload,
error,
});
}

return {
pass: !this.isNot,
message: () => 'dispatch called with correct type and payload',
};
},
});

afterEach(cleanup);
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import httpClient from '@src/utils/httpUtil';
import { ActionTypes } from '@src/constants';
import { fetchFolderItemsByPath } from '@src/store/action/storage';
import { Store } from '@src/store/types';

jest.mock('@src/utils/httpUtil');

const dispatch = jest.fn();

const store = ({ dispatch, getState: () => ({}) } as unknown) as Store;

describe('fetchFolderItemsByPath', () => {
const id = 'default';
const path = '/some/path';

it('dispatches SET_STORAGEFILE_FETCHING_STATUS', async () => {
await fetchFolderItemsByPath(store, id, path);

expect(dispatch).toBeDispatchedWith(ActionTypes.SET_STORAGEFILE_FETCHING_STATUS, {
status: 'pending',
});
});

it('fetches folder items from api', async () => {
await fetchFolderItemsByPath(store, id, path);

expect(httpClient.get).toHaveBeenCalledWith(`/storages/${id}/blobs`, { params: { path } });
});

describe('when api call is successful', () => {
beforeEach(() => {
(httpClient.get as jest.Mock).mockResolvedValue({ some: 'response' });
});

it('dispatches GET_STORAGEFILE_SUCCESS', async () => {
await fetchFolderItemsByPath(store, id, path);

expect(dispatch).toBeDispatchedWith(ActionTypes.GET_STORAGEFILE_SUCCESS, {
response: { some: 'response' },
});
});
});

describe('when api call fails', () => {
beforeEach(() => {
(httpClient.get as jest.Mock).mockRejectedValue('some error');
});

it('dispatches SET_STORAGEFILE_FETCHING_STATUS', async () => {
await fetchFolderItemsByPath(store, id, path);

expect(dispatch).toBeDispatchedWith(
ActionTypes.SET_STORAGEFILE_FETCHING_STATUS,
{
status: 'failure',
},
'some error'
);
});
});
});
6 changes: 3 additions & 3 deletions Composer/packages/client/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@ module.exports = {
'office-ui-fabric-react/lib/(.*)$': 'office-ui-fabric-react/lib-commonjs/$1',
'@uifabric/fluent-theme/lib/(.*)$': '@uifabric/fluent-theme/lib-commonjs/$1',

'^@app/(.*)$': '<rootDir>/src/$1',
'^@src/(.*)$': '<rootDir>/src/$1',
},
testPathIgnorePatterns: ['/node_modules/', '/jestMocks/', '/testUtils/'],
testPathIgnorePatterns: ['/node_modules/', '/jestMocks/', '/testUtils/', '__tests__/setupTests.ts', '.*\\.d\\.ts'],
// Some node modules are packaged and distributed in a non-transpiled form
// (ex. contain import & export statements); and Jest won't be able to
// understand them because node_modules aren't transformed by default. So
// we can specify that they need to be transformed here.
transformIgnorePatterns: ['/node_modules/'],

setupFilesAfterEnv: [path.resolve(__dirname, './setupTests.js')],
setupFilesAfterEnv: [path.resolve(__dirname, './__tests__/setupTests.ts')],
globals: {
'ts-jest': {
tsConfig: path.resolve(__dirname, './tsconfig.json'),
Expand Down
2 changes: 1 addition & 1 deletion Composer/packages/client/src/store/action/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export const fetchFolderItemsByPath: ActionCreator = async ({ dispatch }, id, pa
status: 'pending',
},
});
const response = await httpClient.get(`/storages/${id}/blobs/${path}`);
const response = await httpClient.get(`/storages/${id}/blobs`, { params: { path } });
dispatch({
type: ActionTypes.GET_STORAGEFILE_SUCCESS,
payload: {
Expand Down
14 changes: 14 additions & 0 deletions Composer/packages/client/src/utils/__mocks__/httpUtil.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

/// <reference types="jest" />

const defaultResponse = { data: {} };

export default {
get: jest.fn().mockResolvedValue(defaultResponse),
post: jest.fn().mockResolvedValue(defaultResponse),
put: jest.fn().mockResolvedValue(defaultResponse),
patch: jest.fn().mockResolvedValue(defaultResponse),
delete: jest.fn().mockResolvedValue(defaultResponse),
};
5 changes: 3 additions & 2 deletions Composer/packages/client/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "build",
"allowJs": true,
"declaration": false,
"module": "esnext",
"baseUrl": ".",
"paths": {
"@app/*": ["src/*"]
"@src/*": ["src/*"]
}
},
"include": ["./src/**/*", "./__tests__/**/*"],
"include": ["./src/**/*", "./__tests__/**/*"]
}
22 changes: 11 additions & 11 deletions Composer/packages/server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ API server for composer app
## API spec

### FileSystem API
FileSystem api allows you to management multiple storages and perform file-based on top of them.
FileSystem api allows you to management multiple storages and perform file-based on top of them.


#### storage

`storage` is a top-level resource which follows the common pattern of a REST api.
`storage` is a top-level resource which follows the common pattern of a REST api.

`GET api/storages` list storages

by default return
by default return
```
{
id: "default"
Expand All @@ -39,20 +39,20 @@ by default return


#### blob
blobs is a sub-resouce of storage, but it's not refered by ID, it's refer by path, because we are building a unified file api interface, not targeting a specific clound storage (which always have id for any item).
blobs is a sub-resouce of storage, but it's not refered by ID, it's refer by path, because we are building a unified file api interface, not targeting a specific clound storage (which always have id for any item).

`GET api/storages/{storageId}/blobs/{path}` list dir or get file
`GET api/storages/{storageId}/blobs?path={path}` list dir or get file

this `path` is an absolute path for now

Sample
Sample
```
GET api/storage/default/c:/bots

{
name: "bots",
parent: "c:/",
children:
children:
{
{
name: "config",
Expand All @@ -69,7 +69,7 @@ GET api/storage/default/c:/bots
}
}

GET api/storage/default/c:/bots/a.bot
GET api/storage/default/c:/bots/a.bot

{
entry: "main.dialog"
Expand All @@ -81,12 +81,12 @@ GET api/storage/default/c:/bots/a.bot

### ProjectManagement API

ProjectManagement api allows you to controlled current project status. open\close project, get project related resources etc.
ProjectManagement api allows you to controlled current project status. open\close project, get project related resources etc.

`GET api/projects/opened`

check if there is a opened projects, return path and storage if any, resolved all files inside this project, sample response
```
```
{
storageId: "default"
path: "C:/bots/bot1.bot",
Expand Down Expand Up @@ -140,4 +140,4 @@ sample body:
name:"fire name",
steps:["Microsoft.TextPrompt","Microsoft.CallDialog","Microsoft.AdaptiveDialog"]
}
```
```
56 changes: 56 additions & 0 deletions Composer/packages/server/__tests__/controllers/storage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { Request, Response } from 'express';
import StorageService from '@src/services/storage';
import { StorageController } from '@src/controllers/storage';

jest.mock('@src/services/storage', () => ({
getBlob: jest.fn(),
}));

let mockReq: Request;
let mockRes: Response;

beforeEach(() => {
mockReq = {
params: {},
query: {},
body: {},
} as Request;

mockRes = {
status: jest.fn().mockReturnThis(),
json: jest.fn().mockReturnThis(),
} as any;
});

describe('getBlob', () => {
beforeEach(() => {
mockReq.params.storageId = 'default';
});

it('returns 400 when path query not present', async () => {
await StorageController.getBlob(mockReq, mockRes);

expect(mockRes.status).toHaveBeenCalledWith(400);
expect(mockRes.json).toHaveBeenCalledWith({ message: 'path missing from query' });
});

it('returns 400 when path is not absolute', async () => {
mockReq.query.path = 'some/path';
await StorageController.getBlob(mockReq, mockRes);

expect(mockRes.status).toHaveBeenCalledWith(400);
expect(mockRes.json).toHaveBeenCalledWith({ message: 'path must be absolute' });
});

it('returns blob for absolute path', async () => {
mockReq.query.path = '/some/path';
(StorageService.getBlob as jest.Mock).mockResolvedValue('some blob');
await StorageController.getBlob(mockReq, mockRes);

expect(mockRes.status).toHaveBeenCalledWith(200);
expect(mockRes.json).toHaveBeenCalledWith('some blob');
});
});
Loading