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

Identity Metadata Repository setup #2520

Merged
merged 11 commits into from
Jul 2, 2024
1 change: 1 addition & 0 deletions src/frontend/src/flows/authorize/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ const authenticate = async (
const derivationOrigin =
authContext.authRequest.derivationOrigin ?? authContext.requestOrigin;

// TODO: Commit state
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be done in another PR, but I want to remember exactly where to put it.

const result = await withLoader(() =>
fetchDelegation({
connection: authSuccess.connection,
Expand Down
1 change: 1 addition & 0 deletions src/frontend/src/flows/manage/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ export const renderManage = async ({
for (;;) {
let anchorInfo: IdentityAnchorInfo;
try {
// TODO: commit state
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be done in another PR, but I want to remember exactly where to put it.

anchorInfo = await withLoader(() => connection.getAnchorInfo());
} catch (error: unknown) {
await displayFailedToListDevices(
Expand Down
223 changes: 223 additions & 0 deletions src/frontend/src/repositories/anchorMetadata.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import { MetadataMapV2 } from "$generated/internet_identity_types";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be good to have a test to ensure that the repository leaves additional data unchanged in rawMetadata.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a test for this: "IdentityMetadataRepository commits additional metadata to canister after update".

I rephrased the description to use "additional", maybe that was the confusing part.

import {
AnchorMetadata,
AnchorMetadataRepository,
RECOVERY_PAGE_SHOW_TIMESTAMP_MILLIS,
} from "./anchorMetadata";

const recoveryPageShownTimestampMillis = 1234567890;
const mockRawMetadata: MetadataMapV2 = [
[
RECOVERY_PAGE_SHOW_TIMESTAMP_MILLIS,
{ String: String(recoveryPageShownTimestampMillis) },
],
];
const mockAnchorMetadata: AnchorMetadata = {
recoveryPageShownTimestampMillis,
};

// Used to await that the getter has resolved.
let getterResponse: MetadataMapV2 | null | undefined = null;
const getterMockSuccess = vi.fn().mockImplementation(async () => {
// The `await` is necessary to make sure that the `getterResponse` is set before the test continues.
getterResponse = await mockRawMetadata;
return mockRawMetadata;
});
const getterMockError = vi.fn().mockImplementation(async () => {
// The `await` is necessary to make sure that the `getterResponse` is set before the test continues.
getterResponse = await null;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I'm missing some context here. Why does the await change anything at all? Shouldn't that instantly resolve anyway?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some reason it wasn't waiting before. But I tried again and it was working now. Removed.

throw new Error("test error");
});
const setterMockSuccess = vi.fn();
const setterMockError = vi.fn().mockRejectedValue("test error");

beforeEach(() => {
getterResponse = undefined;
vi.clearAllMocks();
vi.spyOn(console, "warn").mockImplementation(() => {});
});

test("AnchorMetadataRepository loads data on init in the background", async () => {
const instance = AnchorMetadataRepository.init({
getter: getterMockSuccess,
setter: setterMockSuccess,
});

await vi.waitFor(() => expect(getterResponse).toEqual(mockRawMetadata));

expect(getterMockSuccess).toHaveBeenCalledTimes(1);
expect(await instance.getMetadata()).toEqual(mockAnchorMetadata);
});

test("AnchorMetadataRepository loads data again on init in the background", async () => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment below, but I think the metadata should not be loaded again.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

const getterMockErrorOnce = vi
.fn()
.mockImplementationOnce(async () => {
// The `await` is necessary to make sure that the `getterResponse` is set before the test continues.
getterResponse = await null;
throw new Error("test error");
})
.mockResolvedValue(mockRawMetadata);

const instance = AnchorMetadataRepository.init({
getter: getterMockErrorOnce,
setter: setterMockSuccess,
});

// Wait for the first getter to fail.
await vi.waitFor(() => expect(getterResponse).toEqual(null));

// Error is not thrown, but a warning is logged.
expect(console.warn).toHaveBeenCalledTimes(1);
expect(getterMockErrorOnce).toHaveBeenCalledTimes(1);

expect(await instance.getMetadata()).toEqual(mockAnchorMetadata);
expect(getterMockErrorOnce).toHaveBeenCalledTimes(2);
});

test("AnchorMetadataRepository returns no metadata if fetching fails without raising an error", async () => {
lmuntaner marked this conversation as resolved.
Show resolved Hide resolved
const instance = AnchorMetadataRepository.init({
getter: getterMockError,
setter: setterMockSuccess,
});

// Wait for the first getter to fail.
await vi.waitFor(() => expect(getterResponse).toEqual(null));

// Error is not thrown, but warnings is logged.
expect(console.warn).toHaveBeenCalledTimes(1);
expect(getterMockError).toHaveBeenCalledTimes(1);

expect(await instance.getMetadata()).toEqual(undefined);
expect(getterMockError).toHaveBeenCalledTimes(2);
expect(console.warn).toHaveBeenCalledTimes(2);
});

test("AnchorMetadataRepository changes data in memory", async () => {
const instance = AnchorMetadataRepository.init({
getter: getterMockSuccess,
setter: setterMockSuccess,
});

await vi.waitFor(() => expect(getterResponse).toEqual(mockRawMetadata));

const newRecoveryPageShownTimestampMillis = 9876543210;
await instance.setPartialMetadata({
recoveryPageShownTimestampMillis: newRecoveryPageShownTimestampMillis,
});

expect(await instance.getMetadata()).toEqual({
recoveryPageShownTimestampMillis: newRecoveryPageShownTimestampMillis,
});
});

test("AnchorMetadataRepository commits updated metadata to canister", async () => {
const instance = AnchorMetadataRepository.init({
getter: getterMockSuccess,
setter: setterMockSuccess,
});

await vi.waitFor(() => expect(getterResponse).toEqual(mockRawMetadata));

const newRecoveryPageShownTimestampMillis = 9876543210;
await instance.setPartialMetadata({
recoveryPageShownTimestampMillis: newRecoveryPageShownTimestampMillis,
});

expect(setterMockSuccess).not.toHaveBeenCalled();
await instance.commitMetadata();

expect(setterMockSuccess).toHaveBeenCalledTimes(1);
expect(setterMockSuccess).toHaveBeenCalledWith([
[
RECOVERY_PAGE_SHOW_TIMESTAMP_MILLIS,
{ String: String(newRecoveryPageShownTimestampMillis) },
],
]);
});

test("AnchorMetadataRepository doesn't commit to canister without changes", async () => {
const instance = AnchorMetadataRepository.init({
getter: getterMockSuccess,
setter: setterMockSuccess,
});

await vi.waitFor(() => expect(getterResponse).toEqual(mockRawMetadata));

expect(setterMockSuccess).not.toHaveBeenCalled();
await instance.commitMetadata();

expect(setterMockSuccess).not.toHaveBeenCalled();
});

test("AnchorMetadataRepository doesn't raise an error if committing fails", async () => {
const instance = AnchorMetadataRepository.init({
getter: getterMockSuccess,
setter: setterMockError,
});

await vi.waitFor(() => expect(getterResponse).toEqual(mockRawMetadata));

const newRecoveryPageShownTimestampMillis = 9876543210;
const newMetadata = {
recoveryPageShownTimestampMillis: newRecoveryPageShownTimestampMillis,
};
await instance.setPartialMetadata(newMetadata);

expect(setterMockError).not.toHaveBeenCalled();
const committed = await instance.commitMetadata();

expect(committed).toBe(false);
expect(setterMockError).toHaveBeenCalledTimes(1);
expect(setterMockError).toHaveBeenCalledWith([
[
RECOVERY_PAGE_SHOW_TIMESTAMP_MILLIS,
{ String: String(newRecoveryPageShownTimestampMillis) },
],
]);

// But the value in memory is not lost.
expect(await instance.getMetadata()).toEqual(newMetadata);
});

test("AnchorMetadataRepository commits all received metadata to canister after update", async () => {
const anotherMetadataEntry: [string, { String: string }] = [
"otherKey",
{ String: "otherValue" },
];
const mockMoreRawMetadata: MetadataMapV2 = [
[
RECOVERY_PAGE_SHOW_TIMESTAMP_MILLIS,
{ String: String(recoveryPageShownTimestampMillis) },
],
anotherMetadataEntry,
];
const getterMock = vi.fn().mockImplementation(async () => {
// The `await` is necessary to make sure that the `getterResponse` is set before the test continues.
getterResponse = await mockMoreRawMetadata;
return mockMoreRawMetadata;
});
const instance = AnchorMetadataRepository.init({
getter: getterMock,
setter: setterMockSuccess,
});

await vi.waitFor(() => expect(getterResponse).toEqual(mockMoreRawMetadata));

const newRecoveryPageShownTimestampMillis = 9876543210;
await instance.setPartialMetadata({
recoveryPageShownTimestampMillis: newRecoveryPageShownTimestampMillis,
});

expect(setterMockSuccess).not.toHaveBeenCalled();
await instance.commitMetadata();

expect(setterMockSuccess).toHaveBeenCalledTimes(1);
expect(setterMockSuccess).toHaveBeenCalledWith([
[
RECOVERY_PAGE_SHOW_TIMESTAMP_MILLIS,
{ String: String(newRecoveryPageShownTimestampMillis) },
],
anotherMetadataEntry,
]);
});
Loading
Loading