test: enhance deep link testing with real JWT examples

Changes:
- Add real JWT example for invite testing
- Add detailed JWT payload documentation
- Update test-deeplinks.sh with valid claim IDs
- Add test case for single contact invite
- Improve test descriptions and organization

This improves test coverage by using real-world JWT examples
and valid claim identifiers.
This commit is contained in:
Matthew Raymer
2025-03-01 12:28:48 +00:00
parent 6b3542fd09
commit a9fd33fff6
9 changed files with 574 additions and 213 deletions

View File

@@ -193,6 +193,38 @@ import {
import * as libsUtil from "../libs/util";
import { retrieveAccountDids } from "../libs/util";
/**
* Offer Details View Component
* @author Matthew Raymer
*
* This component handles the creation and editing of offers within the platform.
* It supports both new offers and editing existing ones, with validation and
* submission handling.
*
* Features:
* - Offer amount and unit selection
* - Item description
* - Conditional requirements
* - Expiration date setting
* - Project or recipient targeting
* - Raw claim editing option
*
* Data Flow:
* 1. Component loads with optional previous offer data
* 2. Retrieves account settings and contact information
* 3. Populates form with existing or default values
* 4. Validates and submits offer to server
* 5. Redirects on success or shows error
*
* Security Features:
* - DID validation
* - JWT handling for edits
* - Server-side validation
* - Privacy controls for data sharing
*
* @see GiftedDialog for related gift creation
* @see ClaimAddRawView for raw claim editing
*/
@Component({
components: {
QuickNav,
@@ -200,35 +232,96 @@ import { retrieveAccountDids } from "../libs/util";
},
})
export default class OfferDetailsView extends Vue {
/** Notification function injected by Vue */
$notify!: (notification: NotificationIface, timeout?: number) => void;
/** Current route instance */
$route!: RouteLocationNormalizedLoaded;
/** Router instance for navigation */
$router!: Router;
/** Currently active DID */
activeDid = "";
/** API server endpoint */
apiServer = "";
/** Offer amount input field */
amountInput = "0";
/** Conditions for the offer */
descriptionOfCondition = "";
/** Description of offered item */
descriptionOfItem = "";
/** Path to redirect after completion */
destinationPathAfter = "";
/** Controls back button visibility */
hideBackButton = false;
/** Additional message to display */
message = "";
/** Flag for project assignment */
offeredToProject = false;
/** Flag for recipient assignment */
offeredToRecipient = false;
/** DID of offer creator */
offererDid: string | undefined;
/** Offer ID for editing */
offerId = "";
/** Previous offer data for editing */
prevCredToEdit?: GenericCredWrapper<OfferVerifiableCredential>;
/** Project ID if offer is for project */
projectId = "";
/** Project name display */
projectName = "a project";
/** Recipient DID if offer is for person */
recipientDid = "";
/** Recipient name display */
recipientName = "";
/** Advanced features visibility flag */
showGeneralAdvanced = false;
/** Unit type for offer amount */
unitCode = "HUR";
/** Expiration date input */
validThroughDateInput = "";
/** Utility library reference */
libsUtil = libsUtil;
/**
* Component lifecycle hook that initializes the offer form
*
* Workflow:
* 1. Extracts previous offer data if editing
* 2. Sets initial form values from route or previous offer
* 3. Loads account settings and contacts
* 4. Retrieves project information if needed
* 5. Sets offer assignment flags
*
* @throws Will not throw but shows notifications
* @emits Notifications on loading errors
*/
async mounted() {
try {
await this.loadPreviousOffer();
await this.initializeFormValues();
await this.loadAccountSettings();
await this.loadRecipientInfo();
await this.loadProjectInfo();
} catch (err: any) {
console.error("Error in mounted:", err);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: err.message || "There was an error loading the offer details.",
},
5000,
);
}
}
/**
* Loads previous offer data if editing an existing offer
* @throws Will not throw but shows notifications
*/
private async loadPreviousOffer() {
try {
this.prevCredToEdit = (this.$route.query["prevCredToEdit"] as string)
? (JSON.parse(
@@ -246,34 +339,38 @@ export default class OfferDetailsView extends Vue {
5000,
);
}
}
/**
* Initializes form values from route params or previous offer
*/
private async initializeFormValues() {
const prevAmount =
this.prevCredToEdit?.claim?.includesObject?.amountOfThisGood;
this.amountInput =
(this.$route.query["amountInput"] as string) ||
(prevAmount ? String(prevAmount) : "") ||
this.amountInput;
this.unitCode = ((this.$route.query["unitCode"] as string) ||
this.prevCredToEdit?.claim?.includesObject?.unitCode ||
this.unitCode) as string;
this.descriptionOfCondition =
this.prevCredToEdit?.claim?.description || this.descriptionOfCondition;
this.descriptionOfItem =
(this.$route.query["description"] as string) ||
this.prevCredToEdit?.claim?.itemOffered?.description ||
this.descriptionOfItem;
this.destinationPathAfter =
(this.$route.query["destinationPathAfter"] as string) || "";
this.offererDid = ((this.$route.query["offererDid"] as string) ||
(this.prevCredToEdit?.claim?.agent as unknown as { identifier: string })
?.identifier ||
this.offererDid) as string;
this.hideBackButton =
(this.$route.query["hideBackButton"] as string) === "true";
this.message = (this.$route.query["message"] as string) || "";
// find any project ID
// Set project info from previous offer or route
let project;
if (
this.prevCredToEdit?.claim?.itemOffered?.isPartOf?.["@type"] ===
@@ -294,43 +391,43 @@ export default class OfferDetailsView extends Vue {
this.validThroughDateInput =
this.prevCredToEdit?.claim?.validThrough || this.validThroughDateInput;
}
try {
const settings = await retrieveSettingsForActiveAccount();
this.apiServer = settings.apiServer ?? "";
this.activeDid = settings.activeDid ?? "";
this.showGeneralAdvanced = settings.showGeneralAdvanced ?? false;
/**
* Loads account settings and updates component state
* @throws Will not throw but logs errors
*/
private async loadAccountSettings() {
const settings = await retrieveSettingsForActiveAccount();
this.apiServer = settings.apiServer ?? "";
this.activeDid = settings.activeDid ?? "";
this.showGeneralAdvanced = settings.showGeneralAdvanced ?? false;
}
if (this.recipientDid && !this.recipientName) {
const allContacts = await db.contacts.toArray();
const allMyDids = await retrieveAccountDids();
this.recipientName = didInfo(
this.recipientDid,
this.activeDid,
allMyDids,
allContacts,
);
}
// these should be functions but something's wrong with the syntax in the <> conditional
this.offeredToProject = !!this.projectId;
this.offeredToRecipient = !this.offeredToProject && !!this.recipientDid;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
console.error("Error retrieving settings from database:", err);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: err.message || "There was an error retrieving your settings.",
},
5000,
/**
* Loads recipient information if recipient DID exists
*/
private async loadRecipientInfo() {
if (this.recipientDid && !this.recipientName) {
const allContacts = await db.contacts.toArray();
const allMyDids = await retrieveAccountDids();
this.recipientName = didInfo(
this.recipientDid,
this.activeDid,
allMyDids,
allContacts,
);
}
// Set assignment flags
this.offeredToProject = !!this.projectId;
this.offeredToRecipient = !this.offeredToProject && !!this.recipientDid;
}
/**
* Loads project information if project ID exists
*/
private async loadProjectInfo() {
if (this.projectId && !this.projectName) {
// console.log("Getting project name from cache", this.projectId);
const project = await getPlanFromCache(
this.projectId,
this.axios,
@@ -343,16 +440,32 @@ export default class OfferDetailsView extends Vue {
}
}
/**
* Changes the unit type for the offer amount
*
* Cycles through available unit types in UNIT_SHORT.
* Updates display and internal state.
*/
changeUnitCode() {
const units = Object.keys(this.libsUtil.UNIT_SHORT);
const index = units.indexOf(this.unitCode);
this.unitCode = units[(index + 1) % units.length];
}
/**
* Increments the offer amount by 1
*
* Handles string to number conversion and updates display.
*/
increment() {
this.amountInput = `${(parseFloat(this.amountInput) || 0) + 1}`;
}
/**
* Decrements the offer amount by 1
*
* Prevents negative values and handles string to number conversion.
*/
decrement() {
this.amountInput = `${Math.max(
0,
@@ -360,6 +473,15 @@ export default class OfferDetailsView extends Vue {
)}`;
}
/**
* Handles cancellation of offer creation/editing
*
* Workflow:
* 1. Checks for destination path
* 2. Navigates to destination or previous page
*
* @emits Router navigation
*/
cancel() {
if (this.destinationPathAfter) {
(this.$router as Router).push({ path: this.destinationPathAfter });
@@ -368,10 +490,28 @@ export default class OfferDetailsView extends Vue {
}
}
/**
* Handles back button navigation
*
* @emits Router navigation to previous page
*/
cancelBack() {
(this.$router as Router).back();
}
/**
* Validates and initiates offer submission
*
* Workflow:
* 1. Validates active DID exists
* 2. Checks for negative amounts
* 3. Ensures description or amount exists
* 4. Shows processing notification
* 5. Calls recordOffer for submission
*
* @throws Will not throw but shows notifications
* @emits Notifications for validation errors or processing
*/
async confirm() {
if (!this.activeDid) {
this.$notify(
@@ -426,6 +566,15 @@ export default class OfferDetailsView extends Vue {
await this.recordOffer();
}
/**
* Notifies user about project assignment restrictions
*
* Shows appropriate error message based on:
* - Missing project ID
* - Conflict with recipient assignment
*
* @emits Notification with error message
*/
notifyUserOfProject() {
if (!this.projectId) {
this.$notify(
@@ -451,6 +600,15 @@ export default class OfferDetailsView extends Vue {
}
}
/**
* Notifies user about recipient assignment restrictions
*
* Shows appropriate error message based on:
* - Missing recipient DID
* - Conflict with project assignment
*
* @emits Notification with error message
*/
notifyUserOfRecipient() {
if (!this.recipientDid) {
this.$notify(
@@ -477,11 +635,18 @@ export default class OfferDetailsView extends Vue {
}
/**
* Records the offer to the server
*
* @param offererDid may be null
* @param description may be an empty string
* @param amountInput may be 0
* @param unitCode may be omitted, defaults to "HUR"
* Workflow:
* 1. Determines if editing existing or creating new
* 2. Prepares offer data with assignments
* 3. Submits to server via appropriate method
* 4. Handles success/error responses
* 5. Navigates on success
*
* @throws Will not throw but shows notifications
* @emits Notifications for success/failure
* @emits Router navigation on success
*/
public async recordOffer() {
try {
@@ -568,6 +733,17 @@ export default class OfferDetailsView extends Vue {
}
}
/**
* Constructs offer parameters for raw editing
*
* Creates a JSON string containing:
* - Offer details
* - Assignments
* - Conditions
* - Expiration
*
* @returns JSON string of offer parameters
*/
constructOfferParam() {
const recipientDid = this.offeredToRecipient
? this.recipientDid
@@ -589,11 +765,11 @@ export default class OfferDetailsView extends Vue {
return claimStr;
}
// Helper functions for readability
/**
* @param result response "data" from the server
* @returns true if the result indicates an error
* Checks if server response indicates an error
*
* @param result Response data from server
* @returns true if response indicates error
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
isCreationError(result: any) {
@@ -601,8 +777,10 @@ export default class OfferDetailsView extends Vue {
}
/**
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
* @returns best guess at an error message
* Extracts error message from server response
*
* @param result Server response object
* @returns Best available error message
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getCreationErrorMessage(result: any) {
@@ -613,6 +791,13 @@ export default class OfferDetailsView extends Vue {
);
}
/**
* Shows privacy information dialog
*
* Displays standard privacy message about data sharing.
*
* @emits Notification with privacy message
*/
explainData() {
this.$notify(
{