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: support env variables in localization.json #12889

Merged
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
Expand Up @@ -175,8 +175,13 @@ export class CreateAppPackageDriver implements StepDriver {
if (relativePath.startsWith("..")) {
return err(new InvalidFileOutsideOfTheDirectotryError(fileName));
}
const dir = path.dirname(file);
zip.addLocalFile(fileName, dir === "." ? "" : dir);
const resolvedLocFileRes = await manifestUtils.resolveLocFile(fileName);
if (resolvedLocFileRes.isErr()) {
return err(resolvedLocFileRes.error);
}
if (resolvedLocFileRes.value) {
zip.addFile(relativePath, Buffer.from(resolvedLocFileRes.value));
}
}
}
if (manifest.localizationInfo && manifest.localizationInfo.defaultLanguageFile) {
Expand All @@ -186,8 +191,14 @@ export class CreateAppPackageDriver implements StepDriver {
if (relativePath.startsWith("..")) {
return err(new InvalidFileOutsideOfTheDirectotryError(fileName));
}
const dir = path.dirname(file);
zip.addLocalFile(fileName, dir === "." ? "" : dir);

const resolvedLocFileRes = await manifestUtils.resolveLocFile(fileName);
if (resolvedLocFileRes.isErr()) {
return err(resolvedLocFileRes.error);
}
if (resolvedLocFileRes.value) {
zip.addFile(relativePath, Buffer.from(resolvedLocFileRes.value));
}
}

// API ME, API specification and Adaptive card templates
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,16 @@ import { v4 } from "uuid";
import isUUID from "validator/lib/isUUID";
import { ErrorContextMW } from "../../../../common/globalVars";
import { getCapabilities as checkManifestCapabilities } from "../../../../common/projectTypeChecker";
import { FileNotFoundError, JSONSyntaxError, ReadFileError } from "../../../../error/common";
import {
FileNotFoundError,
JSONSyntaxError,
MissingEnvironmentVariablesError,
ReadFileError,
} from "../../../../error/common";
import { CapabilityOptions } from "../../../../question/constants";
import { BotScenario } from "../../../constants";
import { convertManifestTemplateToV2, convertManifestTemplateToV3 } from "../../../migrate";
import { expandEnvironmentVariable } from "../../../utils/common";
import { expandEnvironmentVariable, getEnvironmentVariables } from "../../../utils/common";
import { ManifestType } from "../../../utils/envFunctionUtils";
import { DriverContext } from "../../interface/commonArgs";
import {
Expand Down Expand Up @@ -432,6 +437,27 @@ export class ManifestUtils {
}
return ok(undefined);
}

async resolveLocFile(locFilePath: string): Promise<Result<string, FxError>> {
if (!(await fs.pathExists(locFilePath))) {
return err(new FileNotFoundError("teamsApp", locFilePath));
}

const locFileString = await fs.readFile(locFilePath, "utf8");
const resolvedLocFileString = expandEnvironmentVariable(locFileString);
const unresolvedEnvVariables = getEnvironmentVariables(resolvedLocFileString);
if (unresolvedEnvVariables && unresolvedEnvVariables.length > 0) {
return err(
new MissingEnvironmentVariablesError(
"teamsApp",
unresolvedEnvVariables.join(","),
locFilePath
)
);
}

return ok(resolvedLocFileString);
}
}

export const manifestUtils = new ManifestUtils();
8 changes: 4 additions & 4 deletions packages/fx-core/src/component/driver/teamsApp/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,11 +431,11 @@ export class ValidateManifestDriver implements StepDriver {
);
const localizationFilePath = getAbsolutePath(filePath, localizationFileDir);

const manifestRes = await manifestUtils._readAppManifest(localizationFilePath);
if (manifestRes.isErr()) {
return err(manifestRes.error);
const resolvedLocFileRes = await manifestUtils.resolveLocFile(localizationFilePath);
if (resolvedLocFileRes.isErr()) {
return err(resolvedLocFileRes.error);
}
const localizationFile = manifestRes.value;
const localizationFile = JSON.parse(resolvedLocFileRes.value) as TeamsAppManifest;
try {
const schema = await ManifestUtil.fetchSchema(localizationFile);
// the current localization schema has invalid regex sytax, we need to manually fix the properties temporarily
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,14 @@ import { CreateAppPackageArgs } from "../../../../src/component/driver/teamsApp/
import { MockedLogProvider, MockedUserInteraction } from "../../../plugins/solution/util";
import { FileNotFoundError, JSONSyntaxError } from "../../../../src/error/common";
import { manifestUtils } from "../../../../src/component/driver/teamsApp/utils/ManifestUtils";
import { ok, Platform, PluginManifestSchema, TeamsAppManifest } from "@microsoft/teamsfx-api";
import {
err,
ok,
Platform,
PluginManifestSchema,
SystemError,
TeamsAppManifest,
} from "@microsoft/teamsfx-api";
import AdmZip from "adm-zip";
import { InvalidFileOutsideOfTheDirectotryError } from "../../../../src/error/teamsApp";
import { MockedM365Provider } from "../../../core/utils";
Expand Down Expand Up @@ -1469,6 +1476,86 @@ describe("teamsApp/createAppPackage", async () => {
}
});

it("resolve additional localization file error", async () => {
const args: CreateAppPackageArgs = {
manifestPath:
"./tests/plugins/resource/appstudio/resources-multi-env/templates/appPackage/v3.manifest.template.json",
outputZipPath:
"./tests/plugins/resource/appstudio/resources-multi-env/build/appPackage/appPackage.dev.zip",
outputFolder: "./tests/plugins/resource/appstudio/resources-multi-env/build/appPackage",
};

const manifest = new TeamsAppManifest();
manifest.localizationInfo = {
defaultLanguageTag: "en",
additionalLanguages: [
{
languageTag: "de",
file: "migrate.manifest.json",
},
],
defaultLanguageFile: "de.json",
};
manifest.icons = {
color: "resources/color.png",
outline: "resources/outline.png",
};
sinon.stub(manifestUtils, "getManifestV3").resolves(ok(manifest));
sinon.stub(fs, "pathExists").resolves(true);
sinon.stub(fs, "chmod").callsFake(async () => {});
sinon.stub(fs, "writeFile").callsFake(async () => {});
sinon
.stub(manifestUtils, "resolveLocFile")
.resolves(err(new FileNotFoundError("teamsapp", "faked_loc_path")));

const result = (await teamsAppDriver.execute(args, mockedDriverContext)).result;
if (result.isErr()) {
chai.assert.isTrue(result.error instanceof FileNotFoundError);
}
});

it("resolve default localization file error", async () => {
const args: CreateAppPackageArgs = {
manifestPath:
"./tests/plugins/resource/appstudio/resources-multi-env/templates/appPackage/v3.manifest.template.json",
outputZipPath:
"./tests/plugins/resource/appstudio/resources-multi-env/build/appPackage/appPackage.dev.zip",
outputFolder: "./tests/plugins/resource/appstudio/resources-multi-env/build/appPackage",
};

const manifest = new TeamsAppManifest();
manifest.localizationInfo = {
defaultLanguageTag: "en",
additionalLanguages: [
{
languageTag: "de",
file: "migrate.manifest.json",
},
],
defaultLanguageFile: "de.json",
};
manifest.icons = {
color: "resources/color.png",
outline: "resources/outline.png",
};
sinon.stub(manifestUtils, "getManifestV3").resolves(ok(manifest));
sinon.stub(fs, "pathExists").resolves(true);
sinon.stub(fs, "chmod").callsFake(async () => {});
sinon.stub(fs, "writeFile").callsFake(async () => {});
sinon.stub(manifestUtils, "resolveLocFile").callsFake(async (path) => {
if (path.includes("migrate.manifest.json")) {
return ok("{}");
} else {
return err(new FileNotFoundError("teamsapp", "faked_loc_path"));
}
});

const result = (await teamsAppDriver.execute(args, mockedDriverContext)).result;
if (result.isErr()) {
chai.assert.isTrue(result.error instanceof FileNotFoundError);
}
});

it("relative path error 2", async () => {
const args: CreateAppPackageArgs = {
manifestPath:
Expand Down Expand Up @@ -1546,6 +1633,7 @@ describe("teamsApp/createAppPackage", async () => {

sinon.stub(fs, "chmod").callsFake(async () => {});
const writeFileStub = sinon.stub(fs, "writeFile").callsFake(async () => {});
sinon.stub(manifestUtils, "resolveLocFile").resolves(ok("{}"));

const result = (await teamsAppDriver.execute(args, mockedDriverContext)).result;
chai.assert(result.isOk());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
} from "../../../../src/component/driver/teamsApp/constants";
import { AppStudioError } from "../../../../src/component/driver/teamsApp/errors";
import { FileNotFoundError, JSONSyntaxError, ReadFileError } from "../../../../src/error";
import mockedEnv, { RestoreFn } from "mocked-env";

const latestManifestVersion = "1.17";
const oldManifestVersion = "1.16";
Expand Down Expand Up @@ -520,3 +521,60 @@ describe("trimManifestShortName", () => {
assert.isTrue(writeFileStub.notCalled);
});
});

describe("resolveLocFile", () => {
const sandbox = sinon.createSandbox();
let mockedEnvRestore: RestoreFn;

afterEach(() => {
if (mockedEnvRestore) {
mockedEnvRestore();
}
sandbox.restore();
});

it("returns error when loc file doesn't exist", async () => {
sandbox.stub(fs, "pathExists").resolves(false);

const locFile = await manifestUtils.resolveLocFile("loc_file_path");

assert.isTrue(locFile.isErr());
if (locFile.isErr()) {
assert.equal(locFile.error.name, "FileNotFoundError");
}
});

it("returns error when there're unresolved env variables", async () => {
sandbox.stub(fs, "pathExists").resolves(true);
const fakedLocManifest = new TeamsAppManifest();
fakedLocManifest.name.short = "shortname ${{APP_NAME_SUFFIX}}";
sandbox.stub(fs, "readFile").resolves(JSON.stringify(fakedLocManifest) as any);

const locFile = await manifestUtils.resolveLocFile("loc_file_path");

assert.isTrue(locFile.isErr());
if (locFile.isErr()) {
assert.equal(locFile.error.name, "MissingEnvironmentVariablesError");
}
});

it("happy pass", async () => {
sandbox.stub(fs, "pathExists").resolves(true);
const fakedLocManifest = new TeamsAppManifest();
fakedLocManifest.name.short = "shortname ${{APP_NAME_SUFFIX}}";
mockedEnvRestore = mockedEnv({
["APP_NAME_SUFFIX"]: "- hello world",
});
sandbox.stub(fs, "readFile").resolves(JSON.stringify(fakedLocManifest) as any);

const locFile = await manifestUtils.resolveLocFile("loc_file_path");

assert.isTrue(locFile.isOk());
if (locFile.isOk()) {
assert.equal(
(JSON.parse(locFile.value) as TeamsAppManifest).name.short,
"shortname - hello world"
);
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ describe("teamsApp/validateManifest", async () => {
const manifest = { localizationInfo: { additionalLanguages: [{ file: "filePath" }] } } as any;

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

const result = await teamsAppDriver.validateLocalizatoinFiles(
Expand All @@ -297,7 +297,9 @@ describe("teamsApp/validateManifest", async () => {
"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(manifestUtils, "resolveLocFile")
.resolves(ok(JSON.stringify(fakeLocalizationFile)));
sinon.stub(ManifestUtil, "validateManifestAgainstSchema").resolves(["Validation error"]);

const result = await teamsAppDriver.validateLocalizatoinFiles(
Expand Down Expand Up @@ -365,7 +367,9 @@ describe("teamsApp/validateManifest", async () => {
const manifest = { localizationInfo: { additionalLanguages: [{ file: "filePath" }] } } as any;
const fakeLocalizationFile = {};

sinon.stub(manifestUtils, "_readAppManifest").resolves(ok(fakeLocalizationFile as any));
sinon
.stub(manifestUtils, "resolveLocFile")
.resolves(ok(JSON.stringify(fakeLocalizationFile)));
sinon
.stub(ManifestUtil, "validateManifestAgainstSchema")
.throws(new Error("validation exception"));
Expand All @@ -385,7 +389,7 @@ describe("teamsApp/validateManifest", async () => {
const args: ValidateManifestArgs = { manifestPath: "fakepath" };
const manifest = { localizationInfo: { additionalLanguages: [{ file: "filePath" }] } } as any;
sinon.stub(ManifestUtil, "fetchSchema").resolves({} as any);
sinon.stub(manifestUtils, "_readAppManifest").resolves(ok({} as any));
sinon.stub(manifestUtils, "resolveLocFile").resolves(ok("{}"));
sinon.stub(ManifestUtil, "validateManifestAgainstSchema").resolves([] as any);
const result = await teamsAppDriver.validateLocalizatoinFiles(
args,
Expand All @@ -409,7 +413,9 @@ describe("teamsApp/validateManifest", async () => {
"activities.activityTypes[0].description": "aa",
};

sinon.stub(manifestUtils, "_readAppManifest").resolves(ok(fakeLocalizationFile as any));
sinon
.stub(manifestUtils, "resolveLocFile")
.resolves(ok(JSON.stringify(fakeLocalizationFile)));
const result = await teamsAppDriver.validateLocalizatoinFiles(
args,
mockedDriverContext,
Expand Down
Loading