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: ability to view storages when in local dev on mac #1696

Merged
merged 10 commits into from
Dec 3, 2019
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