Skip to content

Commit

Permalink
Rename IdentityMetadataRepository
Browse files Browse the repository at this point in the history
  • Loading branch information
lmuntaner committed Jul 1, 2024
1 parent 0a230a0 commit 270c219
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 108 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { MetadataMapV2 } from "$generated/internet_identity_types";
import {
AnchorMetadata,
AnchorMetadataRepository,
IdentityMetadata,
IdentityMetadataRepository,
RECOVERY_PAGE_SHOW_TIMESTAMP_MILLIS,
} from "./anchorMetadata";
} from "./identityMetadata";

const recoveryPageShownTimestampMillis = 1234567890;
const mockRawMetadata: MetadataMapV2 = [
Expand All @@ -12,7 +12,7 @@ const mockRawMetadata: MetadataMapV2 = [
{ String: String(recoveryPageShownTimestampMillis) },
],
];
const mockAnchorMetadata: AnchorMetadata = {
const mockIdentityMetadata: IdentityMetadata = {
recoveryPageShownTimestampMillis,
};

Expand All @@ -37,46 +37,20 @@ beforeEach(() => {
vi.spyOn(console, "warn").mockImplementation(() => {});
});

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

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

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

test("AnchorMetadataRepository loads data again on init in the background", async () => {
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 () => {
const instance = AnchorMetadataRepository.init({
test("IdentityMetadataRepository returns no metadata without raising an error if fetching fails", async () => {
const instance = IdentityMetadataRepository.init({
getter: getterMockError,
setter: setterMockSuccess,
});
Expand All @@ -88,13 +62,13 @@ test("AnchorMetadataRepository returns no metadata if fetching fails without rai
expect(console.warn).toHaveBeenCalledTimes(1);
expect(getterMockError).toHaveBeenCalledTimes(1);

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

test("AnchorMetadataRepository changes data in memory", async () => {
const instance = AnchorMetadataRepository.init({
test("IdentityMetadataRepository changes data in memory", async () => {
const instance = IdentityMetadataRepository.init({
getter: getterMockSuccess,
setter: setterMockSuccess,
});
Expand All @@ -106,13 +80,13 @@ test("AnchorMetadataRepository changes data in memory", async () => {
recoveryPageShownTimestampMillis: newRecoveryPageShownTimestampMillis,
});

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

test("AnchorMetadataRepository commits updated metadata to canister", async () => {
const instance = AnchorMetadataRepository.init({
test.only("IdentityMetadataRepository commits updated metadata to canister", async () => {
const instance = IdentityMetadataRepository.init({
getter: getterMockSuccess,
setter: setterMockSuccess,
});
Expand All @@ -136,8 +110,8 @@ test("AnchorMetadataRepository commits updated metadata to canister", async () =
]);
});

test("AnchorMetadataRepository doesn't commit to canister without changes", async () => {
const instance = AnchorMetadataRepository.init({
test("IdentityMetadataRepository doesn't commit to canister without changes", async () => {
const instance = IdentityMetadataRepository.init({
getter: getterMockSuccess,
setter: setterMockSuccess,
});
Expand All @@ -150,8 +124,8 @@ test("AnchorMetadataRepository doesn't commit to canister without changes", asyn
expect(setterMockSuccess).not.toHaveBeenCalled();
});

test("AnchorMetadataRepository doesn't raise an error if committing fails", async () => {
const instance = AnchorMetadataRepository.init({
test("IdentityMetadataRepository doesn't raise an error if committing fails", async () => {
const instance = IdentityMetadataRepository.init({
getter: getterMockSuccess,
setter: setterMockError,
});
Expand All @@ -177,10 +151,10 @@ test("AnchorMetadataRepository doesn't raise an error if committing fails", asyn
]);

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

test("AnchorMetadataRepository commits all received metadata to canister after update", async () => {
test("IdentityMetadataRepository commits all received metadata to canister after update", async () => {
const anotherMetadataEntry: [string, { String: string }] = [
"otherKey",
{ String: "otherValue" },
Expand All @@ -197,7 +171,7 @@ test("AnchorMetadataRepository commits all received metadata to canister after u
getterResponse = await mockMoreRawMetadata;
return mockMoreRawMetadata;
});
const instance = AnchorMetadataRepository.init({
const instance = IdentityMetadataRepository.init({
getter: getterMock,
setter: setterMockSuccess,
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { MetadataMapV2 } from "$generated/internet_identity_types";

export type AnchorMetadata = {
export type IdentityMetadata = {
recoveryPageShownTimestampMillis?: number;
};

export const RECOVERY_PAGE_SHOW_TIMESTAMP_MILLIS =
"recoveryPageShownTimestampMillis";

const convertMetadata = (rawMetadata: MetadataMapV2): AnchorMetadata => {
const convertMetadata = (rawMetadata: MetadataMapV2): IdentityMetadata => {
const recoveryPageEntry = rawMetadata.find(
([key]) => key === RECOVERY_PAGE_SHOW_TIMESTAMP_MILLIS
);
Expand All @@ -32,7 +32,7 @@ type MetadataSetter = (metadata: MetadataMapV2) => Promise<void>;
type RawMetadataState = MetadataMapV2 | "loading" | "error" | "not-loaded";

/**
* Class to manage the metadata of the anchor and interact with the canister.
* Class to manage the metadata of the identity and interact with the canister.
*
* We decided to not throw any errors because this is non-critical for the application
* and we don't want to disrupt user flows if there is an error with the metadata.
Expand All @@ -41,14 +41,14 @@ type RawMetadataState = MetadataMapV2 | "loading" | "error" | "not-loaded";
* It can then be read and updated in memory.
* The metadata needs to be committed to the canister with the `commitMetadata` method to persist the changes.
*/
export class AnchorMetadataRepository {
// The nice AnchorMetadata is exposed to the outside world, while the raw metadata is kept private.
export class IdentityMetadataRepository {
// The nice IdentityMetadata is exposed to the outside world, while the raw metadata is kept private.
// We keep all the raw data to maintain other metadata fields.
private rawMetadata: RawMetadataState;
// Flag to keep track whether we need to commit the metadata to the canister.
private updatedMetadata: boolean;
private getter: MetadataGetter;
private setter: MetadataSetter;
private readonly getter: MetadataGetter;
private readonly setter: MetadataSetter;

static init = ({
getter,
Expand All @@ -57,7 +57,7 @@ export class AnchorMetadataRepository {
getter: MetadataGetter;
setter: MetadataSetter;
}) => {
const instance = new AnchorMetadataRepository({ getter, setter });
const instance = new IdentityMetadataRepository({ getter, setter });
// Load the metadata in the background.
void instance.loadMetadata();
return instance;
Expand Down Expand Up @@ -90,7 +90,7 @@ export class AnchorMetadataRepository {
this.updatedMetadata = false;
this.rawMetadata = await this.getter();
} catch (error) {
// Do not throw the error becasue this is not critical for the application.
// Do not throw the error because this is not critical for the application.
this.rawMetadata = "error";
console.warn("Error loading metadata", error);
return;
Expand All @@ -108,18 +108,13 @@ export class AnchorMetadataRepository {
};

/**
* Returns the metadata transformed to `AnchorMetadata`.
*
* It tries to load the metadata if it's not loaded yet.
* Returns the metadata transformed to `IdentityMetadata`.
*
* It returns `undefined` if the metadata is not loaded.
*
* @returns {AnchorMetadata | undefined}
* @returns {IdentityMetadata | undefined}
*/
getMetadata = async (): Promise<AnchorMetadata | undefined> => {
if (!this.metadataIsLoaded(this.rawMetadata)) {
await this.loadMetadata();
}
getMetadata = (): IdentityMetadata | undefined => {
if (this.metadataIsLoaded(this.rawMetadata)) {
return convertMetadata(this.rawMetadata);
}
Expand All @@ -129,25 +124,17 @@ export class AnchorMetadataRepository {
/**
* Changes the metadata in memory but doesn't commit it to the canister.
*
* At the moment, this functoin only supports changing the `recoveryPageShownTimestampMillis` field.
* At the moment, this function only supports changing the `recoveryPageShownTimestampMillis` field.
*
* The metadata passed will be merged with the existing metadata. Same keys will be overwritten.
*
* If the metadata is not loaded yet, it will try to load it.
* If loading the metadata fails, it will console a warning but won't throw an error.
*
* We consider the metadata to not be crucial for the application.
* Therefore, we don't want to disrupt user flows if there is an error with the metadata.
*
* @param {Partial<AnchorMetadata>} partialMetadata
* @param {Partial<IdentityMetadata>} partialMetadata
* @returns {Promise<void>} To indicate that the metadata has been set.
*/
setPartialMetadata = async (
partialMetadata: Partial<AnchorMetadata>
): Promise<void> => {
if (!this.metadataIsLoaded(this.rawMetadata)) {
await this.getMetadata();
}
setPartialMetadata = (partialMetadata: Partial<IdentityMetadata>): void => {
if (this.metadataIsLoaded(this.rawMetadata)) {
let updatedMetadata: MetadataMapV2 = [...this.rawMetadata];
this.updatedMetadata = true;
Expand Down
33 changes: 10 additions & 23 deletions src/frontend/src/utils/iiConnection.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { MetadataMapV2, _SERVICE } from "$generated/internet_identity_types";
import {
AnchorMetadata,
IdentityMetadata,
RECOVERY_PAGE_SHOW_TIMESTAMP_MILLIS,
} from "$src/repositories/anchorMetadata";
} from "$src/repositories/identityMetadata";
import { ActorSubclass } from "@dfinity/agent";
import { DelegationIdentity } from "@dfinity/identity";
import { AuthenticatedConnection } from "./iiConnection";
Expand All @@ -23,19 +23,14 @@ const mockRawMetadata: MetadataMapV2 = [
{ String: String(recoveryPageShownTimestampMillis) },
],
];
const mockAnchorMetadata: AnchorMetadata = {
const mockIdentityMetadata: IdentityMetadata = {
recoveryPageShownTimestampMillis,
};
const expectedAnchorInfo = {
devices: [],
device_registration: [],
};

// Used to await that the getter has resolved.
let infoResponse: MetadataMapV2 | null | undefined = null;

const mockActor = {
get_anchor_info: vi.fn().mockResolvedValue(expectedAnchorInfo),
identity_info: vi.fn().mockImplementation(async () => {
// The `await` is necessary to make sure that the `getterResponse` is set before the test continues.
infoResponse = await mockRawMetadata;
Expand All @@ -44,20 +39,12 @@ const mockActor = {
identity_metadata_replace: vi.fn().mockResolvedValue({ Ok: null }),
} as unknown as ActorSubclass<_SERVICE>;

test("gets anchor info from actor", async () => {
const connection = new AuthenticatedConnection(
"12345",
MultiWebAuthnIdentity.fromCredentials([]),
mockDelegationIdentity,
BigInt(1234),
mockActor
);
const anchorInfo = await connection.getAnchorInfo();
expect(anchorInfo).toEqual(expectedAnchorInfo);
expect(mockActor.get_anchor_info).toHaveBeenCalledTimes(1);
beforeEach(() => {
infoResponse = undefined;
vi.clearAllMocks();
});

test("initializes anchor metadata repository", async () => {
test("initializes identity metadata repository", async () => {
const connection = new AuthenticatedConnection(
"12345",
MultiWebAuthnIdentity.fromCredentials([]),
Expand All @@ -68,10 +55,10 @@ test("initializes anchor metadata repository", async () => {

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

expect(await connection.getAnchorMetadata()).toEqual(mockAnchorMetadata);
expect(await connection.getIdentityMetadata()).toEqual(mockIdentityMetadata);
});

test("comits changes on anchor metadata", async () => {
test("commits changes on identity metadata", async () => {
const userNumber = BigInt(1234);
const connection = new AuthenticatedConnection(
"12345",
Expand All @@ -83,7 +70,7 @@ test("comits changes on anchor metadata", async () => {

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

expect(await connection.getAnchorMetadata()).toEqual(mockAnchorMetadata);
expect(connection.getIdentityMetadata()).toEqual(mockIdentityMetadata);

const newRecoveryPageShownTimestampMillis = 9876543210;
await connection.setPartialMetadata({
Expand Down
14 changes: 7 additions & 7 deletions src/frontend/src/utils/iiConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ import {
import { fromMnemonicWithoutValidation } from "$src/crypto/ed25519";
import { features } from "$src/features";
import {
AnchorMetadata,
AnchorMetadataRepository,
} from "$src/repositories/anchorMetadata";
IdentityMetadata,
IdentityMetadataRepository,
} from "$src/repositories/identityMetadata";
import { diagnosticInfo, unknownToString } from "$src/utils/utils";
import {
Actor,
Expand Down Expand Up @@ -419,7 +419,7 @@ export class Connection {
}

export class AuthenticatedConnection extends Connection {
private metadataRepository: AnchorMetadataRepository;
private metadataRepository: IdentityMetadataRepository;
public constructor(
public canisterId: string,
public identity: SignIdentity,
Expand All @@ -442,7 +442,7 @@ export class AuthenticatedConnection extends Connection {
}
throw new Error("Error updating metadata");
};
this.metadataRepository = AnchorMetadataRepository.init({
this.metadataRepository = IdentityMetadataRepository.init({
getter: metadataGetter,
setter: metadataSetter,
});
Expand Down Expand Up @@ -552,12 +552,12 @@ export class AuthenticatedConnection extends Connection {
return await actor.identity_metadata_replace(this.userNumber, metadata);
};

getAnchorMetadata = (): Promise<AnchorMetadata | undefined> => {
getIdentityMetadata = (): IdentityMetadata | undefined => {
return this.metadataRepository.getMetadata();
};

setPartialMetadata = async (
partialMetadata: Partial<AnchorMetadata>
partialMetadata: Partial<IdentityMetadata>
): Promise<void> => {
await this.metadataRepository.setPartialMetadata(partialMetadata);
};
Expand Down

0 comments on commit 270c219

Please sign in to comment.