Skip to content

Commit

Permalink
Populate sso-session and services sections when loading config files (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
trivikr authored Oct 6, 2023
1 parent 719777c commit d6b4c09
Show file tree
Hide file tree
Showing 12 changed files with 121 additions and 61 deletions.
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
4 changes: 2 additions & 2 deletions packages/shared-ini-file-loader/src/loadSharedConfigFiles.ts
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

0 comments on commit d6b4c09

Please sign in to comment.