@sasjs/cli 6.2.7 → 6.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/build/commands/auth/authCommand.js +5 -3
- package/build/commands/auth/login.d.ts +9 -3
- package/build/commands/auth/login.js +40 -21
- package/build/commands/auth/spec/login.spec.js +103 -3
- package/build/commands/compile/internal/compileTestFile.d.ts +2 -0
- package/build/commands/compile/internal/compileTestFile.js +48 -60
- package/build/commands/compile/internal/spec/compileTestFile.spec.js +158 -0
- package/build/commands/help/help.js +1 -0
- package/build/types/index.d.ts +1 -1
- package/build/types/testing.d.ts +33 -0
- package/build/types/testing.js +1 -0
- package/build/utils/auth.d.ts +30 -0
- package/build/utils/auth.js +230 -0
- package/build/utils/config.js +2 -2
- package/build/utils/spec/sasjsLogin.spec.d.ts +1 -0
- package/build/utils/spec/sasjsLogin.spec.js +370 -0
- package/package.json +1 -1
|
@@ -51,6 +51,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
51
51
|
};
|
|
52
52
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
53
53
|
var __1 = require("../");
|
|
54
|
+
var compileTestFile_1 = require("../compileTestFile");
|
|
54
55
|
var utils_1 = require("@sasjs/utils");
|
|
55
56
|
var test_1 = require("../../../../utils/test");
|
|
56
57
|
var path_1 = __importDefault(require("path"));
|
|
@@ -391,6 +392,163 @@ describe('compileTestFile', function () {
|
|
|
391
392
|
}
|
|
392
393
|
});
|
|
393
394
|
}); });
|
|
395
|
+
it('should write the coverage report to the build folder', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
396
|
+
var report, _a, _b;
|
|
397
|
+
return __generator(this, function (_c) {
|
|
398
|
+
switch (_c.label) {
|
|
399
|
+
case 0: return [4 /*yield*/, (0, compile_1.compile)(target)];
|
|
400
|
+
case 1:
|
|
401
|
+
_c.sent();
|
|
402
|
+
_b = (_a = JSON).parse;
|
|
403
|
+
return [4 /*yield*/, (0, utils_1.readFile)(path_1.default.join(buildPath, compileTestFile_1.coverageFileName))];
|
|
404
|
+
case 2:
|
|
405
|
+
report = _b.apply(_a, [_c.sent()]);
|
|
406
|
+
expect(report.summary).toEqual({
|
|
407
|
+
services: { covered: 0, total: 4, percentage: 0 },
|
|
408
|
+
jobs: { covered: 0, total: 0, percentage: 0 },
|
|
409
|
+
macros: { covered: 0, total: 0, percentage: 0 },
|
|
410
|
+
overall: { covered: 0, total: 4, percentage: 0 }
|
|
411
|
+
});
|
|
412
|
+
// the same rows the console table lists, standalone tests included
|
|
413
|
+
expect(report.files[['services', 'common', 'appinit.sas'].join('/')]).toEqual({ Type: 'service', Coverage: 'not covered' });
|
|
414
|
+
expect(report.files[['tests', 'jobs', 'jobs', 'testJob.test.sas'].join('/')]).toEqual({ Type: 'test', Coverage: 'standalone' });
|
|
415
|
+
return [2 /*return*/];
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
}); });
|
|
419
|
+
it('should key the report by project-relative paths, never build machine paths', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
420
|
+
var macroPath, tableSpy, report, _a, _b, tableRows;
|
|
421
|
+
return __generator(this, function (_c) {
|
|
422
|
+
switch (_c.label) {
|
|
423
|
+
case 0:
|
|
424
|
+
macroPath = path_1.default.join(sasjsPath, 'macros', 'uncoveredmacro.sas');
|
|
425
|
+
tableSpy = jest
|
|
426
|
+
.spyOn(process.logger, 'table')
|
|
427
|
+
.mockImplementation(function () { return ''; });
|
|
428
|
+
return [4 /*yield*/, (0, utils_1.createFile)(macroPath, '%macro uncoveredmacro(); %mend uncoveredmacro;')];
|
|
429
|
+
case 1:
|
|
430
|
+
_c.sent();
|
|
431
|
+
_c.label = 2;
|
|
432
|
+
case 2:
|
|
433
|
+
_c.trys.push([2, , 5, 7]);
|
|
434
|
+
return [4 /*yield*/, (0, compile_1.compile)(target)];
|
|
435
|
+
case 3:
|
|
436
|
+
_c.sent();
|
|
437
|
+
_b = (_a = JSON).parse;
|
|
438
|
+
return [4 /*yield*/, (0, utils_1.readFile)(path_1.default.join(buildPath, compileTestFile_1.coverageFileName))];
|
|
439
|
+
case 4:
|
|
440
|
+
report = _b.apply(_a, [_c.sent()]);
|
|
441
|
+
// macros are keyed like services and jobs, in the report and in the
|
|
442
|
+
// console table alike - a build machine path in either would make the
|
|
443
|
+
// same project produce different output elsewhere.
|
|
444
|
+
expect(report.files['macros/uncoveredmacro.sas']).toEqual({
|
|
445
|
+
Type: 'macro',
|
|
446
|
+
Coverage: 'not covered'
|
|
447
|
+
});
|
|
448
|
+
expect(Object.keys(report.files).filter(function (key) { return path_1.default.isAbsolute(key); })).toEqual([]);
|
|
449
|
+
expect(report.summary.macros).toEqual({
|
|
450
|
+
covered: 0,
|
|
451
|
+
total: 1,
|
|
452
|
+
percentage: 0
|
|
453
|
+
});
|
|
454
|
+
tableRows = tableSpy.mock.calls[0][0];
|
|
455
|
+
expect(tableRows).toContainEqual([
|
|
456
|
+
'macros/uncoveredmacro.sas',
|
|
457
|
+
'macro',
|
|
458
|
+
'not covered'
|
|
459
|
+
]);
|
|
460
|
+
expect(tableRows.flat().filter(function (cell) { return path_1.default.isAbsolute(cell); })).toEqual([]);
|
|
461
|
+
return [3 /*break*/, 7];
|
|
462
|
+
case 5: return [4 /*yield*/, (0, utils_1.deleteFile)(macroPath)];
|
|
463
|
+
case 6:
|
|
464
|
+
_c.sent();
|
|
465
|
+
return [7 /*endfinally*/];
|
|
466
|
+
case 7: return [2 /*return*/];
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
}); });
|
|
470
|
+
it('should mark covered files in the report, not just the gaps', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
471
|
+
var macroPath, macroTestPath, tableSpy, report, _a, _b, tableRows;
|
|
472
|
+
return __generator(this, function (_c) {
|
|
473
|
+
switch (_c.label) {
|
|
474
|
+
case 0:
|
|
475
|
+
macroPath = path_1.default.join(sasjsPath, 'macros', 'coveredmacro.sas');
|
|
476
|
+
macroTestPath = path_1.default.join(sasjsPath, 'macros', 'coveredmacro.test.sas');
|
|
477
|
+
tableSpy = jest
|
|
478
|
+
.spyOn(process.logger, 'table')
|
|
479
|
+
.mockImplementation(function () { return ''; });
|
|
480
|
+
return [4 /*yield*/, (0, utils_1.createFile)(macroPath, '%macro coveredmacro(); %mend coveredmacro;')];
|
|
481
|
+
case 1:
|
|
482
|
+
_c.sent();
|
|
483
|
+
return [4 /*yield*/, (0, utils_1.createFile)(macroTestPath, '%put testing coveredmacro;')];
|
|
484
|
+
case 2:
|
|
485
|
+
_c.sent();
|
|
486
|
+
_c.label = 3;
|
|
487
|
+
case 3:
|
|
488
|
+
_c.trys.push([3, , 6, 9]);
|
|
489
|
+
return [4 /*yield*/, (0, compile_1.compile)(target)];
|
|
490
|
+
case 4:
|
|
491
|
+
_c.sent();
|
|
492
|
+
_b = (_a = JSON).parse;
|
|
493
|
+
return [4 /*yield*/, (0, utils_1.readFile)(path_1.default.join(buildPath, compileTestFile_1.coverageFileName))];
|
|
494
|
+
case 5:
|
|
495
|
+
report = _b.apply(_a, [_c.sent()]);
|
|
496
|
+
expect(report.files['macros/coveredmacro.sas']).toEqual({
|
|
497
|
+
Type: 'macro',
|
|
498
|
+
Coverage: 'covered'
|
|
499
|
+
});
|
|
500
|
+
expect(report.summary.macros).toEqual({
|
|
501
|
+
covered: 1,
|
|
502
|
+
total: 1,
|
|
503
|
+
percentage: 100
|
|
504
|
+
});
|
|
505
|
+
tableRows = tableSpy.mock.calls[0][0];
|
|
506
|
+
expect(tableRows).toContainEqual([
|
|
507
|
+
'macros/coveredmacro.sas',
|
|
508
|
+
'macro',
|
|
509
|
+
'covered'
|
|
510
|
+
]);
|
|
511
|
+
return [3 /*break*/, 9];
|
|
512
|
+
case 6: return [4 /*yield*/, (0, utils_1.deleteFile)(macroPath)];
|
|
513
|
+
case 7:
|
|
514
|
+
_c.sent();
|
|
515
|
+
return [4 /*yield*/, (0, utils_1.deleteFile)(macroTestPath)];
|
|
516
|
+
case 8:
|
|
517
|
+
_c.sent();
|
|
518
|
+
return [7 /*endfinally*/];
|
|
519
|
+
case 9: return [2 /*return*/];
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
}); });
|
|
523
|
+
it('should not write a coverage report when no tests were compiled', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
524
|
+
var coveragePath, testFlow;
|
|
525
|
+
return __generator(this, function (_a) {
|
|
526
|
+
switch (_a.label) {
|
|
527
|
+
case 0: return [4 /*yield*/, (0, compile_1.compile)(target)];
|
|
528
|
+
case 1:
|
|
529
|
+
_a.sent();
|
|
530
|
+
coveragePath = path_1.default.join(buildPath, compileTestFile_1.coverageFileName);
|
|
531
|
+
// remove the report the compile wrote, so the assertion below is about
|
|
532
|
+
// the no-tests path and not about a file left over from it
|
|
533
|
+
return [4 /*yield*/, (0, utils_1.deleteFile)(coveragePath)];
|
|
534
|
+
case 2:
|
|
535
|
+
// remove the report the compile wrote, so the assertion below is about
|
|
536
|
+
// the no-tests path and not about a file left over from it
|
|
537
|
+
_a.sent();
|
|
538
|
+
return [4 /*yield*/, (0, utils_1.deleteFolder)(path_1.default.join(buildPath, 'tests'))];
|
|
539
|
+
case 3:
|
|
540
|
+
_a.sent();
|
|
541
|
+
return [4 /*yield*/, (0, __1.compileTestFlow)(target)];
|
|
542
|
+
case 4:
|
|
543
|
+
testFlow = _a.sent();
|
|
544
|
+
expect(testFlow).toBeUndefined();
|
|
545
|
+
return [4 /*yield*/, expect((0, utils_1.fileExists)(coveragePath)).resolves.toEqual(false)];
|
|
546
|
+
case 5:
|
|
547
|
+
_a.sent();
|
|
548
|
+
return [2 /*return*/];
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
}); });
|
|
394
552
|
it('should log a warning is testFolders was used in root or target configuration', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
395
553
|
var testTarget, testConfig, expectedWarn;
|
|
396
554
|
return __generator(this, function (_a) {
|
|
@@ -97,6 +97,7 @@ function printHelpText() {
|
|
|
97
97
|
"compiles the services specified in the ".concat(chalk_1.default.cyanBright('serviceFolders'), " property of the 'serviceConfig' in your target and common configuration."),
|
|
98
98
|
"A single file is compiled by naming its type as a sub command - ".concat(chalk_1.default.cyanBright('job'), ", ").concat(chalk_1.default.cyanBright('service'), ", ").concat(chalk_1.default.cyanBright('test'), " or ").concat(chalk_1.default.cyanBright('identify'), ", which works the type out from the file itself - together with ").concat(chalk_1.default.cyanBright('--source'), " (or ").concat(chalk_1.default.cyanBright('-s'), ")."),
|
|
99
99
|
"The compiled file is written to the ".concat(chalk_1.default.cyanBright('jobs'), " or ").concat(chalk_1.default.cyanBright('services'), " folder of sasjsbuild for its type, or below ").concat(chalk_1.default.cyanBright('--output'), " (or ").concat(chalk_1.default.cyanBright('-o'), ") when given."),
|
|
100
|
+
"The test coverage report is written to ".concat(chalk_1.default.cyanBright('coverage.json'), " in the build folder alongside ").concat(chalk_1.default.cyanBright('testFlow.json'), "."),
|
|
100
101
|
"command example: sasjs compile identify --source sasjs/jobs/daily/load.sas --target myTarget"
|
|
101
102
|
]
|
|
102
103
|
},
|
package/build/types/index.d.ts
CHANGED
|
@@ -2,6 +2,6 @@ export { CommandBase } from './command/commandBase';
|
|
|
2
2
|
export { File, FilePath } from './file';
|
|
3
3
|
export { Folder } from './folder';
|
|
4
4
|
export { TargetScope } from './targetScope';
|
|
5
|
-
export { TestFlow, Coverage, CoverageType, CoverageState, TestResults, TestResultStatus, TestDescription, TestResult, TestResultDescription, TestResultCsv } from './testing';
|
|
5
|
+
export { TestFlow, Coverage, CoverageReport, CoverageSummary, CoverageType, CoverageState, TestResults, TestResultStatus, TestDescription, TestResult, TestResultDescription, TestResultCsv } from './testing';
|
|
6
6
|
export { Flow, FlowWave, FlowWaveJob } from './flow';
|
|
7
7
|
export * from './log';
|
package/build/types/testing.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export declare enum CoverageType {
|
|
|
12
12
|
}
|
|
13
13
|
export declare enum CoverageState {
|
|
14
14
|
notCovered = "not covered",
|
|
15
|
+
covered = "covered",
|
|
15
16
|
standalone = "standalone"
|
|
16
17
|
}
|
|
17
18
|
export interface Coverage {
|
|
@@ -20,6 +21,38 @@ export interface Coverage {
|
|
|
20
21
|
Coverage: CoverageState;
|
|
21
22
|
};
|
|
22
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Totals for one group of files (services, jobs, macros or all of them).
|
|
26
|
+
* `percentage` is rounded to a whole number, matching the figure printed in
|
|
27
|
+
* the console summary.
|
|
28
|
+
*/
|
|
29
|
+
export interface CoverageSummary {
|
|
30
|
+
covered: number;
|
|
31
|
+
total: number;
|
|
32
|
+
percentage: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The content of the coverage report `sasjs compile` writes to
|
|
36
|
+
* `coverage.json` in the build folder.
|
|
37
|
+
*
|
|
38
|
+
* `summary` carries the same totals as the console output. `files` is a
|
|
39
|
+
* complete per-file map - every service, job and macro the compile found,
|
|
40
|
+
* each marked `covered` or `not covered`, plus standalone tests - keyed by
|
|
41
|
+
* project-relative path (`services/common/appinit.sas`,
|
|
42
|
+
* `macros/mymacro.sas`), never by a path from the machine that compiled it.
|
|
43
|
+
*
|
|
44
|
+
* The console table renders this same map, so the rows a reader sees and the
|
|
45
|
+
* keys a consumer reads are the same, covered rows included.
|
|
46
|
+
*/
|
|
47
|
+
export interface CoverageReport {
|
|
48
|
+
summary: {
|
|
49
|
+
services: CoverageSummary;
|
|
50
|
+
jobs: CoverageSummary;
|
|
51
|
+
macros: CoverageSummary;
|
|
52
|
+
overall: CoverageSummary;
|
|
53
|
+
};
|
|
54
|
+
files: Coverage;
|
|
55
|
+
}
|
|
23
56
|
export interface TestResultDescription {
|
|
24
57
|
TEST_DESCRIPTION: string;
|
|
25
58
|
TEST_RESULT: TestResultStatus.pass | TestResultStatus.fail;
|
package/build/types/testing.js
CHANGED
|
@@ -11,6 +11,7 @@ var CoverageType;
|
|
|
11
11
|
var CoverageState;
|
|
12
12
|
(function (CoverageState) {
|
|
13
13
|
CoverageState["notCovered"] = "not covered";
|
|
14
|
+
CoverageState["covered"] = "covered";
|
|
14
15
|
CoverageState["standalone"] = "standalone";
|
|
15
16
|
})(CoverageState || (exports.CoverageState = CoverageState = {}));
|
|
16
17
|
var TestResultStatus;
|
package/build/utils/auth.d.ts
CHANGED
|
@@ -59,3 +59,33 @@ export declare function getNewAccessToken(sasjsInstance: SASjs, clientId: string
|
|
|
59
59
|
access_token: string;
|
|
60
60
|
refresh_token: string;
|
|
61
61
|
}>;
|
|
62
|
+
/**
|
|
63
|
+
* Mints a token pair from the local login of a SASjs Server target.
|
|
64
|
+
*
|
|
65
|
+
* SASjs Server has no secret-less OAuth client and no password grant: the
|
|
66
|
+
* tokens it issues come from its own sign-in flow, the one its web app uses.
|
|
67
|
+
* Reproducing that flow here is what makes `sasjs auth login` usable on a SASjs
|
|
68
|
+
* target from CI, where there is no browser to visit /SASjsLogon and no
|
|
69
|
+
* administrator-registered client secret to fall back on:
|
|
70
|
+
*
|
|
71
|
+
* GET / sets the XSRF-TOKEN cookie
|
|
72
|
+
* POST /SASLogon/login creates the session
|
|
73
|
+
* POST /SASLogon/authorize returns a single-use authorization code
|
|
74
|
+
* POST /SASjsApi/auth/token exchanges it for an access/refresh pair
|
|
75
|
+
*
|
|
76
|
+
* @param {Target} target - the SASJS target to authenticate against.
|
|
77
|
+
* @param {string} user - the SASjs Server username.
|
|
78
|
+
* @param {string} pass - the SASjs Server password.
|
|
79
|
+
* @param {string} [clientId] - the registered client to authorize, usually from
|
|
80
|
+
* the SASJS_CLIENT environment variable. When absent, the target's configured
|
|
81
|
+
* client is used, and failing that the first registered client if the account
|
|
82
|
+
* is an administrator.
|
|
83
|
+
* @returns the token pair, the client that was authorized, and the display name
|
|
84
|
+
* the server reports for the user.
|
|
85
|
+
*/
|
|
86
|
+
export declare function getTokensWithSasjsLogin(target: Target, user: string, pass: string, clientId?: string): Promise<{
|
|
87
|
+
access_token: string;
|
|
88
|
+
refresh_token: string;
|
|
89
|
+
client: string;
|
|
90
|
+
displayName?: string;
|
|
91
|
+
}>;
|
package/build/utils/auth.js
CHANGED
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
36
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
37
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
@@ -35,6 +68,34 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
|
35
68
|
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
36
69
|
}
|
|
37
70
|
};
|
|
71
|
+
var __read = (this && this.__read) || function (o, n) {
|
|
72
|
+
var m = typeof Symbol === "function" && o[Symbol.iterator];
|
|
73
|
+
if (!m) return o;
|
|
74
|
+
var i = m.call(o), r, ar = [], e;
|
|
75
|
+
try {
|
|
76
|
+
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
|
|
77
|
+
}
|
|
78
|
+
catch (error) { e = { error: error }; }
|
|
79
|
+
finally {
|
|
80
|
+
try {
|
|
81
|
+
if (r && !r.done && (m = i["return"])) m.call(i);
|
|
82
|
+
}
|
|
83
|
+
finally { if (e) throw e.error; }
|
|
84
|
+
}
|
|
85
|
+
return ar;
|
|
86
|
+
};
|
|
87
|
+
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
88
|
+
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
|
|
89
|
+
if (ar || !(i in from)) {
|
|
90
|
+
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
|
|
91
|
+
ar[i] = from[i];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return to.concat(ar || Array.prototype.slice.call(from));
|
|
95
|
+
};
|
|
96
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
97
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
98
|
+
};
|
|
38
99
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
100
|
exports.SAS_CLI_CLIENT_ID = exports.getAuthUrl = void 0;
|
|
40
101
|
exports.getAuthCode = getAuthCode;
|
|
@@ -42,6 +103,9 @@ exports.refreshTokens = refreshTokens;
|
|
|
42
103
|
exports.getTokensWithPasswordGrant = getTokensWithPasswordGrant;
|
|
43
104
|
exports.fetchLoggedInUser = fetchLoggedInUser;
|
|
44
105
|
exports.getNewAccessToken = getNewAccessToken;
|
|
106
|
+
exports.getTokensWithSasjsLogin = getTokensWithSasjsLogin;
|
|
107
|
+
var axios_1 = __importDefault(require("axios"));
|
|
108
|
+
var https = __importStar(require("https"));
|
|
45
109
|
var utils_1 = require("@sasjs/utils");
|
|
46
110
|
var node_1 = require("@sasjs/adapter/node");
|
|
47
111
|
/**
|
|
@@ -217,3 +281,169 @@ function getNewAccessToken(sasjsInstance, clientId, clientSecret, target) {
|
|
|
217
281
|
});
|
|
218
282
|
});
|
|
219
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* Mints a token pair from the local login of a SASjs Server target.
|
|
286
|
+
*
|
|
287
|
+
* SASjs Server has no secret-less OAuth client and no password grant: the
|
|
288
|
+
* tokens it issues come from its own sign-in flow, the one its web app uses.
|
|
289
|
+
* Reproducing that flow here is what makes `sasjs auth login` usable on a SASjs
|
|
290
|
+
* target from CI, where there is no browser to visit /SASjsLogon and no
|
|
291
|
+
* administrator-registered client secret to fall back on:
|
|
292
|
+
*
|
|
293
|
+
* GET / sets the XSRF-TOKEN cookie
|
|
294
|
+
* POST /SASLogon/login creates the session
|
|
295
|
+
* POST /SASLogon/authorize returns a single-use authorization code
|
|
296
|
+
* POST /SASjsApi/auth/token exchanges it for an access/refresh pair
|
|
297
|
+
*
|
|
298
|
+
* @param {Target} target - the SASJS target to authenticate against.
|
|
299
|
+
* @param {string} user - the SASjs Server username.
|
|
300
|
+
* @param {string} pass - the SASjs Server password.
|
|
301
|
+
* @param {string} [clientId] - the registered client to authorize, usually from
|
|
302
|
+
* the SASJS_CLIENT environment variable. When absent, the target's configured
|
|
303
|
+
* client is used, and failing that the first registered client if the account
|
|
304
|
+
* is an administrator.
|
|
305
|
+
* @returns the token pair, the client that was authorized, and the display name
|
|
306
|
+
* the server reports for the user.
|
|
307
|
+
*/
|
|
308
|
+
function getTokensWithSasjsLogin(target, user, pass, clientId) {
|
|
309
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
310
|
+
var baseUrl, cookies, safeDecode, collectCookies, instance, call, fail, login, client, clients, authorized, code, tokens;
|
|
311
|
+
var _this = this;
|
|
312
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
313
|
+
return __generator(this, function (_k) {
|
|
314
|
+
switch (_k.label) {
|
|
315
|
+
case 0:
|
|
316
|
+
baseUrl = target.serverUrl.replace(/\/+$/, '');
|
|
317
|
+
cookies = new Map();
|
|
318
|
+
safeDecode = function (value) {
|
|
319
|
+
try {
|
|
320
|
+
return decodeURIComponent(value);
|
|
321
|
+
}
|
|
322
|
+
catch (_a) {
|
|
323
|
+
return value;
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
collectCookies = function (setCookie) {
|
|
327
|
+
var entries = Array.isArray(setCookie)
|
|
328
|
+
? setCookie
|
|
329
|
+
: setCookie
|
|
330
|
+
? [setCookie]
|
|
331
|
+
: [];
|
|
332
|
+
entries.forEach(function (entry) {
|
|
333
|
+
var pair = entry.split(';')[0];
|
|
334
|
+
var index = pair.indexOf('=');
|
|
335
|
+
if (index > 0) {
|
|
336
|
+
cookies.set(pair.slice(0, index).trim(), pair.slice(index + 1).trim());
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
};
|
|
340
|
+
instance = axios_1.default.create({
|
|
341
|
+
baseURL: baseUrl,
|
|
342
|
+
validateStatus: function () { return true; },
|
|
343
|
+
// The login request carries the password in a body, so a redirect must not
|
|
344
|
+
// replay it to wherever 'Location' points. A redirect is a configuration
|
|
345
|
+
// problem (proxy, SSO front end) - report it instead of following it.
|
|
346
|
+
maxRedirects: 0,
|
|
347
|
+
httpsAgent: target.httpsAgentOptions
|
|
348
|
+
? new https.Agent(target.httpsAgentOptions)
|
|
349
|
+
: undefined,
|
|
350
|
+
headers: { Accept: 'application/json' }
|
|
351
|
+
});
|
|
352
|
+
call = function (method, path, data) { return __awaiter(_this, void 0, void 0, function () {
|
|
353
|
+
var headers, xsrf, response;
|
|
354
|
+
return __generator(this, function (_a) {
|
|
355
|
+
switch (_a.label) {
|
|
356
|
+
case 0:
|
|
357
|
+
headers = {};
|
|
358
|
+
if (cookies.size) {
|
|
359
|
+
headers.Cookie = __spreadArray([], __read(cookies), false).map(function (_a) {
|
|
360
|
+
var _b = __read(_a, 2), name = _b[0], value = _b[1];
|
|
361
|
+
return "".concat(name, "=").concat(value);
|
|
362
|
+
})
|
|
363
|
+
.join('; ');
|
|
364
|
+
}
|
|
365
|
+
xsrf = cookies.get('XSRF-TOKEN');
|
|
366
|
+
if (xsrf)
|
|
367
|
+
headers['X-XSRF-TOKEN'] = safeDecode(xsrf);
|
|
368
|
+
return [4 /*yield*/, instance.request({
|
|
369
|
+
method: method,
|
|
370
|
+
url: path,
|
|
371
|
+
data: data,
|
|
372
|
+
headers: headers
|
|
373
|
+
})];
|
|
374
|
+
case 1:
|
|
375
|
+
response = _a.sent();
|
|
376
|
+
if (response.status >= 300 && response.status < 400) {
|
|
377
|
+
throw new Error("".concat(baseUrl).concat(path, " answered with a ").concat(response.status, " redirect to ") +
|
|
378
|
+
"'".concat(response.headers.location || 'unknown', "'. It is not followed: ") +
|
|
379
|
+
"the sign-in request carries the password. Point the target at the " +
|
|
380
|
+
"final URL.");
|
|
381
|
+
}
|
|
382
|
+
collectCookies(response.headers['set-cookie']);
|
|
383
|
+
return [2 /*return*/, response];
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
}); };
|
|
387
|
+
fail = function (message) {
|
|
388
|
+
return new Error("".concat(message, "\nCheck that '").concat(user, "' can sign in to ").concat(target.serverUrl, ", and that ") +
|
|
389
|
+
"the client id is set (SASJS_CLIENT) if the account is not an administrator.");
|
|
390
|
+
};
|
|
391
|
+
// GET / serves the single page app but sets the XSRF cookie the login needs.
|
|
392
|
+
return [4 /*yield*/, call('get', '/')];
|
|
393
|
+
case 1:
|
|
394
|
+
// GET / serves the single page app but sets the XSRF cookie the login needs.
|
|
395
|
+
_k.sent();
|
|
396
|
+
return [4 /*yield*/, call('post', '/SASLogon/login', {
|
|
397
|
+
username: user,
|
|
398
|
+
password: pass
|
|
399
|
+
})];
|
|
400
|
+
case 2:
|
|
401
|
+
login = _k.sent();
|
|
402
|
+
if (login.status !== 200 || !((_a = login.data) === null || _a === void 0 ? void 0 : _a.loggedIn)) {
|
|
403
|
+
throw fail("Login failed for user '".concat(user, "' on ").concat(target.serverUrl, " (HTTP ").concat(login.status, ")."));
|
|
404
|
+
}
|
|
405
|
+
client = clientId || ((_b = target.authConfig) === null || _b === void 0 ? void 0 : _b.client) || '';
|
|
406
|
+
if (!!client) return [3 /*break*/, 4];
|
|
407
|
+
return [4 /*yield*/, call('get', '/SASjsApi/client')];
|
|
408
|
+
case 3:
|
|
409
|
+
clients = _k.sent();
|
|
410
|
+
if (Array.isArray(clients.data) && clients.data.length) {
|
|
411
|
+
client = clients.data[0].clientId;
|
|
412
|
+
// Say which one was picked: the listing order is the server's, and a
|
|
413
|
+
// silent choice here is impossible to debug from the tokens alone.
|
|
414
|
+
(_c = process.logger) === null || _c === void 0 ? void 0 : _c.info("Using the first registered client ('".concat(client, "'). Set SASJS_CLIENT ") +
|
|
415
|
+
"to authorize a specific client.");
|
|
416
|
+
}
|
|
417
|
+
_k.label = 4;
|
|
418
|
+
case 4:
|
|
419
|
+
if (!client) {
|
|
420
|
+
throw fail('No client id is available to authorize.');
|
|
421
|
+
}
|
|
422
|
+
return [4 /*yield*/, call('post', '/SASLogon/authorize', {
|
|
423
|
+
clientId: client
|
|
424
|
+
})];
|
|
425
|
+
case 5:
|
|
426
|
+
authorized = _k.sent();
|
|
427
|
+
code = (_d = authorized.data) === null || _d === void 0 ? void 0 : _d.code;
|
|
428
|
+
if (!code) {
|
|
429
|
+
throw fail("Authorization failed for client '".concat(client, "' (HTTP ").concat(authorized.status, ")."));
|
|
430
|
+
}
|
|
431
|
+
return [4 /*yield*/, call('post', '/SASjsApi/auth/token', {
|
|
432
|
+
clientId: client,
|
|
433
|
+
code: code
|
|
434
|
+
})];
|
|
435
|
+
case 6:
|
|
436
|
+
tokens = _k.sent();
|
|
437
|
+
if (!((_e = tokens.data) === null || _e === void 0 ? void 0 : _e.accessToken)) {
|
|
438
|
+
throw fail("The token endpoint returned no access token (HTTP ".concat(tokens.status, ")."));
|
|
439
|
+
}
|
|
440
|
+
return [2 /*return*/, {
|
|
441
|
+
access_token: tokens.data.accessToken,
|
|
442
|
+
refresh_token: tokens.data.refreshToken || '',
|
|
443
|
+
client: client,
|
|
444
|
+
displayName: ((_g = (_f = login.data) === null || _f === void 0 ? void 0 : _f.user) === null || _g === void 0 ? void 0 : _g.displayName) || ((_j = (_h = login.data) === null || _h === void 0 ? void 0 : _h.user) === null || _j === void 0 ? void 0 : _j.username)
|
|
445
|
+
}];
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
}
|
package/build/utils/config.js
CHANGED
|
@@ -968,7 +968,7 @@ var saveTokens = function (targetName, access_token, refresh_token, client, secr
|
|
|
968
968
|
if (!isLocalTarget) return [3 /*break*/, 3];
|
|
969
969
|
VERBOSE = process.env.VERBOSE;
|
|
970
970
|
clientSecretContent = client
|
|
971
|
-
? "CLIENT=".concat(client, "\
|
|
971
|
+
? "CLIENT=".concat(client, "\n").concat(secret ? "SECRET=".concat(secret, "\n") : '')
|
|
972
972
|
: '';
|
|
973
973
|
envFileContent = "".concat(clientSecretContent, "ACCESS_TOKEN=").concat(access_token, "\nREFRESH_TOKEN=").concat(refresh_token, "\n").concat(VERBOSE ? 'VERBOSE=' + VERBOSE + '\n' : '');
|
|
974
974
|
envFilePath = path_1.default.join(process.projectDir, ".env.".concat(targetName));
|
|
@@ -986,7 +986,7 @@ var saveTokens = function (targetName, access_token, refresh_token, client, secr
|
|
|
986
986
|
}
|
|
987
987
|
targetJson = __assign({}, target);
|
|
988
988
|
targetJson.authConfig = __assign(__assign(__assign({}, (targetJson.authConfig || {})), (client
|
|
989
|
-
? { client: client, secret: secret ||
|
|
989
|
+
? { client: client, secret: secret || undefined }
|
|
990
990
|
: { client: undefined, secret: undefined })), { access_token: access_token, refresh_token: refresh_token });
|
|
991
991
|
return [4 /*yield*/, saveToGlobalConfig(new utils_1.Target(targetJson), globalConfig.defaultTarget === targetName)];
|
|
992
992
|
case 5:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|