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

Cards/Api: adaptive card action bugfix & card tests #72

Merged
merged 24 commits into from
Mar 10, 2025
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
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
name: Build & Test & Lint

on:
workflow_call:
workflow_dispatch:
Expand All @@ -8,8 +10,8 @@ on:
permissions: read-all

jobs:
build:
name: Build
build-test-lint:
name: Build & Test & Lint
runs-on: ubuntu-latest
strategy:
matrix:
Expand All @@ -26,3 +28,6 @@ jobs:
run: npm install
- name: Build
run: npm run build
- name: Test
run: npm run test

8 changes: 4 additions & 4 deletions packages/api/src/activities/message/message.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ describe('MessageActivity', () => {
})
.addText(', how are you?');

activity.removeMentionsText();
activity.stripMentionsText();
expect(activity.text).toEqual('hi , how are you?');
});

Expand All @@ -152,7 +152,7 @@ describe('MessageActivity', () => {
})
.addText(', how are you?');

activity.removeMentionsText({ tagOnly: true });
activity.stripMentionsText({ tagOnly: true });
expect(activity.text).toEqual('hi test-user, how are you?');
});

Expand All @@ -171,7 +171,7 @@ describe('MessageActivity', () => {
})
.addText(' how are you?');

activity.removeMentionsText({ accountId: '1234' });
activity.stripMentionsText({ accountId: '1234' });
expect(activity.text).toEqual('hi <at>test-user</at>, how are you?');
});

Expand All @@ -184,7 +184,7 @@ describe('MessageActivity', () => {
})
.addText(', how are you?');

activity.removeMentionsText({ accountId: '1' });
activity.stripMentionsText({ accountId: '1' });
expect(activity.text).toEqual('hi <at>test-user</at>, how are you?');
});
});
Expand Down
10 changes: 5 additions & 5 deletions packages/api/src/activities/message/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
} from '../../models';

import { IActivity, Activity } from '../activity';
import { removeMentionsText, RemoveMentionsTextOptions } from '../utils';
import { stripMentionsText, StripMentionsTextOptions } from '../utils';

export interface IMessageActivity extends IActivity<'message'> {
/**
Expand Down Expand Up @@ -85,7 +85,7 @@ export interface IMessageActivity extends IActivity<'message'> {
/**
* remove "\<at>...\</at>" text from an activity
*/
removeMentionsText(options?: RemoveMentionsTextOptions): IMessageActivity;
stripMentionsText(options?: StripMentionsTextOptions): IMessageActivity;

/**
* is the recipient account mentioned
Expand Down Expand Up @@ -186,7 +186,7 @@ export class MessageActivity extends Activity<'message'> implements IMessageActi
toInterface(): IMessageActivity {
return Object.assign(
{
removeMentionsText: this.removeMentionsText.bind(this),
stripMentionsText: this.stripMentionsText.bind(this),
isRecipientMentioned: this.isRecipientMentioned.bind(this),
getAccountMention: this.getAccountMention.bind(this),
},
Expand Down Expand Up @@ -339,8 +339,8 @@ export class MessageActivity extends Activity<'message'> implements IMessageActi
/**
* remove "\<at>...\</at>" text from an activity
*/
removeMentionsText(options: RemoveMentionsTextOptions = {}) {
this.text = removeMentionsText(this, options);
stripMentionsText(options: StripMentionsTextOptions = {}) {
this.text = stripMentionsText(this, options);
return this;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/api/src/activities/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * from './remove-mentions-text';
export * from './strip-mentions-text';
export * from './to-activity-params';
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { MessageActivity } from '../message';
import { removeMentionsText } from './remove-mentions-text';
import { stripMentionsText } from './strip-mentions-text';

describe('Activity Utils', () => {
describe('removeMentionsText', () => {
describe('stripMentionsText', () => {
const activity = new MessageActivity('Hello <at>test-bot</at>! How are you?')
.withChannelId('msteams')
.withConversation({
Expand All @@ -29,7 +29,7 @@ describe('Activity Utils', () => {
});

it('should do nothing when no text', () => {
const text = removeMentionsText({
const text = stripMentionsText({
...activity.toInterface(),
type: 'typing',
text: undefined,
Expand All @@ -39,7 +39,7 @@ describe('Activity Utils', () => {
});

it('should do nothing when no mentions', () => {
const text = removeMentionsText({
const text = stripMentionsText({
...activity.toInterface(),
entities: undefined,
});
Expand All @@ -48,12 +48,12 @@ describe('Activity Utils', () => {
});

it('should remove mention', () => {
const text = removeMentionsText(activity);
const text = stripMentionsText(activity);
expect(text).toEqual('Hello ! How are you?');
});

it('should remove multiple mentions', () => {
const text = removeMentionsText(
const text = stripMentionsText(
activity
.clone()
.withText(`${activity.text} <at>some other text</at>`)
Expand All @@ -72,7 +72,7 @@ describe('Activity Utils', () => {
});

it('should remove only mention tags', () => {
const text = removeMentionsText(
const text = stripMentionsText(
activity
.clone()
.withText(`${activity.text} <at>some other text</at>`)
Expand All @@ -92,7 +92,7 @@ describe('Activity Utils', () => {
});

it('should remove only specific account mentions', () => {
const text = removeMentionsText(
const text = stripMentionsText(
activity
.clone()
.withText(`${activity.text} <at>test-bot-2</at>`)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { ITypingActivity } from '../typing';
*/
type TextActivity = IMessageActivity | IMessageUpdateActivity | ITypingActivity;

export type RemoveMentionsTextOptions = {
export type StripMentionsTextOptions = {
/**
* the account to remove mentions for
* by default, all at-mentions listed in `entities` are removed.
Expand All @@ -26,9 +26,9 @@ export type RemoveMentionsTextOptions = {
* remove "\<at>...\</at>" text from an activity
* @param activity the activity
*/
export function removeMentionsText<TActivity extends TextActivity>(
export function stripMentionsText<TActivity extends TextActivity>(
activity: TActivity,
{ accountId, tagOnly }: RemoveMentionsTextOptions = {}
{ accountId, tagOnly }: StripMentionsTextOptions = {}
): TActivity['text'] {
if (!activity.text) return;

Expand Down
138 changes: 138 additions & 0 deletions packages/api/src/models/adaptive-card/adaptive-card-action-response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { ICard } from '@microsoft/spark.cards';
import { HttpError } from '../error';
import { OAuthCard } from '../oauth';

/**
* Defines the structure that is returned as the result of an Invoke activity with
* Name of 'adaptiveCard/action'.
* https://learn.microsoft.com/en-us/adaptive-cards/authoring-cards/universal-action-model#response-format
*/
export type AdaptiveCardActionResponse =
| AdaptiveCardActionCardResponse
| AdaptiveCardActionMessageResponse
| AdaptiveCardActionErrorResponse
| AdaptiveCardActionLoginResponse
| AdaptiveCardActionIncorrectAuthCodeResponse
| AdaptiveCardActionPreconditionFailedResponse;

/**
* The request was successfully processed, and the response includes
* an Adaptive Card that the client should display in place of the current one
*/
export type AdaptiveCardActionCardResponse = {
/**
* The Card Action response status code.
*/
statusCode: 200;

/**
* The type of this response.
*/
type: 'application/vnd.microsoft.card.adaptive';

/**
* The card response object.
*/
value: ICard;
};

/**
* The request was successfully processed, and the response includes a message that the client should display
*/
export type AdaptiveCardActionMessageResponse = {
/**
* The Card Action response status code.
*/
statusCode: 200;

/**
* The type of this response.
*/
type: 'application/vnd.microsoft.activity.message';

/**
* the response message.
*/
value: string;
};

/**
* `400`: The incoming request was invalid
* `500`: An unexpected error occurred
*/
export type AdaptiveCardActionErrorResponse = {
/**
* The Card Action response status code.
*/
statusCode: 400 | 500;

/**
* The type of this response.
*/
type: 'application/vnd.microsoft.error';

/**
* The error response object.
*/
value: HttpError;
};

/**
* The client needs to prompt the user to authenticate
*/
export type AdaptiveCardActionLoginResponse = {
/**
* The Card Action response status code.
*/
statusCode: 401;

/**
* The type of this response.
*/
type: 'application/vnd.microsoft.activity.loginRequest';

/**
* The auth response object.
*/
value: OAuthCard;
};

/**
* The authentication state passed by the client was incorrect and authentication failed
*/
export type AdaptiveCardActionIncorrectAuthCodeResponse = {
/**
* The Card Action response status code.
*/
statusCode: 401;

/**
* The type of this response.
*/
type: 'application/vnd.microsoft.error.incorrectAuthCode';

/**
* The auth response object.
*/
value: null;
};

/**
* The SSO authentication flow failed
*/
export type AdaptiveCardActionPreconditionFailedResponse = {
/**
* The Card Action response status code.
*/
statusCode: 412;

/**
* The type of this response.
*/
type: 'application/vnd.microsoft.error.preconditionFailed';

/**
* The auth response object.
*/
value: HttpError;
};
1 change: 1 addition & 0 deletions packages/api/src/models/adaptive-card/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './adaptive-card-authentication';
export * from './adaptive-card-invoke-action';
export * from './adaptive-card-invoke-value';
export * from './adaptive-card-action-response';
2 changes: 1 addition & 1 deletion packages/api/src/models/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export type InnerHttpError = {
/**
* An HTTP API response
*/
export type HttpErrorResponse = {
export type ErrorResponse = {
/**
* Error message
*/
Expand Down
5 changes: 2 additions & 3 deletions packages/api/src/models/invoke-response.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { ICard } from '@microsoft/spark.cards';

import {
AdaptiveCardActionResponse,
ConfigResponse,
MessagingExtensionActionResponse,
MessagingExtensionResponse,
Expand Down Expand Up @@ -48,5 +47,5 @@ type InvokeResponseBody = {
'handoff/action': void;
'signin/tokenExchange': TokenExchangeInvokeResponse | undefined;
'signin/verifyState': void;
'adaptiveCard/action': ICard;
'adaptiveCard/action': AdaptiveCardActionResponse;
};
6 changes: 6 additions & 0 deletions packages/apps/src/app.oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export async function onTokenExchange(
if (err instanceof AxiosError) {
if (err.status !== 404 && err.status !== 400) {
this.onActivityError({ ...ctx, err });
return { status: err.status || 500 };
}

if (err.status === 404) {
Expand Down Expand Up @@ -91,6 +92,11 @@ export async function onVerifyState(
if (err instanceof AxiosError) {
if (err.status !== 404 && err.status !== 400) {
this.onActivityError({ ...ctx, err, plugin });
return { status: err.status || 500 };
}

if (err.status === 404) {
return { status: 404 };
}
}

Expand Down
4 changes: 2 additions & 2 deletions packages/apps/src/app.process.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ConversationReference } from '@microsoft/spark.api';
import { ConversationReference, InvokeResponse } from '@microsoft/spark.api';

import { App } from './app';
import { ApiClient } from './api';
Expand Down Expand Up @@ -102,7 +102,7 @@ export async function $process(this: App, sender: ISenderPlugin, event: IActivit
}

try {
const res = (await routes[0](context.toInterface())) || { status: 200 };
const res: InvokeResponse = (await routes[0](context.toInterface())) || { status: 200 };
await context.stream.close();
this.events.emit('activity.response', {
plugin: sender.name,
Expand Down
Loading