mirror of
https://github.com/digital-asset/daml.git
synced 2024-09-20 09:17:43 +03:00
a0aee0f248
* Rename daml sandbox to daml sandbox-kv Also drop the default sandbox on `daml start` Part of #11831 changelog_begin changelog_end * update release test instructions * try to fix a couple compat tests * dont need special 0.0.0 logic * buildifier-fixx
383 lines
14 KiB
TypeScript
383 lines
14 KiB
TypeScript
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
// Keep in sync with compatibility/bazel_tools/create-daml-app/index.test.ts
|
|
|
|
import { ChildProcess, spawn, spawnSync, SpawnOptions } from 'child_process';
|
|
import { promises as fs } from 'fs';
|
|
import puppeteer, { Browser, Page } from 'puppeteer';
|
|
import waitOn from 'wait-on';
|
|
|
|
import Ledger from '@daml/ledger';
|
|
import { User } from '@daml.js/create-daml-app';
|
|
import { authConfig } from './config';
|
|
|
|
const JSON_API_PORT_FILE_NAME = 'json-api.port';
|
|
|
|
const UI_PORT = 3000;
|
|
|
|
// `daml start` process
|
|
let startProc: ChildProcess | undefined = undefined;
|
|
|
|
// `npm start` process
|
|
let uiProc: ChildProcess | undefined = undefined;
|
|
|
|
// Chrome browser that we run in headless mode
|
|
let browser: Browser | undefined = undefined;
|
|
|
|
// Function to generate unique party names for us.
|
|
let nextPartyId = 1;
|
|
const getParty = async () : [string, string] => {
|
|
// TODO For now we use grpcurl to allocate parties and users.
|
|
// Once the JSON API exposes party & user management we can switch to that.
|
|
const grpcurlPartyArgs = [
|
|
"-plaintext",
|
|
"localhost:6865",
|
|
"com.daml.ledger.api.v1.admin.PartyManagementService/AllocateParty",
|
|
];
|
|
const allocResult = spawnSync('grpcurl', grpcurlPartyArgs, {"encoding": "utf8"});
|
|
const parsedResult = JSON.parse(allocResult.stdout);
|
|
const user = `u${nextPartyId}`;
|
|
const party = parsedResult.partyDetails.party;
|
|
const createUser = {
|
|
"user": {
|
|
"id": user,
|
|
"primary_party": party,
|
|
},
|
|
"rights": [{"can_act_as": {"party": party}}]
|
|
};
|
|
const grpcurlUserArgs = [
|
|
"-plaintext",
|
|
"-d",
|
|
JSON.stringify(createUser),
|
|
"localhost:6865",
|
|
"com.daml.ledger.api.v1.admin.UserManagementService/CreateUser",
|
|
];
|
|
const result = spawnSync('grpcurl', grpcurlUserArgs, {"encoding": "utf8"});
|
|
nextPartyId++;
|
|
return [user, party];
|
|
};
|
|
|
|
test('Party names are unique', async () => {
|
|
const parties = new Set(await Promise.all(Array(10).fill({}).map(() => getParty())));
|
|
expect(parties.size).toEqual(10);
|
|
});
|
|
|
|
const removeFile = async (path: string) => {
|
|
try {
|
|
await fs.stat(path);
|
|
await fs.unlink(path);
|
|
} catch (_e) {
|
|
// Do nothing if the file does not exist.
|
|
}
|
|
}
|
|
|
|
// Start the Daml and UI processes before the tests begin.
|
|
// To reduce test times, we reuse the same processes between all the tests.
|
|
// This means we need to use a different set of parties and a new browser page for each test.
|
|
beforeAll(async () => {
|
|
// If the JSON API server was previously shut down abruptly then the port file
|
|
// may not have been removed.
|
|
// Since we use this file to know when the server is up, we remove it first
|
|
// (if it exists) to be sure.
|
|
const jsonApiPortFilePath = `../${JSON_API_PORT_FILE_NAME}`; // relative to ui folder
|
|
await removeFile(jsonApiPortFilePath);
|
|
|
|
// Run `daml start` from the project root (where the `daml.yaml` is located).
|
|
// The path should include '.daml/bin' in the environment where this is run,
|
|
// which contains the `daml` assistant executable.
|
|
const startOpts: SpawnOptions = { cwd: '..', stdio: 'inherit' };
|
|
|
|
// Arguments for `daml start` (besides those in the `daml.yaml`).
|
|
// The JSON API `--port-file` gives us a file we can check to know that both
|
|
// the sandbox and JSON API server are up and running.
|
|
// We use the default ports for the sandbox and JSON API as done in the
|
|
// Getting Started Guide.
|
|
const startArgs = [
|
|
'start',
|
|
'--sandbox-kv',
|
|
`--json-api-option=--port-file=${JSON_API_PORT_FILE_NAME}`,
|
|
];
|
|
|
|
console.debug("Starting daml start");
|
|
|
|
startProc = spawn('daml', startArgs, startOpts);
|
|
|
|
await waitOn({resources: [`file:${jsonApiPortFilePath}`]});
|
|
|
|
console.debug("daml start API are running");
|
|
|
|
// Run `npm start` in another shell.
|
|
// Disable automatically opening a browser using the env var described here:
|
|
// https://github.com/facebook/create-react-app/issues/873#issuecomment-266318338
|
|
const env = {...process.env, BROWSER: 'none'};
|
|
console.debug("Starting npm start");
|
|
uiProc = spawn('npm-cli.js', ['run-script', 'start'], { env, stdio: 'inherit', detached: true});
|
|
// Note(kill-npm-start): The `detached` flag starts the process in a new process group.
|
|
// This allows us to kill the process with all its descendents after the tests finish,
|
|
// following https://azimi.me/2014/12/31/kill-child_process-node-js.html.
|
|
|
|
// Ensure the UI server is ready by checking that the port is available.
|
|
await waitOn({resources: [`tcp:localhost:${UI_PORT}`]});
|
|
console.debug("npm start is running");
|
|
|
|
// Launch a single browser for all tests.
|
|
console.debug("Starting puppeteer");
|
|
browser = await puppeteer.launch();
|
|
console.debug("Puppeteer is running");
|
|
}, 60_000);
|
|
|
|
afterAll(async () => {
|
|
// Kill the `daml start` process, allowing the sandbox and JSON API server to
|
|
// shut down gracefully.
|
|
// The latter process should also remove the JSON API port file.
|
|
// TODO: Test this on Windows.
|
|
if (startProc) {
|
|
startProc.kill('SIGTERM');
|
|
}
|
|
|
|
// Kill the `npm start` process including all its descendents.
|
|
// The `-` indicates to kill all processes in the process group.
|
|
// See Note(kill-npm-start).
|
|
// TODO: Test this on Windows.
|
|
if (uiProc) {
|
|
process.kill(-uiProc.pid)
|
|
}
|
|
|
|
if (browser) {
|
|
browser.close();
|
|
}
|
|
});
|
|
|
|
test('create and look up user using ledger library', async () => {
|
|
const [user, party] = await getParty();
|
|
const token = authConfig.makeToken(user);
|
|
const ledger = new Ledger({token});
|
|
const users0 = await ledger.query(User.User);
|
|
expect(users0).toEqual([]);
|
|
const userPayload = {username: party, following: []};
|
|
const userContract1 = await ledger.create(User.User, userPayload);
|
|
const userContract2 = await ledger.fetchByKey(User.User, party);
|
|
expect(userContract1).toEqual(userContract2);
|
|
const users = await ledger.query(User.User);
|
|
expect(users[0]).toEqual(userContract1);
|
|
});
|
|
|
|
// The tests following use the headless browser to interact with the app.
|
|
// We select the relevant DOM elements using CSS class names that we embedded
|
|
// specifically for testing.
|
|
// See https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors.
|
|
|
|
const newUiPage = async (): Promise<Page> => {
|
|
if (!browser) {
|
|
throw Error('Puppeteer browser has not been launched');
|
|
}
|
|
const page = await browser.newPage();
|
|
page.on('console', message =>
|
|
console.log(`${message.type().substr(0, 3).toUpperCase()} ${message.text()}`))
|
|
await page.goto(`http://localhost:${UI_PORT}`); // ignore the Response
|
|
return page;
|
|
}
|
|
|
|
// Note that Follow is a consuming choice on a contract
|
|
// with a contract key so it is crucial to wait between follows.
|
|
// Otherwise, you get errors due to contention.
|
|
// Those can manifest in puppeteer throwing `Target closed`
|
|
// but that is not the underlying error (the JSON API will
|
|
// output the contention errors as well so look through the log).
|
|
const waitForFollowers = async (page: Page, n: number) => {
|
|
await page.waitForFunction(
|
|
(n) => document.querySelectorAll(".test-select-following").length == n,
|
|
{},
|
|
n
|
|
);
|
|
}
|
|
|
|
// LOGIN_FUNCTION_BEGIN
|
|
// Log in using a party name and wait for the main screen to load.
|
|
const login = async (page: Page, partyName: string) => {
|
|
const usernameInput = await page.waitForSelector('.test-select-username-field');
|
|
await usernameInput.click();
|
|
await usernameInput.type(partyName);
|
|
await page.click('.test-select-login-button');
|
|
await page.waitForSelector('.test-select-main-menu');
|
|
}
|
|
// LOGIN_FUNCTION_END
|
|
|
|
// Log out and wait to get back to the login screen.
|
|
const logout = async (page: Page) => {
|
|
await page.click('.test-select-log-out');
|
|
await page.waitForSelector('.test-select-login-screen');
|
|
}
|
|
|
|
// Follow a user using the text input in the follow panel.
|
|
const follow = async (page: Page, userToFollow: string) => {
|
|
await page.click('.test-select-follow-input');
|
|
await page.type('.test-select-follow-input', userToFollow);
|
|
await page.click('.test-select-follow-button');
|
|
|
|
// Wait for the request to complete, either successfully or after the error
|
|
// dialog has been handled.
|
|
// We check this by the absence of the `loading` class.
|
|
// (Both the `test-...` and `loading` classes appear in `div`s surrounding
|
|
// the `input`, due to the translation of Semantic UI's `Input` element.)
|
|
await page.waitForSelector('.test-select-follow-input > :not(.loading)', {timeout: 40_000});
|
|
}
|
|
|
|
// LOGIN_TEST_BEGIN
|
|
test('log in as a new user, log out and log back in', async () => {
|
|
const [user, party] = await getParty();
|
|
|
|
// Log in as a new user.
|
|
const page = await newUiPage();
|
|
await login(page, user);
|
|
|
|
// Check that the ledger contains the new User contract.
|
|
const token = authConfig.makeToken(user);
|
|
const ledger = new Ledger({token});
|
|
const users = await ledger.query(User.User);
|
|
expect(users).toHaveLength(1);
|
|
expect(users[0].payload.username).toEqual(party);
|
|
|
|
// Log out and in again as the same user.
|
|
await logout(page);
|
|
await login(page, user);
|
|
|
|
// Check we have the same one user.
|
|
const usersFinal = await ledger.query(User.User);
|
|
expect(usersFinal).toHaveLength(1);
|
|
expect(usersFinal[0].payload.username).toEqual(party);
|
|
|
|
await page.close();
|
|
}, 40_000);
|
|
// LOGIN_TEST_END
|
|
|
|
// This tests following users in a few different ways:
|
|
// - using the text box in the Follow panel
|
|
// - using the icon in the Network panel
|
|
// - while the user that is followed is logged in
|
|
// - while the user that is followed is logged out
|
|
// These are all successful cases.
|
|
|
|
test('log in as three different users and start following each other', async () => {
|
|
const [user1, party1] = await getParty();
|
|
const [user2, party2] = await getParty();
|
|
const [user3, party3] = await getParty();
|
|
|
|
// Log in as Party 1.
|
|
const page1 = await newUiPage();
|
|
await login(page1, user1);
|
|
|
|
// Party 1 should initially follow no one.
|
|
const noFollowing1 = await page1.$$('.test-select-following');
|
|
expect(noFollowing1).toEqual([]);
|
|
|
|
// Follow Party 2 using the text input.
|
|
// This should work even though Party 2 has not logged in yet.
|
|
// Check Party 1 follows exactly Party 2.
|
|
await follow(page1, party2);
|
|
await waitForFollowers(page1, 1);
|
|
const followingList1 = await page1.$$eval('.test-select-following', following => following.map(e => e.innerHTML));
|
|
expect(followingList1).toEqual([party2]);
|
|
|
|
// Add Party 3 as well and check both are in the list.
|
|
await follow(page1, party3);
|
|
await waitForFollowers(page1, 2);
|
|
const followingList11 = await page1.$$eval('.test-select-following', following => following.map(e => e.innerHTML));
|
|
expect(followingList11).toHaveLength(2);
|
|
expect(followingList11).toContain(party2);
|
|
expect(followingList11).toContain(party3);
|
|
|
|
// Log in as Party 2.
|
|
const page2 = await newUiPage();
|
|
await login(page2, user2);
|
|
|
|
// Party 2 should initially follow no one.
|
|
const noFollowing2 = await page2.$$('.test-select-following');
|
|
expect(noFollowing2).toEqual([]);
|
|
|
|
// However, Party 2 should see Party 1 in the network.
|
|
await page2.waitForSelector('.test-select-user-in-network');
|
|
const network2 = await page2.$$eval('.test-select-user-in-network', users => users.map(e => e.innerHTML));
|
|
expect(network2).toEqual([party1]);
|
|
|
|
// Follow Party 1 using the 'add user' icon on the right.
|
|
await page2.waitForSelector('.test-select-add-user-icon');
|
|
const userIcons = await page2.$$('.test-select-add-user-icon');
|
|
expect(userIcons).toHaveLength(1);
|
|
await userIcons[0].click();
|
|
await waitForFollowers(page2, 1);
|
|
|
|
// Also follow Party 3 using the text input.
|
|
// Note that we can also use the icon to follow Party 3 as they appear in the
|
|
// Party 1's Network panel, but that's harder to test at the
|
|
// moment because there is no loading indicator to tell when it's done.
|
|
await follow(page2, party3);
|
|
|
|
// Check the following list is updated correctly.
|
|
await waitForFollowers(page2, 2);
|
|
const followingList2 = await page2.$$eval('.test-select-following', following => following.map(e => e.innerHTML));
|
|
expect(followingList2).toHaveLength(2);
|
|
expect(followingList2).toContain(party1);
|
|
expect(followingList2).toContain(party3);
|
|
|
|
// Party 1 should now also see Party 2 in the network (but not Party 3 as they
|
|
// didn't yet started following Party 1).
|
|
await page1.waitForSelector('.test-select-user-in-network');
|
|
const network1 = await page1.$$eval('.test-select-user-in-network', following => following.map(e => e.innerHTML));
|
|
expect(network1).toEqual([party2]);
|
|
|
|
// Log in as Party 3.
|
|
const page3 = await newUiPage();
|
|
await login(page3, user3);
|
|
|
|
// Party 3 should follow no one.
|
|
const noFollowing3 = await page3.$$('.test-select-following');
|
|
expect(noFollowing3).toEqual([]);
|
|
|
|
// However, Party 3 should see both Party 1 and Party 2 in the network.
|
|
await page3.waitForSelector('.test-select-user-in-network');
|
|
const network3 = await page3.$$eval('.test-select-user-in-network', following => following.map(e => e.innerHTML));
|
|
expect(network3).toHaveLength(2);
|
|
expect(network3).toContain(party1);
|
|
expect(network3).toContain(party2);
|
|
|
|
await page1.close();
|
|
await page2.close();
|
|
await page3.close();
|
|
}, 60_000);
|
|
|
|
test('error when following self', async () => {
|
|
const [user, party] = await getParty();
|
|
const page = await newUiPage();
|
|
|
|
const dismissError = jest.fn(dialog => dialog.dismiss());
|
|
page.on('dialog', dismissError);
|
|
|
|
await login(page, user);
|
|
await follow(page, party);
|
|
|
|
expect(dismissError).toHaveBeenCalled();
|
|
|
|
await page.close();
|
|
});
|
|
|
|
test('error when adding a user that you are already following', async () => {
|
|
const [user1, party1] = await getParty();
|
|
const [user2, party2] = await getParty();
|
|
const page = await newUiPage();
|
|
|
|
const dismissError = jest.fn(dialog => dialog.dismiss());
|
|
page.on('dialog', dismissError);
|
|
|
|
await login(page, user1);
|
|
// First attempt should succeed
|
|
await follow(page, party2);
|
|
// Second attempt should result in an error
|
|
await follow(page, party2);
|
|
|
|
expect(dismissError).toHaveBeenCalled();
|
|
|
|
await page.close();
|
|
});
|