Skip to content
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
38 changes: 37 additions & 1 deletion apps/wallet/src/components/requests/RequestDetailView.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { describe, expect, it } from 'vitest';
import { Principal } from '@dfinity/principal';
import { describe, expect, it, vi } from 'vitest';
import { mount } from '~/test.utils';
import RequestDetailView from './RequestDetailView.vue';
import ChangeExternalCanisterOperation from './operations/ChangeExternalCanisterOperation.vue';
import { variantIs } from '~/utils/helper.utils';
import { useStationStore } from '~/stores/station.store';
import { services } from '~/plugins/services.plugin';
import { flushPromises } from '@vue/test-utils';

type RequestDetailViewProps = InstanceType<typeof RequestDetailView>['$props'];
Expand Down Expand Up @@ -250,7 +253,40 @@ const cancelledProps: RequestDetailViewProps = {
},
};

const changeCanisterProps: RequestDetailViewProps = {
details: pendingProps.details,
request: {
...pendingProps.request,
operation: {
ChangeExternalCanister: {
canister_id: Principal.fromText('rrkah-fqaaa-aaaaa-aaaaq-cai'),
mode: {
upgrade: [{ wasm_memory_persistence: [{ keep: null }], skip_pre_upgrade: [] }],
},
module_checksum: 'a'.repeat(64),
arg_checksum: [],
},
},
},
};

describe('RequestDetailView', () => {
it('renders change canister requests with their upgrade options', async () => {
vi.spyOn(services().station, 'getExternalCanisterByCanisterId').mockRejectedValueOnce(
new Error('not found'),
);

const wrapper = mount(RequestDetailView, {
props: changeCanisterProps,
});
await flushPromises();

expect(wrapper.findComponent(ChangeExternalCanisterOperation).exists()).toBe(true);
expect(wrapper.find('[data-test-id="change-canister-wasm-memory-persistence"]').text()).toBe(
'Keep',
);
});

it('renders properly', () => {
const wrapper = mount(RequestDetailView, {
props: pendingProps,
Expand Down
3 changes: 2 additions & 1 deletion apps/wallet/src/components/requests/RequestDetailView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ import AddRequestPolicyOperation from './operations/AddRequestPolicyOperation.vu
import AddUserGroupOperation from './operations/AddUserGroupOperation.vue';
import AddUserOperation from './operations/AddUserOperation.vue';
import CallExternalCanisterOperation from './operations/CallExternalCanisterOperation.vue';
import ChangeExternalCanisterOperation from './operations/ChangeExternalCanisterOperation.vue';
import EditAccountOperation from './operations/EditAccountOperation.vue';
import EditAddressBookEntryOperation from './operations/EditAddressBookEntryOperation.vue';
import EditPermissionOperation from './operations/EditPermissionOperation.vue';
Expand Down Expand Up @@ -318,12 +319,12 @@ const componentsMap: {
EditAsset: EditAssetOperation,
RemoveAsset: RemoveAssetOperation,
CallExternalCanister: CallExternalCanisterOperation,
ChangeExternalCanister: ChangeExternalCanisterOperation,
AddNamedRule: AddNamedRuleOperation,
EditNamedRule: EditNamedRuleOperation,
RemoveNamedRule: RemoveNamedRuleOperation,
SetDisasterRecovery: SetDisasterRecoveryOperation,

ChangeExternalCanister: UnsupportedOperation,
CreateExternalCanister: UnsupportedOperation,
ConfigureExternalCanister: UnsupportedOperation,
FundExternalCanister: UnsupportedOperation,
Expand Down
3 changes: 2 additions & 1 deletion apps/wallet/src/components/requests/RequestListItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import AddRequestPolicyOperation from './operations/AddRequestPolicyOperation.vu
import AddUserGroupOperation from './operations/AddUserGroupOperation.vue';
import AddUserOperation from './operations/AddUserOperation.vue';
import CallExternalCanisterOperation from './operations/CallExternalCanisterOperation.vue';
import ChangeExternalCanisterOperation from './operations/ChangeExternalCanisterOperation.vue';
import EditAccountOperation from './operations/EditAccountOperation.vue';
import EditAddressBookEntryOperation from './operations/EditAddressBookEntryOperation.vue';
import EditPermissionOperation from './operations/EditPermissionOperation.vue';
Expand Down Expand Up @@ -108,6 +109,7 @@ const componentsMap: {
EditPermission: EditPermissionOperation,
ManageSystemInfo: ManageSystemInfoOperation,
CallExternalCanister: CallExternalCanisterOperation,
ChangeExternalCanister: ChangeExternalCanisterOperation,
AddAsset: AddAssetOperation,
EditAsset: EditAssetOperation,
RemoveAsset: RemoveAssetOperation,
Expand All @@ -117,7 +119,6 @@ const componentsMap: {
SetDisasterRecovery: SetDisasterRecoveryOperation,

// below variants are not supported yet
ChangeExternalCanister: UnsupportedOperation,
CreateExternalCanister: UnsupportedOperation,
ConfigureExternalCanister: UnsupportedOperation,
FundExternalCanister: UnsupportedOperation,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import { Principal } from '@dfinity/principal';
import { flushPromises } from '@vue/test-utils';
import { afterEach, describe, expect, it, vi } from 'vitest';
import {
CanisterInstallMode,
ChangeExternalCanisterOperation as ChangeExternalCanisterOperationDTO,
Request,
} from '~/generated/station/station.did';
import { services } from '~/plugins/services.plugin';
import { mount } from '~/test.utils';
import ChangeExternalCanisterOperation from './ChangeExternalCanisterOperation.vue';

type ExternalCanisterLookup = Awaited<
ReturnType<ReturnType<typeof services>['station']['getExternalCanisterByCanisterId']>
>;

const canisterId = Principal.fromText('rrkah-fqaaa-aaaaa-aaaaq-cai');
const moduleChecksum = 'a'.repeat(64);
const argChecksum = 'b'.repeat(64);

const operationWithMode = (mode: CanisterInstallMode): ChangeExternalCanisterOperationDTO => ({
mode,
canister_id: canisterId,
module_checksum: moduleChecksum,
arg_checksum: [argChecksum],
});

const requestWith = (operation: ChangeExternalCanisterOperationDTO): Request => ({
id: 'request-id',
title: 'Upgrade canister',
summary: [],
status: { Created: null },
approvals: [],
created_at: '',
execution_plan: { Immediate: null },
expiration_dt: '',
requested_by: 'requester-id',
tags: [],
deduplication_key: [],
operation: { ChangeExternalCanister: operation },
});

const mountOperation = (operation: ChangeExternalCanisterOperationDTO, mode: 'list' | 'detail') =>
mount(ChangeExternalCanisterOperation, {
props: {
request: requestWith(operation),
operation,
mode,
},
});

const keepUpgrade: CanisterInstallMode = {
upgrade: [{ wasm_memory_persistence: [{ keep: null }], skip_pre_upgrade: [] }],
};

describe('ChangeExternalCanisterOperation', () => {
afterEach(() => {
vi.restoreAllMocks();
});

it('shows the canister id, install mode and checksums', () => {
const wrapper = mountOperation(operationWithMode({ install: null }), 'list');

expect(wrapper.find('[data-test-id="change-canister-target"]').text()).toBe(
canisterId.toText(),
);
expect(wrapper.find('[data-test-id="change-canister-mode"]').text()).toBe('Install');
expect(wrapper.find('[data-test-id="change-canister-module-checksum"]').exists()).toBe(true);
expect(wrapper.find('[data-test-id="change-canister-arg-checksum"]').exists()).toBe(true);
});

it('shows the full checksums in detail mode', async () => {
vi.spyOn(services().station, 'getExternalCanisterByCanisterId').mockRejectedValueOnce(
new Error('not found'),
);

const wrapper = mountOperation(operationWithMode({ reinstall: null }), 'detail');
await flushPromises();

expect(wrapper.find('[data-test-id="change-canister-mode"]').text()).toBe('Reinstall');
expect(wrapper.find('[data-test-id="change-canister-module-checksum"]').text()).toBe(
moduleChecksum,
);
expect(wrapper.find('[data-test-id="change-canister-arg-checksum"]').text()).toBe(argChecksum);
});

it('omits the argument checksum row when the request has no argument', () => {
const wrapper = mountOperation(
{ ...operationWithMode({ install: null }), arg_checksum: [] },
'list',
);

expect(wrapper.find('[data-test-id="change-canister-arg-checksum"]').exists()).toBe(false);
});

it('shows the wasm memory persistence of an upgrade in list mode when it is set', () => {
const wrapper = mountOperation(operationWithMode(keepUpgrade), 'list');

expect(wrapper.find('[data-test-id="change-canister-mode"]').text()).toBe('Upgrade');
expect(wrapper.find('[data-test-id="change-canister-wasm-memory-persistence"]').text()).toBe(
'Keep',
);
expect(wrapper.text()).toContain('Wasm Memory Persistence');
});

it('shows the skip pre-upgrade flag of an upgrade when it is set', () => {
const wrapper = mountOperation(
operationWithMode({
upgrade: [{ wasm_memory_persistence: [{ replace: null }], skip_pre_upgrade: [true] }],
}),
'list',
);

expect(wrapper.find('[data-test-id="change-canister-wasm-memory-persistence"]').text()).toBe(
'Replace',
);
expect(wrapper.find('[data-test-id="change-canister-skip-pre-upgrade"]').text()).toBe('Yes');
});

it('shows an explicitly disabled skip pre-upgrade flag in list mode', () => {
const wrapper = mountOperation(
operationWithMode({
upgrade: [{ wasm_memory_persistence: [], skip_pre_upgrade: [false] }],
}),
'list',
);

expect(wrapper.find('[data-test-id="change-canister-skip-pre-upgrade"]').text()).toBe('No');
expect(wrapper.find('[data-test-id="change-canister-wasm-memory-persistence"]').exists()).toBe(
false,
);
});

it('hides the upgrade options in list mode when the upgrade does not set them', () => {
const wrapper = mountOperation(operationWithMode({ upgrade: [] }), 'list');

expect(wrapper.find('[data-test-id="change-canister-wasm-memory-persistence"]').exists()).toBe(
false,
);
expect(wrapper.find('[data-test-id="change-canister-skip-pre-upgrade"]').exists()).toBe(false);
});

it('shows the effective upgrade options in detail mode for a plain upgrade', async () => {
vi.spyOn(services().station, 'getExternalCanisterByCanisterId').mockRejectedValueOnce(
new Error('not found'),
);

const wrapper = mountOperation(operationWithMode({ upgrade: [] }), 'detail');
await flushPromises();

expect(wrapper.find('[data-test-id="change-canister-wasm-memory-persistence"]').text()).toBe(
'Default (replace)',
);
expect(wrapper.find('[data-test-id="change-canister-skip-pre-upgrade"]').text()).toBe('No');
});

it('does not show upgrade options for install and reinstall requests', async () => {
vi.spyOn(services().station, 'getExternalCanisterByCanisterId').mockRejectedValue(
new Error('not found'),
);

for (const mode of [{ install: null }, { reinstall: null }] as CanisterInstallMode[]) {
const wrapper = mountOperation(operationWithMode(mode), 'detail');
await flushPromises();

expect(
wrapper.find('[data-test-id="change-canister-wasm-memory-persistence"]').exists(),
).toBe(false);
expect(wrapper.find('[data-test-id="change-canister-skip-pre-upgrade"]').exists()).toBe(
false,
);
}
});

it('resolves the canister name in detail mode', async () => {
const lookup = vi
.spyOn(services().station, 'getExternalCanisterByCanisterId')
.mockResolvedValueOnce({ canister: { name: 'Backend' } } as ExternalCanisterLookup);

const wrapper = mountOperation(operationWithMode(keepUpgrade), 'detail');
await flushPromises();

// The name is resolved with a verified call, like the request itself.
expect(lookup).toHaveBeenCalledWith(canisterId, true);
expect(wrapper.find('[data-test-id="change-canister-target"]').text()).toBe(
`Backend (${canisterId.toText()})`,
);
});

it('falls back to the canister id when the canister cannot be resolved', async () => {
vi.spyOn(services().station, 'getExternalCanisterByCanisterId').mockRejectedValueOnce(
new Error('not found'),
);

const wrapper = mountOperation(operationWithMode(keepUpgrade), 'detail');
await flushPromises();

expect(wrapper.find('[data-test-id="change-canister-target"]').text()).toBe(
canisterId.toText(),
);
});

it('does not look up the canister in list mode', async () => {
const lookup = vi.spyOn(services().station, 'getExternalCanisterByCanisterId');

mountOperation(operationWithMode(keepUpgrade), 'list');
await flushPromises();

expect(lookup).not.toHaveBeenCalled();
});
});
Loading
Loading