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

Populate sso-session and services sections when loading config files #993

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
5 changes: 5 additions & 0 deletions .changeset/eight-lions-sniff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@smithy/types": patch
---

Add enum IniSectionType
5 changes: 5 additions & 0 deletions .changeset/mighty-pianos-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@smithy/shared-ini-file-loader": minor
---

Populate sso-session and services sections when loading config files
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
import { getProfileData } from "./getProfileData";
import { IniSectionType } from "@smithy/types";

describe(getProfileData.name, () => {
import { getConfigData } from "./getConfigData";
import { CONFIG_PREFIX_SEPARATOR } from "./loadSharedConfigFiles";

describe(getConfigData.name, () => {
it("returns empty for no data", () => {
expect(getProfileData({})).toStrictEqual({});
expect(getConfigData({})).toStrictEqual({});
});

it("returns default profile if present", () => {
const mockInput = { default: { key: "value" } };
expect(getProfileData(mockInput)).toStrictEqual(mockInput);
expect(getConfigData(mockInput)).toStrictEqual(mockInput);
});

it("skips profiles without prefix profile", () => {
const mockInput = { test: { key: "value" } };
expect(getProfileData(mockInput)).toStrictEqual({});
expect(getConfigData(mockInput)).toStrictEqual({});
});

it("skips profiles with different prefix", () => {
const mockInput = { "not-profile test": { key: "value" } };
expect(getProfileData(mockInput)).toStrictEqual({});
it.each([IniSectionType.SSO_SESSION, IniSectionType.SERVICES])("includes sections with '%s' prefix", (prefix) => {
const mockInput = { [[prefix, "test"].join(CONFIG_PREFIX_SEPARATOR)]: { key: "value" } };
expect(getConfigData(mockInput)).toStrictEqual(mockInput);
});

describe("normalizes profile names", () => {
Expand All @@ -30,38 +33,41 @@ describe(getProfileData.name, () => {
profileNames.reduce((acc, profileName) => ({ ...acc, [profileName]: getMockProfileData(profileName) }), {});

const getMockInput = (mockOutput: Record<string, Record<string, string>>) =>
Object.entries(mockOutput).reduce((acc, [key, value]) => ({ ...acc, [`profile ${key}`]: value }), {});
Object.entries(mockOutput).reduce(
(acc, [key, value]) => ({ ...acc, [[IniSectionType.PROFILE, key].join(CONFIG_PREFIX_SEPARATOR)]: value }),
{}
);

it("single profile", () => {
const mockOutput = getMockOutput(["one"]);
const mockInput = getMockInput(mockOutput);
expect(getProfileData(mockInput)).toStrictEqual(mockOutput);
expect(getConfigData(mockInput)).toStrictEqual(mockOutput);
});

it("two profiles", () => {
const mockOutput = getMockOutput(["one", "two"]);
const mockInput = getMockInput(mockOutput);
expect(getProfileData(mockInput)).toStrictEqual(mockOutput);
expect(getConfigData(mockInput)).toStrictEqual(mockOutput);
});

it("three profiles", () => {
const mockOutput = getMockOutput(["one", "two", "three"]);
const mockInput = getMockInput(mockOutput);
expect(getProfileData(mockInput)).toStrictEqual(mockOutput);
expect(getConfigData(mockInput)).toStrictEqual(mockOutput);
});

it("with default", () => {
const defaultInput = { default: { key: "value" } };
const mockOutput = getMockOutput(["one"]);
const mockInput = getMockInput(mockOutput);
expect(getProfileData({ ...defaultInput, ...mockInput })).toStrictEqual({ ...defaultInput, ...mockOutput });
expect(getConfigData({ ...defaultInput, ...mockInput })).toStrictEqual({ ...defaultInput, ...mockOutput });
});

it("with profileName without prefix", () => {
const profileWithPrefix = { test: { key: "value" } };
const mockOutput = getMockOutput(["one"]);
const mockInput = getMockInput(mockOutput);
expect(getProfileData({ ...profileWithPrefix, ...mockInput })).toStrictEqual(mockOutput);
expect(getConfigData({ ...profileWithPrefix, ...mockInput })).toStrictEqual(mockOutput);
});
});
});
32 changes: 32 additions & 0 deletions packages/shared-ini-file-loader/src/getConfigData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { IniSectionType, ParsedIniData } from "@smithy/types";

import { CONFIG_PREFIX_SEPARATOR } from "./loadSharedConfigFiles";

/**
* Returns the config data from parsed ini data.
* * Returns data for `default`
* * Returns profile name without prefix.
* * Returns non-profiles as is.
*/
export const getConfigData = (data: ParsedIniData): ParsedIniData =>
Object.entries(data)
// filter out
.filter(([key]) => {
const sections = key.split(CONFIG_PREFIX_SEPARATOR);
if (sections.length === 2 && Object.values(IniSectionType).includes(sections[0] as IniSectionType)) {
return true;
}
return false;
})
// replace profile prefix, if present.
.reduce(
(acc, [key, value]) => {
const updatedKey = key.startsWith(IniSectionType.PROFILE) ? key.split(CONFIG_PREFIX_SEPARATOR)[1] : key;
acc[updatedKey] = value;
return acc;
},
{
// Populate default profile, if present.
...(data.default && { default: data.default }),
} as ParsedIniData
);
18 changes: 0 additions & 18 deletions packages/shared-ini-file-loader/src/getProfileData.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { IniSectionType } from "@smithy/types";

import { getSsoSessionData } from "./getSsoSessionData";
import { CONFIG_PREFIX_SEPARATOR } from "./loadSharedConfigFiles";

describe(getSsoSessionData.name, () => {
it("returns empty for no data", () => {
Expand All @@ -25,7 +28,10 @@ describe(getSsoSessionData.name, () => {
ssoSessionNames.reduce((acc, profileName) => ({ ...acc, [profileName]: getMockSsoSessionData(profileName) }), {});

const getMockInput = (mockOutput: { [key: string]: { [key: string]: string } }) =>
Object.entries(mockOutput).reduce((acc, [key, value]) => ({ ...acc, [`sso-session ${key}`]: value }), {});
Object.entries(mockOutput).reduce(
(acc, [key, value]) => ({ ...acc, [[IniSectionType.SSO_SESSION, key].join(CONFIG_PREFIX_SEPARATOR)]: value }),
{}
);

it("single sso-session section", () => {
const mockOutput = getMockOutput(["one"]);
Expand Down
8 changes: 4 additions & 4 deletions packages/shared-ini-file-loader/src/getSsoSessionData.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ParsedIniData } from "@smithy/types";
import { IniSectionType, ParsedIniData } from "@smithy/types";

const ssoSessionKeyRegex = /^sso-session\s(["'])?([^\1]+)\1$/;
import { CONFIG_PREFIX_SEPARATOR } from "./loadSharedConfigFiles";

/**
* Returns the sso-session data from parsed ini data by reading
Expand All @@ -9,6 +9,6 @@ const ssoSessionKeyRegex = /^sso-session\s(["'])?([^\1]+)\1$/;
export const getSsoSessionData = (data: ParsedIniData): ParsedIniData =>
Object.entries(data)
// filter out non sso-session keys
.filter(([key]) => ssoSessionKeyRegex.test(key))
.filter(([key]) => key.startsWith(IniSectionType.SSO_SESSION + CONFIG_PREFIX_SEPARATOR))
// replace sso-session key with sso-session name
.reduce((acc, [key, value]) => ({ ...acc, [ssoSessionKeyRegex.exec(key)![2]]: value }), {});
.reduce((acc, [key, value]) => ({ ...acc, [key.split(CONFIG_PREFIX_SEPARATOR)[1]]: value }), {});
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { getConfigData } from "./getConfigData";
import { getConfigFilepath } from "./getConfigFilepath";
import { getCredentialsFilepath } from "./getCredentialsFilepath";
import { getProfileData } from "./getProfileData";
import { loadSharedConfigFiles } from "./loadSharedConfigFiles";
import { parseIni } from "./parseIni";
import { slurpFile } from "./slurpFile";

jest.mock("./getConfigData");
jest.mock("./getConfigFilepath");
jest.mock("./getCredentialsFilepath");
jest.mock("./getProfileData");
jest.mock("./parseIni");
jest.mock("./slurpFile");

Expand All @@ -23,7 +23,7 @@ describe("loadSharedConfigFiles", () => {
(getConfigFilepath as jest.Mock).mockReturnValue(mockConfigFilepath);
(getCredentialsFilepath as jest.Mock).mockReturnValue(mockCredsFilepath);
(parseIni as jest.Mock).mockImplementation((args) => args);
(getProfileData as jest.Mock).mockImplementation((args) => args);
(getConfigData as jest.Mock).mockImplementation((args) => args);
(slurpFile as jest.Mock).mockImplementation((path) => Promise.resolve(path));
});

Expand Down Expand Up @@ -63,7 +63,7 @@ describe("loadSharedConfigFiles", () => {
});

it("when normalizeConfigFile throws error", async () => {
(getProfileData as jest.Mock).mockRejectedValue("error");
(getConfigData as jest.Mock).mockRejectedValue("error");
const sharedConfigFiles = await loadSharedConfigFiles();
expect(sharedConfigFiles).toStrictEqual({
configFile: {},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { SharedConfigFiles } from "@smithy/types";

import { getConfigData } from "./getConfigData";
import { getConfigFilepath } from "./getConfigFilepath";
import { getCredentialsFilepath } from "./getCredentialsFilepath";
import { getProfileData } from "./getProfileData";
import { parseIni } from "./parseIni";
import { slurpFile } from "./slurpFile";

Expand Down Expand Up @@ -40,7 +40,7 @@ export const loadSharedConfigFiles = async (init: SharedConfigInit = {}): Promis
ignoreCache: init.ignoreCache,
})
.then(parseIni)
.then(getProfileData)
.then(getConfigData)
.catch(swallowError),
slurpFile(filepath, {
ignoreCache: init.ignoreCache,
Expand Down
27 changes: 13 additions & 14 deletions packages/shared-ini-file-loader/src/parseIni.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { IniSectionType } from "@smithy/types";

import { CONFIG_PREFIX_SEPARATOR } from "./loadSharedConfigFiles";
import { parseIni } from "./parseIni";

Expand Down Expand Up @@ -44,12 +46,17 @@ describe(parseIni.name, () => {
});
});

it("returns data for one profile", () => {
const mockInput = getMockProfileContent(mockProfileName, mockProfileData);
expect(parseIni(mockInput)).toStrictEqual({
[mockProfileName]: mockProfileData,
});
});
it.each(Object.values(IniSectionType))(
"returns data for section '%s' with separator",
(sectionType: IniSectionType) => {
const mockSectionName = "mock_section_name";
const mockSectionFullName = [sectionType, mockSectionName].join(" ");
const mockInput = getMockProfileContent(mockSectionFullName, mockProfileData);
expect(parseIni(mockInput)).toStrictEqual({
[[sectionType, mockSectionName].join(CONFIG_PREFIX_SEPARATOR)]: mockProfileData,
});
}
);

it("returns data for two profiles", () => {
const mockProfile1 = getMockProfileContent(mockProfileName, mockProfileData);
Expand All @@ -75,14 +82,6 @@ describe(parseIni.name, () => {
});
});

it("returns data profile name containing multiple words", () => {
const mockProfileNameMultiWords = "foo bar baz";
const mockInput = getMockProfileContent(mockProfileNameMultiWords, mockProfileData);
expect(parseIni(mockInput)).toStrictEqual({
[mockProfileNameMultiWords]: mockProfileData,
});
});

it("returns data for profile containing multiple entries", () => {
const mockProfileDataMultipleEntries = { key1: "value1", key2: "value2", key3: "value3" };
const mockInput = getMockProfileContent(mockProfileName, mockProfileDataMultipleEntries);
Expand Down
24 changes: 20 additions & 4 deletions packages/shared-ini-file-loader/src/parseIni.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { ParsedIniData } from "@smithy/types";
import { IniSectionType, ParsedIniData } from "@smithy/types";

import { CONFIG_PREFIX_SEPARATOR } from "./loadSharedConfigFiles";

const prefixKeyRegex = /^([\w-]+)\s(["'])?([\w-]+)\2$/;
const profileNameBlockList = ["__proto__", "profile __proto__"];

export const parseIni = (iniData: string): ParsedIniData => {
Expand All @@ -14,10 +15,25 @@ export const parseIni = (iniData: string): ParsedIniData => {
line = line.split(/(^|\s)[;#]/)[0].trim(); // remove comments and trim
const isSection: boolean = line[0] === "[" && line[line.length - 1] === "]";
if (isSection) {
// New section found. Reset currentSection and currentSubSection.
currentSection = undefined;
currentSubSection = undefined;
currentSection = line.substring(1, line.length - 1);
if (profileNameBlockList.includes(currentSection)) {
throw new Error(`Found invalid profile name "${currentSection}"`);

const sectionName = line.substring(1, line.length - 1);
const matches = prefixKeyRegex.exec(sectionName);
if (matches) {
const [, prefix, , name] = matches;
// Add prefix, if the section name starts with `profile`, `sso-session` or `services`.
if (Object.values(IniSectionType).includes(prefix as IniSectionType)) {
currentSection = [prefix, name].join(CONFIG_PREFIX_SEPARATOR);
}
} else {
// If the section name does not match the regex, use the section name as is.
currentSection = sectionName;
}

if (profileNameBlockList.includes(sectionName)) {
throw new Error(`Found invalid profile name "${sectionName}"`);
}
} else if (currentSection) {
const indexOfEqualsSign = line.indexOf("=");
Expand Down
9 changes: 9 additions & 0 deletions packages/types/src/profile.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
/**
* @public
*/
export enum IniSectionType {
PROFILE = "profile",
SSO_SESSION = "sso-session",
SERVICES = "services",
}

/**
* @public
*/
Expand Down