Skip to content

Commit

Permalink
Identity Metadata Repository setup (#2520)
Browse files Browse the repository at this point in the history
* AnchorMetadataRepository

* Improvements

* Integrate in AuthenticatedConnection

* Add tests for anchor metadata repository and AuthenticatedConnection

* Rename IdentityMetadataRepository

* CR changes

* Rename

* Change return

* await metadata

* Set metadata if empty

* Update waits for metadata and clean tests
  • Loading branch information
lmuntaner authored Jul 2, 2024
1 parent 73c4d12 commit 5a685b0
Show file tree
Hide file tree
Showing 6 changed files with 541 additions and 0 deletions.
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
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
anchorInfo = await withLoader(() => connection.getAnchorInfo());
} catch (error: unknown) {
await displayFailedToListDevices(
Expand Down
199 changes: 199 additions & 0 deletions src/frontend/src/repositories/identityMetadata.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import { MetadataMapV2 } from "$generated/internet_identity_types";
import {
IdentityMetadata,
IdentityMetadataRepository,
RECOVERY_PAGE_SHOW_TIMESTAMP_MILLIS,
} from "./identityMetadata";

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

const getterMockSuccess = vi.fn().mockResolvedValue(mockRawMetadata);
const getterMockError = vi.fn().mockImplementation(() => {
throw new Error("test error");
});
const setterMockSuccess = vi.fn();
const setterMockError = vi.fn().mockRejectedValue("test error");

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

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

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

test("getMetadata waits until metadata is loaded", async () => {
const slowGetter = vi.fn().mockImplementation(async () => {
await new Promise((resolve) => setTimeout(resolve, 1_000));
return mockRawMetadata;
});
const instance = IdentityMetadataRepository.init({
getter: slowGetter,
setter: setterMockSuccess,
});

expect(await instance.getMetadata()).toEqual(mockIdentityMetadata);
});

test("IdentityMetadataRepository returns undefined without raising an error if fetching fails", async () => {
const instance = IdentityMetadataRepository.init({
getter: getterMockError,
setter: setterMockSuccess,
});

// 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(1);
expect(console.warn).toHaveBeenCalledTimes(1);
});

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

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

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

test("IdentityMetadataRepository sets data in memory", async () => {
const noMetadata: MetadataMapV2 = [];
const instance = IdentityMetadataRepository.init({
getter: vi.fn().mockResolvedValue(noMetadata),
setter: setterMockSuccess,
});

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

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

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

const newRecoveryPageShownTimestampMillis = 9876543210;
await instance.updateMetadata({
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("IdentityMetadataRepository doesn't commit to canister without changes", async () => {
const instance = IdentityMetadataRepository.init({
getter: getterMockSuccess,
setter: setterMockSuccess,
});

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

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

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

const newRecoveryPageShownTimestampMillis = 9876543210;
const newMetadata = {
recoveryPageShownTimestampMillis: newRecoveryPageShownTimestampMillis,
};
await instance.updateMetadata(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("IdentityMetadataRepository commits additional 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().mockResolvedValue(mockMoreRawMetadata);
const instance = IdentityMetadataRepository.init({
getter: getterMock,
setter: setterMockSuccess,
});

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

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

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

0 comments on commit 5a685b0

Please sign in to comment.