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

feat: add schema validation for localization files #12537

Merged
merged 7 commits into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
3 changes: 3 additions & 0 deletions packages/fx-core/resource/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,9 @@
"_error.appstudio.BotProvisionReturnsForbiddenResult.comment": "This is to describe API call, no need to translate 'Botframework'.",
"error.appstudio.BotProvisionReturnsConflictResult": "Botframework provisioning returns conflict result when attempting to create bot registration.",
"_error.appstudio.BotProvisionReturnsConflictResult.comment": "This is to describe API call, no need to translate 'Botframework'.",
"error.appstudio.localizationFile.pathNotDefined": "Localization file not found. Path: %s.",
"error.appstudio.localizationFile.validationFailed": "Localization file validation failed. File: %s. Error:\n%s",
anchenyi marked this conversation as resolved.
Show resolved Hide resolved
"error.appstudio.localizationFile.validationException": "Localization file validation failed with exceptions. File: %s. Error: %s",
anchenyi marked this conversation as resolved.
Show resolved Hide resolved
"error.generator.ScaffoldLocalTemplateError": "Unable to scaffold template based on local zip package.",
"error.generator.TemplateNotFoundError": "Unable to find template: %s.",
"error.generator.SampleNotFoundError": "Unable to find sample: %s.",
Expand Down
101 changes: 100 additions & 1 deletion packages/fx-core/src/component/driver/teamsApp/validate.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { Result, FxError, ok, err, Platform, ManifestUtil, Colors } from "@microsoft/teamsfx-api";
import {
Result,
FxError,
ok,
err,
Platform,
ManifestUtil,
Colors,
TeamsAppManifest,
UserError,
} from "@microsoft/teamsfx-api";
Fixed Show fixed Hide fixed
import { hooks } from "@feathersjs/hooks/lib";
import { Service } from "typedi";
import { EOL } from "os";
Expand Down Expand Up @@ -96,6 +106,16 @@
);
}

// validate localization files
const localizationFilesValidationRes = await this.validateLocalizatoinFiles(
args,
context,
manifest
);
if (localizationFilesValidationRes.isErr()) {
return err(localizationFilesValidationRes.error);

Check warning on line 116 in packages/fx-core/src/component/driver/teamsApp/validate.ts

View check run for this annotation

Codecov / codecov/patch

packages/fx-core/src/component/driver/teamsApp/validate.ts#L116

Added line #L116 was not covered by tests
}

let declarativeCopilotValidationResult;
let pluginValidationResult;
let pluginPath = "";
Expand Down Expand Up @@ -337,4 +357,83 @@
}
return ok(undefined);
}

public async validateLocalizatoinFiles(
args: ValidateManifestArgs,
context: WrapDriverContext,
manifest: TeamsAppManifest
): Promise<Result<any, FxError>> {
const additionalLanguages = manifest.localizationInfo?.additionalLanguages;
if (!additionalLanguages || additionalLanguages.length == 0) {
return ok(undefined);
}
for (const language of additionalLanguages) {
const filePath = language?.file;
if (!filePath) {
return err(
AppStudioResultFactory.UserError(
AppStudioError.ValidationFailedError.name,
AppStudioError.ValidationFailedError.message([
getLocalizedString("error.appstudio.localizationFile.pathNotDefined", filePath),
]),
HelpLinks.WhyNeedProvision
)
);
}
const localizationFileDir = path.dirname(
getAbsolutePath(args.manifestPath, context.projectPath)
);
const localizationFilePath = getAbsolutePath(filePath, localizationFileDir);

const manifestRes = await manifestUtils._readAppManifest(localizationFilePath);
if (manifestRes.isErr()) {
return err(manifestRes.error);
}
const localizationFile = manifestRes.value;
try {
const schema = await ManifestUtil.fetchSchema(localizationFile);
// the current localization schema has invalid regex sytax, we will skip some properties validation temporarily
delete schema.patternProperties[
"^activities.activityTypes\\[\\b([0-9]|[1-8][0-9]|9[0-9]|1[01][0-9]|12[0-7])\\b]\\.description$"
];
delete schema.patternProperties[
"^activities.activityTypes\\[\\b([0-9]|[1-8][0-9]|9[0-9]|1[01][0-9]|12[0-7])\\b]\\.templateText$"
];
const validationRes = await ManifestUtil.validateManifestAgainstSchema(
localizationFile,
schema
);
if (validationRes.length > 0) {
return err(

Check warning on line 407 in packages/fx-core/src/component/driver/teamsApp/validate.ts

View check run for this annotation

Codecov / codecov/patch

packages/fx-core/src/component/driver/teamsApp/validate.ts#L407

Added line #L407 was not covered by tests
AppStudioResultFactory.UserError(
AppStudioError.ValidationFailedError.name,
AppStudioError.ValidationFailedError.message([
getLocalizedString(
"error.appstudio.localizationFile.validationFailed",
filePath,
validationRes.join(EOL)
),
]),
HelpLinks.WhyNeedProvision
)
);
}
} catch (e: any) {
return err(
AppStudioResultFactory.UserError(
AppStudioError.ValidationFailedError.name,
AppStudioError.ValidationFailedError.message([
getLocalizedString(
"error.appstudio.localizationFile.validationException",
filePath,
e.message
),
]),
HelpLinks.WhyNeedProvision
)
);
}
}
return ok(undefined);
}
}
116 changes: 116 additions & 0 deletions packages/fx-core/tests/component/driver/teamsApp/validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,122 @@ describe("teamsApp/validateManifest", async () => {
}
});

describe("validateLocalizatoinFiles", async () => {
const teamsAppDriver = new ValidateManifestDriver();
const mockedDriverContext: any = {
projectPath: "./",
};

afterEach(() => {
sinon.restore();
});

it("should return ok when no additionalLanguages in manifest", async () => {
const args: ValidateManifestArgs = { manifestPath: "fakepath" };
const manifest = { localizationInfo: { additionalLanguages: [] } } as any;

const result = await teamsAppDriver.validateLocalizatoinFiles(
args,
mockedDriverContext,
manifest
);
chai.assert(result.isOk());
});

it("should return error when language file path is not defined", async () => {
const args: ValidateManifestArgs = { manifestPath: "fakepath" };
const manifest = { localizationInfo: { additionalLanguages: [{ file: undefined }] } } as any;

const result = await teamsAppDriver.validateLocalizatoinFiles(
args,
mockedDriverContext,
manifest
);
chai.assert(result.isErr());
if (result.isErr()) {
chai.assert.equal(result.error.name, AppStudioError.ValidationFailedError.name);
}
});

it("should return error when manifest file cannot be found", async () => {
const args: ValidateManifestArgs = { manifestPath: "fakepath" };
const manifest = { localizationInfo: { additionalLanguages: [{ file: "filePath" }] } } as any;

sinon
.stub(manifestUtils, "_readAppManifest")
.resolves(err(new SystemError("error", "error", "", "")));

const result = await teamsAppDriver.validateLocalizatoinFiles(
args,
mockedDriverContext,
manifest
);
chai.assert(result.isErr());
if (result.isErr()) {
chai.assert.equal(result.error.name, "error");
}
});

it("should return error when validation fails", async () => {
const args: ValidateManifestArgs = { manifestPath: "fakepath" };
const manifest = { localizationInfo: { additionalLanguages: [{ file: "filePath" }] } } as any;
const fakeLocalizationFile = {};

sinon.stub(manifestUtils, "_readAppManifest").resolves(ok(fakeLocalizationFile as any));
sinon.stub(ManifestUtil, "validateManifestAgainstSchema").resolves(["Validation error"]);

const result = await teamsAppDriver.validateLocalizatoinFiles(
args,
mockedDriverContext,
manifest
);
chai.assert(result.isErr());
if (result.isErr()) {
chai.assert.equal(result.error.name, AppStudioError.ValidationFailedError.name);
}
});

it("should return error when validation throws exception", async () => {
const args: ValidateManifestArgs = { manifestPath: "fakepath" };
const manifest = { localizationInfo: { additionalLanguages: [{ file: "filePath" }] } } as any;
const fakeLocalizationFile = {};

sinon.stub(manifestUtils, "_readAppManifest").resolves(ok(fakeLocalizationFile as any));
sinon
.stub(ManifestUtil, "validateManifestAgainstSchema")
.throws(new Error("validation exception"));

const result = await teamsAppDriver.validateLocalizatoinFiles(
args,
mockedDriverContext,
manifest
);
chai.assert(result.isErr());
if (result.isErr()) {
chai.assert.equal(result.error.name, AppStudioError.ValidationFailedError.name);
}
});

it("should return ok when localization file is valid", async () => {
const args: ValidateManifestArgs = { manifestPath: "fakepath" };
const manifest = { localizationInfo: { additionalLanguages: [{ file: "filePath" }] } } as any;
const fakeLocalizationFile = {
$schema:
"https://developer.microsoft.com/en-us/json-schemas/teams/v1.16/MicrosoftTeams.Localization.schema.json",
};

sinon.stub(manifestUtils, "_readAppManifest").resolves(ok(fakeLocalizationFile as any));
sinon.stub(ManifestUtil, "validateManifestAgainstSchema").resolves([]);

const result = await teamsAppDriver.validateLocalizatoinFiles(
args,
mockedDriverContext,
manifest
);
chai.assert(result.isOk());
});
});

describe("validate Copilot extensions", async () => {
it("validate with errors returned", async () => {
const teamsManifest: TeamsAppManifest = new TeamsAppManifest();
Expand Down
Loading