Skip to content

Commit 549683c

Browse files
committed
Make it clearer what the expectations for isUsernamePassword are
1 parent 7a6ed56 commit 549683c

6 files changed

Lines changed: 149 additions & 20 deletions

File tree

lib/start-proxy-action.js

Lines changed: 8 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/json/testing-util.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
1+
import { ExecutionContext } from "ava";
2+
13
import * as json from ".";
24

5+
/**
6+
* Constructs an object based on `schema` for unit tests.
7+
* Assumes that all keys in `schema` have string values.
8+
*
9+
* @param includeOptional Whether to include optional properties.
10+
* @param schema The schema to base the object on.
11+
* @returns An object that satisfies `schema`.
12+
*/
313
export function makeFromSchema<S extends json.Schema>(
414
includeOptional: boolean,
515
schema: S,
@@ -13,3 +23,75 @@ export function makeFromSchema<S extends json.Schema>(
1323
}
1424
return result as json.FromSchema<S>;
1525
}
26+
27+
/** Options for `withSchemaMatrix`. */
28+
export interface SchemaMatrixOptions {
29+
/** Whether cases where the properties are entirely absent should be excluded. */
30+
excludeAbsent?: boolean;
31+
}
32+
33+
/**
34+
* Constructs a test matrix of possible objects for `schema`: all required properties
35+
* plus all permutations of possible states for the optional properties.
36+
*
37+
* @param schema The schema to construct a test matrix for.
38+
* @param body The test body to call with each value from the test matrix.
39+
*/
40+
export function withSchemaMatrix<S extends json.Schema>(
41+
t: ExecutionContext<any>,
42+
schema: S,
43+
opts: SchemaMatrixOptions,
44+
body: (value: json.FromSchema<S>) => void,
45+
): void {
46+
// Construct a base object that includes all required properties.
47+
const required = makeFromSchema(false, schema);
48+
49+
// Identify optional properties.
50+
const optionalKeys: Array<keyof S> = [];
51+
52+
for (const [key, validator] of Object.entries(schema)) {
53+
if (!validator.required) {
54+
optionalKeys.push(key);
55+
}
56+
}
57+
58+
const optionalValues = (key: keyof S) => [
59+
null,
60+
undefined,
61+
`value-for-${String(key)}`,
62+
];
63+
64+
// Constructs an array of test objects, starting with `required` and adding
65+
// acceptable values for any
66+
const permutations = (keys: Array<keyof S>) => {
67+
if (keys.length === 0) return [required];
68+
69+
const bases = permutations(keys.slice(1));
70+
const result: Array<json.FromSchema<S>> = [];
71+
72+
const optionalKey = keys[0];
73+
for (const base of bases) {
74+
if (!opts.excludeAbsent) {
75+
// Optional keys can be absent entirely.
76+
result.push(base);
77+
}
78+
79+
// Or be present and have one of the `optionalValues`.
80+
for (const optionalValue of optionalValues(optionalKey)) {
81+
result.push({ ...base, [optionalKey]: optionalValue });
82+
}
83+
}
84+
return result;
85+
};
86+
87+
// Call `body` for all test cases.
88+
const testCases = permutations(optionalKeys);
89+
for (const testCase of testCases) {
90+
try {
91+
body(testCase);
92+
} catch (err) {
93+
t.log(testCase);
94+
throw err;
95+
}
96+
}
97+
}

src/start-proxy.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -532,7 +532,7 @@ test(
532532
t.is(results[0].type, "git_server");
533533
t.is(results[0].host, "https://github.com/");
534534

535-
if (startProxyExports.isUsernamePassword(results[0])) {
535+
if (startProxyExports.hasUsernameAndPassword(results[0])) {
536536
t.assert(results[0].password?.startsWith("ghp_"));
537537
} else {
538538
t.fail("Expected a `UsernamePassword`-based credential.");
@@ -563,7 +563,7 @@ test(
563563
t.is(results[0].type, "git_server");
564564
t.is(results[0].host, "https://github.com/");
565565

566-
if (startProxyExports.isUsernamePassword(results[0])) {
566+
if (startProxyExports.hasUsernameAndPassword(results[0])) {
567567
t.assert(results[0].password?.startsWith("ghp_"));
568568
} else {
569569
t.fail("Expected a `UsernamePassword`-based credential.");

src/start-proxy.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ import {
2424
Address,
2525
Registry,
2626
Credential,
27-
isToken,
28-
isUsernamePassword,
27+
hasToken,
28+
hasUsernameAndPassword,
2929
hasUsername,
3030
RawCredential,
3131
} from "./start-proxy/types";
@@ -331,11 +331,11 @@ export function getCredentials(
331331
const noUsername =
332332
!hasUsername(authConfig) || !isDefined(authConfig.username);
333333
const passwordIsPAT =
334-
isUsernamePassword(authConfig) &&
334+
hasUsernameAndPassword(authConfig) &&
335335
isDefined(authConfig.password) &&
336336
isPAT(authConfig.password);
337337
const tokenIsPAT =
338-
isToken(authConfig) &&
338+
hasToken(authConfig) &&
339339
isDefined(authConfig.token) &&
340340
isPAT(authConfig.token);
341341

src/start-proxy/types.test.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import test from "ava";
22

3-
import { makeFromSchema } from "../json/testing-util";
3+
import { makeFromSchema, withSchemaMatrix } from "../json/testing-util";
44
import { setupTests } from "../testing-utils";
55

66
import * as types from "./types";
@@ -27,6 +27,38 @@ const validJFrogCredential: types.JFrogConfig = {
2727
"identity-mapping-name": "my-mapping",
2828
};
2929

30+
test("hasUsername", (t) => {
31+
// Reject the case where `username` is missing.
32+
t.false(types.hasUsername({}));
33+
34+
// Test all cases where `username` is present.
35+
withSchemaMatrix(
36+
t,
37+
types.usernameSchema,
38+
{ excludeAbsent: true },
39+
(value) => {
40+
t.true(types.hasUsername(value));
41+
},
42+
);
43+
});
44+
45+
test("hasUsernameAndPassword", (t) => {
46+
// Reject cases where `username` or `password` are missing.
47+
t.false(types.hasUsernameAndPassword({}));
48+
t.false(types.hasUsernameAndPassword({ username: "foo" }));
49+
t.false(types.hasUsernameAndPassword({ password: "foo" }));
50+
51+
// Test all cases where both `username` and `password` are present.
52+
withSchemaMatrix(
53+
t,
54+
types.usernamePasswordSchema,
55+
{ excludeAbsent: true },
56+
(value) => {
57+
t.true(types.hasUsernameAndPassword(value));
58+
},
59+
);
60+
});
61+
3062
test("credentialToStr - pretty-prints valid username+password configurations", (t) => {
3163
const secret = "password123";
3264
const credential: types.Credential = {

src/start-proxy/types.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@ export const usernameSchema = {
1818
/** Usernames may be present for both authentication with tokens or passwords. */
1919
export type Username = json.FromSchema<typeof usernameSchema>;
2020

21-
/** Decides whether `config` has a username. */
22-
export function hasUsername(
23-
config: UnvalidatedObject<unknown>,
24-
): config is Username {
21+
/**
22+
* Narrows `config` to `Username` if `config` has a `username` property.
23+
* Not used for validation. Assumes that `config` is already a validated `AuthConfig`.
24+
*/
25+
export function hasUsername(config: AuthConfig): config is Username {
2526
return "username" in config;
2627
}
2728

@@ -38,11 +39,14 @@ export const usernamePasswordSchema = {
3839
*/
3940
export type UsernamePassword = json.FromSchema<typeof usernamePasswordSchema>;
4041

41-
/** Decides whether `config` is based on a username and password. */
42-
export function isUsernamePassword(
42+
/**
43+
* Narrows `config` to `UsernamePassword` if it has a `username` and `password` property.
44+
* Not used for validation. Assumes that `config` is already a validated `AuthConfig`.
45+
*/
46+
export function hasUsernameAndPassword(
4347
config: AuthConfig,
4448
): config is UsernamePassword {
45-
return json.validateSchema(usernamePasswordSchema, config);
49+
return hasUsername(config) && "password" in config;
4650
}
4751

4852
/** A schema for credential objects for token-based authentication. */
@@ -58,6 +62,14 @@ export const tokenSchema = {
5862
*/
5963
export type Token = json.FromSchema<typeof tokenSchema>;
6064

65+
/**
66+
* Narrows `config` to `Token` if it has a `token` property.
67+
* Not used for validation. Assumes that `config` is already a validated `AuthConfig`.
68+
*/
69+
export function hasToken(config: AuthConfig): config is Token {
70+
return "token" in config;
71+
}
72+
6173
/** Decides whether `config` is token-based. */
6274
export function isToken(
6375
config: UnvalidatedObject<AuthConfig>,
@@ -205,7 +217,7 @@ export function credentialToStr(credential: Credential): string {
205217
isDefined(credential.password) ? "***" : undefined,
206218
);
207219
}
208-
if (isToken(credential)) {
220+
if (hasToken(credential)) {
209221
appendIfDefined("Token", isDefined(credential.token) ? "***" : undefined);
210222
}
211223

0 commit comments

Comments
 (0)