Add util function for parsing durations and many more tests (#69)

This commit is contained in:
Seth Vargo 2021-12-01 16:13:51 -05:00 committed by GitHub
parent 1e9245c68a
commit 057960bb62
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 505 additions and 42 deletions

110
dist/main/index.js vendored
View File

@ -610,7 +610,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.trimmedString = exports.fromBase64 = exports.toBase64 = exports.explodeStrings = exports.removeExportedCredentials = exports.writeSecureFile = void 0;
exports.parseDuration = exports.trimmedString = exports.fromBase64 = exports.toBase64 = exports.explodeStrings = exports.removeExportedCredentials = exports.writeSecureFile = void 0;
const fs_1 = __webpack_require__(747);
const crypto_1 = __importDefault(__webpack_require__(417));
const path_1 = __importDefault(__webpack_require__(622));
@ -673,17 +673,38 @@ exports.removeExportedCredentials = removeExportedCredentials;
* of trimmed strings.
*/
function explodeStrings(input) {
if (input == null || input.length === 0) {
if (!input || input.trim().length === 0) {
return [];
}
const list = new Array();
for (const line of input.split(`\n`)) {
for (const piece of line.split(',')) {
const entry = piece.trim();
if (entry !== '') {
list.push(entry);
}
let curr = '';
let escaped = false;
for (const ch of input) {
if (escaped) {
curr += ch;
escaped = false;
continue;
}
switch (ch) {
case '\\':
escaped = true;
continue;
case ',':
case '\n': {
const val = curr.trim();
if (val) {
list.push(val);
}
curr = '';
break;
}
default:
curr += ch;
}
}
const val = curr.trim();
if (val) {
list.push(val);
}
return list;
}
@ -705,7 +726,7 @@ exports.toBase64 = toBase64;
*/
function fromBase64(s) {
const str = s.replace(/-/g, '+').replace(/_/g, '/');
while (str.length % 4)
while (s.length % 4)
s += '=';
return Buffer.from(str, 'base64').toString('utf8');
}
@ -718,6 +739,65 @@ function trimmedString(s) {
return s ? s.trim() : '';
}
exports.trimmedString = trimmedString;
/**
* parseDuration parses a user-supplied string duration with optional suffix and
* returns a number representing the number of seconds. It returns 0 when given
* the empty string.
*
* @param str Duration string
*/
function parseDuration(str) {
const given = (str || '').trim();
if (!given) {
return 0;
}
let total = 0;
let curr = '';
for (let i = 0; i < str.length; i++) {
const ch = str[i];
switch (ch) {
case ' ':
continue;
case ',':
continue;
case 's': {
total += +curr;
curr = '';
break;
}
case 'm': {
total += +curr * 60;
curr = '';
break;
}
case 'h': {
total += +curr * 60 * 60;
curr = '';
break;
}
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
curr += ch;
break;
default:
throw new SyntaxError(`Unsupported character "${ch}" at position ${i}`);
}
}
// Anything left over is seconds
if (curr) {
total += +curr;
}
return total;
}
exports.parseDuration = parseDuration;
/***/ }),
@ -1901,8 +1981,8 @@ class CredentialsJSONClient {
const signature = signer.sign(__classPrivateFieldGet(this, _CredentialsJSONClient_credentials, "f")['private_key']);
return message + '.' + (0, utils_1.toBase64)(signature);
}
catch (e) {
throw new Error(`Failed to sign auth token: ${e}`);
catch (err) {
throw new Error(`Failed to sign auth token using ${this.getServiceAccount()}: ${err}`);
}
});
}
@ -2190,8 +2270,8 @@ class BaseClient {
expiration: parsed['expireTime'],
};
}
catch (e) {
throw new Error(`Failed to generate Google Cloud access token for ${serviceAccount}: ${e}`);
catch (err) {
throw new Error(`Failed to generate Google Cloud access token for ${serviceAccount}: ${err}`);
}
});
}
@ -2267,8 +2347,8 @@ class WorkloadIdentityClient {
return project;
}
/**
* getAuthToken generates a Google Cloud federated token using the provided OIDC
* token and Workload Identity Provider.
* getAuthToken generates a Google Cloud federated token using the provided
* OIDC token and Workload Identity Provider.
*/
getAuthToken() {
return __awaiter(this, void 0, void 0, function* () {

98
dist/post/index.js vendored
View File

@ -449,7 +449,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.trimmedString = exports.fromBase64 = exports.toBase64 = exports.explodeStrings = exports.removeExportedCredentials = exports.writeSecureFile = void 0;
exports.parseDuration = exports.trimmedString = exports.fromBase64 = exports.toBase64 = exports.explodeStrings = exports.removeExportedCredentials = exports.writeSecureFile = void 0;
const fs_1 = __webpack_require__(747);
const crypto_1 = __importDefault(__webpack_require__(417));
const path_1 = __importDefault(__webpack_require__(622));
@ -512,17 +512,38 @@ exports.removeExportedCredentials = removeExportedCredentials;
* of trimmed strings.
*/
function explodeStrings(input) {
if (input == null || input.length === 0) {
if (!input || input.trim().length === 0) {
return [];
}
const list = new Array();
for (const line of input.split(`\n`)) {
for (const piece of line.split(',')) {
const entry = piece.trim();
if (entry !== '') {
list.push(entry);
}
let curr = '';
let escaped = false;
for (const ch of input) {
if (escaped) {
curr += ch;
escaped = false;
continue;
}
switch (ch) {
case '\\':
escaped = true;
continue;
case ',':
case '\n': {
const val = curr.trim();
if (val) {
list.push(val);
}
curr = '';
break;
}
default:
curr += ch;
}
}
const val = curr.trim();
if (val) {
list.push(val);
}
return list;
}
@ -544,7 +565,7 @@ exports.toBase64 = toBase64;
*/
function fromBase64(s) {
const str = s.replace(/-/g, '+').replace(/_/g, '/');
while (str.length % 4)
while (s.length % 4)
s += '=';
return Buffer.from(str, 'base64').toString('utf8');
}
@ -557,6 +578,65 @@ function trimmedString(s) {
return s ? s.trim() : '';
}
exports.trimmedString = trimmedString;
/**
* parseDuration parses a user-supplied string duration with optional suffix and
* returns a number representing the number of seconds. It returns 0 when given
* the empty string.
*
* @param str Duration string
*/
function parseDuration(str) {
const given = (str || '').trim();
if (!given) {
return 0;
}
let total = 0;
let curr = '';
for (let i = 0; i < str.length; i++) {
const ch = str[i];
switch (ch) {
case ' ':
continue;
case ',':
continue;
case 's': {
total += +curr;
curr = '';
break;
}
case 'm': {
total += +curr * 60;
curr = '';
break;
}
case 'h': {
total += +curr * 60 * 60;
curr = '';
break;
}
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
curr += ch;
break;
default:
throw new SyntaxError(`Unsupported character "${ch}" at position ${i}`);
}
}
// Anything left over is seconds
if (curr) {
total += +curr;
}
return total;
}
exports.parseDuration = parseDuration;
/***/ }),

View File

@ -138,8 +138,8 @@ export class BaseClient {
accessToken: parsed['accessToken'],
expiration: parsed['expireTime'],
};
} catch (e) {
throw new Error(`Failed to generate Google Cloud access token for ${serviceAccount}: ${e}`);
} catch (err) {
throw new Error(`Failed to generate Google Cloud access token for ${serviceAccount}: ${err}`);
}
}
}

View File

@ -96,8 +96,8 @@ export class CredentialsJSONClient implements AuthClient {
const signature = signer.sign(this.#credentials['private_key']);
return message + '.' + toBase64(signature);
} catch (e) {
throw new Error(`Failed to sign auth token: ${e}`);
} catch (err) {
throw new Error(`Failed to sign auth token using ${this.getServiceAccount()}: ${err}`);
}
}

View File

@ -71,8 +71,8 @@ export class WorkloadIdentityClient implements AuthClient {
}
/**
* getAuthToken generates a Google Cloud federated token using the provided OIDC
* token and Workload Identity Provider.
* getAuthToken generates a Google Cloud federated token using the provided
* OIDC token and Workload Identity Provider.
*/
async getAuthToken(): Promise<string> {
const stsURL = new URL('https://sts.googleapis.com/v1/token');

View File

@ -62,19 +62,43 @@ export async function removeExportedCredentials(): Promise<string> {
* of trimmed strings.
*/
export function explodeStrings(input: string): Array<string> {
if (input == null || input.length === 0) {
if (!input || input.trim().length === 0) {
return [];
}
const list = new Array<string>();
for (const line of input.split(`\n`)) {
for (const piece of line.split(',')) {
const entry = piece.trim();
if (entry !== '') {
list.push(entry);
let curr = '';
let escaped = false;
for (const ch of input) {
if (escaped) {
curr += ch;
escaped = false;
continue;
}
switch (ch) {
case '\\':
escaped = true;
continue;
case ',':
case '\n': {
const val = curr.trim();
if (val) {
list.push(val);
}
curr = '';
break;
}
default:
curr += ch;
}
}
const val = curr.trim();
if (val) {
list.push(val);
}
return list;
}
@ -95,7 +119,7 @@ export function toBase64(s: string | Buffer): string {
*/
export function fromBase64(s: string): string {
const str = s.replace(/-/g, '+').replace(/_/g, '/');
while (str.length % 4) s += '=';
while (s.length % 4) s += '=';
return Buffer.from(str, 'base64').toString('utf8');
}
@ -103,6 +127,69 @@ export function fromBase64(s: string): string {
* trimmedString returns a string trimmed of whitespace. If the input string is
* null, then it returns the empty string.
*/
export function trimmedString(s: string): string {
export function trimmedString(s: string | undefined | null): string {
return s ? s.trim() : '';
}
/**
* parseDuration parses a user-supplied string duration with optional suffix and
* returns a number representing the number of seconds. It returns 0 when given
* the empty string.
*
* @param str Duration string
*/
export function parseDuration(str: string): number {
const given = (str || '').trim();
if (!given) {
return 0;
}
let total = 0;
let curr = '';
for (let i = 0; i < str.length; i++) {
const ch = str[i];
switch (ch) {
case ' ':
continue;
case ',':
continue;
case 's': {
total += +curr;
curr = '';
break;
}
case 'm': {
total += +curr * 60;
curr = '';
break;
}
case 'h': {
total += +curr * 60 * 60;
curr = '';
break;
}
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
curr += ch;
break;
default:
throw new SyntaxError(`Unsupported character "${ch}" at position ${i}`);
}
}
// Anything left over is seconds
if (curr) {
total += +curr;
}
return total;
}

View File

@ -4,12 +4,36 @@ import 'mocha';
import { expect } from 'chai';
import { tmpdir } from 'os';
import { existsSync } from 'fs';
import { existsSync, readFileSync } from 'fs';
import { removeExportedCredentials } from '../src/utils';
import { writeSecureFile } from '../src/utils';
import {
explodeStrings,
fromBase64,
parseDuration,
removeExportedCredentials,
toBase64,
trimmedString,
writeSecureFile,
} from '../src/utils';
describe('Utils', () => {
describe('#writeSecureFile', () => {
it('writes data to the file', async () => {
const tmp = tmpdir();
const filePath = await writeSecureFile(tmp, 'hi');
expect(existsSync(filePath)).to.be.true;
expect(readFileSync(filePath).toString('utf8')).to.eq('hi');
});
it('generates a random name', async () => {
const tmp = tmpdir();
const filePath1 = await writeSecureFile(tmp, 'hi');
const filePath2 = await writeSecureFile(tmp, 'bye');
expect(filePath1).to.not.eq(filePath2);
expect(filePath1).to.not.eq(filePath2);
});
});
describe('post', () => {
describe('#removeExportedCredentials', () => {
it('does nothing when GOOGLE_GHA_CREDS_PATH is unset', async () => {
delete process.env.GOOGLE_GHA_CREDS_PATH;
@ -32,4 +56,196 @@ describe('post', () => {
expect(pth).to.eq('');
});
});
describe('#explodeStrings', () => {
const cases = [
{
name: 'empty string',
input: '',
expected: [],
},
{
name: 'padded empty string',
input: ' ',
expected: [],
},
{
name: 'comma-separated',
input: 'hello , world , and goodbye',
expected: ['hello', 'world', 'and goodbye'],
},
{
name: 'newline-separated',
input: `
hello
world
and goodbye`,
expected: ['hello', 'world', 'and goodbye'],
},
{
name: 'comma and newline-separated',
input: `
hello,
world,
and goodbye,`,
expected: ['hello', 'world', 'and goodbye'],
},
{
name: 'comma-escaped',
input: 'hello , world , and\\, goodbye',
expected: ['hello', 'world', 'and, goodbye'],
},
];
cases.forEach((tc) => {
it(tc.name, async () => {
expect(explodeStrings(tc.input)).to.eql(tc.expected);
});
});
});
describe('#toBase64', () => {
const cases = [
{
name: 'empty string',
input: '',
expected: '',
},
{
name: 'empty buffer',
input: Buffer.from(''),
expected: '',
},
{
name: 'encodes string',
input: 'hello',
expected: 'aGVsbG8',
},
{
name: 'encodes buffer',
input: Buffer.from('hello'),
expected: 'aGVsbG8',
},
];
cases.forEach((tc) => {
it(tc.name, async () => {
expect(toBase64(tc.input)).to.eq(tc.expected);
});
});
});
describe('#fromBase64', () => {
const cases = [
{
name: 'decodes',
input: 'aGVsbG8',
expected: 'hello',
},
{
name: 'decodes padded',
input: 'aGVsbG8==',
expected: 'hello',
},
];
cases.forEach((tc) => {
it(tc.name, async () => {
expect(fromBase64(tc.input)).to.eq(tc.expected);
});
});
});
describe('#trimmedString', () => {
const cases = [
{
name: 'null',
input: null,
expected: '',
},
{
name: 'undefined',
input: undefined,
expected: '',
},
{
name: 'empty string',
input: '',
expected: '',
},
{
name: 'trims',
input: ' hello world ',
expected: 'hello world',
},
];
cases.forEach((tc) => {
it(tc.name, async () => {
expect(trimmedString(tc.input)).to.eq(tc.expected);
});
});
});
describe('#parseDuration', () => {
const cases = [
{
name: 'empty string',
input: '',
expected: 0,
},
{
name: 'unitless',
input: '149585',
expected: 149585,
},
{
name: 'with commas',
input: '149,585',
expected: 149585,
},
{
name: 'suffix seconds',
input: '149585s',
expected: 149585,
},
{
name: 'suffix minutes',
input: '25m',
expected: 1500,
},
{
name: 'suffix hours',
input: '12h',
expected: 43200,
},
{
name: 'suffix hours minutes seconds',
input: '12h10m55s',
expected: 43855,
},
{
name: 'commas and spaces',
input: '12h, 10m 55s',
expected: 43855,
},
{
name: 'invalid',
input: '12h blueberries',
error: 'Unsupported character "b" at position 4',
},
];
cases.forEach((tc) => {
it(tc.name, async () => {
if (tc.expected) {
expect(parseDuration(tc.input)).to.eq(tc.expected);
} else if (tc.error) {
expect(() => {
parseDuration(tc.input);
}).to.throw(tc.error);
}
});
});
});
});