@sasjs/cli 4.18.5 → 4.19.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/build/commands/add/spec/addCommand.spec.js +0 -21
- package/build/commands/auth/authCommand.d.ts +9 -0
- package/build/commands/auth/authCommand.js +181 -0
- package/build/commands/auth/login.d.ts +16 -0
- package/build/commands/auth/login.js +130 -0
- package/build/commands/auth/spec/authCommand.spec.d.ts +1 -0
- package/build/commands/auth/spec/authCommand.spec.js +207 -0
- package/build/commands/auth/spec/login.spec.d.ts +1 -0
- package/build/commands/auth/spec/login.spec.js +364 -0
- package/build/commands/build/build.js +9 -2
- package/build/commands/build/spec/build.spec.js +3 -0
- package/build/commands/deploy/internal/executeDeployScriptSasViya.js +2 -1
- package/build/commands/fs/internal/executeCode.js +2 -1
- package/build/commands/help/help.js +3 -2
- package/build/commands/run/run.js +2 -1
- package/build/types/command/commandAliases.js +0 -1
- package/build/types/command/commandBase.js +1 -0
- package/build/types/command/commandFactory.js +2 -0
- package/build/utils/auth.d.ts +35 -16
- package/build/utils/auth.js +97 -37
- package/build/utils/config.d.ts +13 -2
- package/build/utils/config.js +125 -55
- package/build/utils/spec/auth.spec.d.ts +1 -0
- package/build/utils/spec/auth.spec.js +255 -0
- package/build/utils/spec/config.spec.js +334 -32
- package/package.json +4 -3
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __assign = (this && this.__assign) || function () {
|
|
3
|
+
__assign = Object.assign || function(t) {
|
|
4
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
5
|
+
s = arguments[i];
|
|
6
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
7
|
+
t[p] = s[p];
|
|
8
|
+
}
|
|
9
|
+
return t;
|
|
10
|
+
};
|
|
11
|
+
return __assign.apply(this, arguments);
|
|
12
|
+
};
|
|
13
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
16
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
17
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
18
|
+
}
|
|
19
|
+
Object.defineProperty(o, k2, desc);
|
|
20
|
+
}) : (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
o[k2] = m[k];
|
|
23
|
+
}));
|
|
24
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
25
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
26
|
+
}) : function(o, v) {
|
|
27
|
+
o["default"] = v;
|
|
28
|
+
});
|
|
29
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
30
|
+
if (mod && mod.__esModule) return mod;
|
|
31
|
+
var result = {};
|
|
32
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
33
|
+
__setModuleDefault(result, mod);
|
|
34
|
+
return result;
|
|
35
|
+
};
|
|
36
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
37
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
38
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
39
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
40
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
41
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
42
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
46
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
|
47
|
+
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
48
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
49
|
+
function step(op) {
|
|
50
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
51
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
52
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
53
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
54
|
+
switch (op[0]) {
|
|
55
|
+
case 0: case 1: t = op; break;
|
|
56
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
57
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
58
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
59
|
+
default:
|
|
60
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
61
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
62
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
63
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
64
|
+
if (t[2]) _.ops.pop();
|
|
65
|
+
_.trys.pop(); continue;
|
|
66
|
+
}
|
|
67
|
+
op = body.call(thisArg, _);
|
|
68
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
69
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
73
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
74
|
+
};
|
|
75
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
76
|
+
var prompts_1 = __importDefault(require("prompts"));
|
|
77
|
+
var utils_1 = require("@sasjs/utils");
|
|
78
|
+
var utilsModule = __importStar(require("../../../utils"));
|
|
79
|
+
var authLoginModule = __importStar(require("../login"));
|
|
80
|
+
// Mock prompts so the password prompt returns immediately without blocking.
|
|
81
|
+
jest.mock('prompts', function () { return ({
|
|
82
|
+
__esModule: true,
|
|
83
|
+
default: jest.fn(function () { return Promise.resolve({ pass: 'test-pass' }); })
|
|
84
|
+
}); });
|
|
85
|
+
// Mock getString from @sasjs/utils so the username prompt doesn't block.
|
|
86
|
+
// We keep the rest of @sasjs/utils real (ServerType, Target, Logger, etc.).
|
|
87
|
+
jest.mock('@sasjs/utils', function () {
|
|
88
|
+
var actual = jest.requireActual('@sasjs/utils');
|
|
89
|
+
return __assign(__assign({}, actual), { getString: jest.fn(function () { return Promise.resolve('test-user'); }) });
|
|
90
|
+
});
|
|
91
|
+
// Re-import after mock so login.ts picks up the mocked getString.
|
|
92
|
+
var utils_2 = require("@sasjs/utils");
|
|
93
|
+
// Mock the three orchestration dependencies that authLogin delegates to.
|
|
94
|
+
// We spy on the utils barrel (../../utils) which re-exports from auth.ts and
|
|
95
|
+
// config.ts, so the spies are the same references login.ts resolves at runtime.
|
|
96
|
+
jest.mock('../../../utils', function () { return (__assign(__assign({}, jest.requireActual('../../../utils')), { getTokensWithPasswordGrant: jest.fn(), fetchLoggedInUser: jest.fn(), saveTokens: jest.fn() })); });
|
|
97
|
+
var viyaTarget = new utils_1.Target({
|
|
98
|
+
name: 'viya-test',
|
|
99
|
+
serverType: utils_1.ServerType.SasViya,
|
|
100
|
+
serverUrl: 'https://viya.example.com',
|
|
101
|
+
appLoc: '/Public/test',
|
|
102
|
+
contextName: 'test context'
|
|
103
|
+
});
|
|
104
|
+
var sas9Target = new utils_1.Target({
|
|
105
|
+
name: 'sas9-test',
|
|
106
|
+
serverType: utils_1.ServerType.Sas9,
|
|
107
|
+
serverUrl: 'https://sas9.example.com',
|
|
108
|
+
appLoc: '/Public/test',
|
|
109
|
+
contextName: 'test context'
|
|
110
|
+
});
|
|
111
|
+
describe('authLogin', function () {
|
|
112
|
+
beforeEach(function () {
|
|
113
|
+
process.logger = new utils_1.Logger(utils_1.LogLevel.Off);
|
|
114
|
+
jest.clearAllMocks();
|
|
115
|
+
utils_2.getString.mockResolvedValue('test-user');
|
|
116
|
+
prompts_1.default.mockResolvedValue({
|
|
117
|
+
pass: 'test-pass'
|
|
118
|
+
});
|
|
119
|
+
utilsModule.getTokensWithPasswordGrant.mockResolvedValue({
|
|
120
|
+
access_token: 'access-123',
|
|
121
|
+
refresh_token: 'refresh-456'
|
|
122
|
+
});
|
|
123
|
+
utilsModule.fetchLoggedInUser.mockResolvedValue({
|
|
124
|
+
id: 'sastest',
|
|
125
|
+
name: 'SAS Test User'
|
|
126
|
+
});
|
|
127
|
+
utilsModule.saveTokens.mockResolvedValue(undefined);
|
|
128
|
+
});
|
|
129
|
+
afterAll(function () {
|
|
130
|
+
jest.restoreAllMocks();
|
|
131
|
+
});
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// Guard 1: Non-Viya rejection
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
it('should reject when target.serverType is not SASVIYA', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
136
|
+
return __generator(this, function (_a) {
|
|
137
|
+
switch (_a.label) {
|
|
138
|
+
case 0: return [4 /*yield*/, expect(authLoginModule.authLogin(sas9Target)).rejects.toThrow("'sasjs auth login' is only supported for SASVIYA targets.")];
|
|
139
|
+
case 1:
|
|
140
|
+
_a.sent();
|
|
141
|
+
expect(utilsModule.getTokensWithPasswordGrant).not.toHaveBeenCalled();
|
|
142
|
+
expect(utilsModule.saveTokens).not.toHaveBeenCalled();
|
|
143
|
+
return [2 /*return*/];
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}); });
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// Guard 2: Missing serverUrl
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
it('should reject when target.serverUrl is missing/empty', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
151
|
+
var noUrlTarget;
|
|
152
|
+
return __generator(this, function (_a) {
|
|
153
|
+
switch (_a.label) {
|
|
154
|
+
case 0:
|
|
155
|
+
noUrlTarget = new utils_1.Target({
|
|
156
|
+
name: 'no-url',
|
|
157
|
+
serverType: utils_1.ServerType.SasViya,
|
|
158
|
+
serverUrl: '',
|
|
159
|
+
appLoc: '/Public/test',
|
|
160
|
+
contextName: 'test context'
|
|
161
|
+
});
|
|
162
|
+
return [4 /*yield*/, expect(authLoginModule.authLogin(noUrlTarget)).rejects.toThrow("Target 'no-url' does not have a serverUrl configured.")];
|
|
163
|
+
case 1:
|
|
164
|
+
_a.sent();
|
|
165
|
+
expect(utilsModule.getTokensWithPasswordGrant).not.toHaveBeenCalled();
|
|
166
|
+
expect(utilsModule.saveTokens).not.toHaveBeenCalled();
|
|
167
|
+
return [2 /*return*/];
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}); });
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// Guard 3: --insecure httpsAgentOptions mutation
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
it('should set rejectUnauthorized: false on target.httpsAgentOptions when insecure is true', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
175
|
+
var callTarget;
|
|
176
|
+
return __generator(this, function (_a) {
|
|
177
|
+
switch (_a.label) {
|
|
178
|
+
case 0: return [4 /*yield*/, authLoginModule.authLogin(viyaTarget, true)];
|
|
179
|
+
case 1:
|
|
180
|
+
_a.sent();
|
|
181
|
+
callTarget = utilsModule.getTokensWithPasswordGrant
|
|
182
|
+
.mock.calls[0][0];
|
|
183
|
+
expect(callTarget.httpsAgentOptions).toMatchObject({
|
|
184
|
+
rejectUnauthorized: false,
|
|
185
|
+
allowInsecureRequests: true
|
|
186
|
+
});
|
|
187
|
+
return [2 /*return*/];
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}); });
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Guard 3b: insecure=false should NOT mutate httpsAgentOptions
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
it('should not mutate httpsAgentOptions when insecure is false', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
195
|
+
var callTarget;
|
|
196
|
+
var _a;
|
|
197
|
+
return __generator(this, function (_b) {
|
|
198
|
+
switch (_b.label) {
|
|
199
|
+
case 0: return [4 /*yield*/, authLoginModule.authLogin(viyaTarget, false)];
|
|
200
|
+
case 1:
|
|
201
|
+
_b.sent();
|
|
202
|
+
callTarget = utilsModule.getTokensWithPasswordGrant
|
|
203
|
+
.mock.calls[0][0];
|
|
204
|
+
// The original target has no rejectUnauthorized set at all.
|
|
205
|
+
expect((_a = callTarget.httpsAgentOptions) === null || _a === void 0 ? void 0 : _a.rejectUnauthorized).not.toBe(false);
|
|
206
|
+
return [2 /*return*/];
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}); });
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
// Guard 4: saveTokens called with correct args on successful login
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
it('should call saveTokens with the target name, access token, and refresh token after successful login', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
214
|
+
return __generator(this, function (_a) {
|
|
215
|
+
switch (_a.label) {
|
|
216
|
+
case 0: return [4 /*yield*/, authLoginModule.authLogin(viyaTarget, false)];
|
|
217
|
+
case 1:
|
|
218
|
+
_a.sent();
|
|
219
|
+
expect(utilsModule.saveTokens).toHaveBeenCalledWith(viyaTarget.name, 'access-123', 'refresh-456');
|
|
220
|
+
expect(utilsModule.saveTokens).toHaveBeenCalledTimes(1);
|
|
221
|
+
return [2 /*return*/];
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}); });
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
// Guard 4b: saveTokens should default refresh_token to '' when omitted
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
it('should call saveTokens with empty string refresh_token when the server omits it', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
229
|
+
return __generator(this, function (_a) {
|
|
230
|
+
switch (_a.label) {
|
|
231
|
+
case 0:
|
|
232
|
+
;
|
|
233
|
+
utilsModule.getTokensWithPasswordGrant.mockResolvedValue({
|
|
234
|
+
access_token: 'access-789',
|
|
235
|
+
refresh_token: ''
|
|
236
|
+
});
|
|
237
|
+
return [4 /*yield*/, authLoginModule.authLogin(viyaTarget, false)];
|
|
238
|
+
case 1:
|
|
239
|
+
_a.sent();
|
|
240
|
+
expect(utilsModule.saveTokens).toHaveBeenCalledWith(viyaTarget.name, 'access-789', '');
|
|
241
|
+
return [2 /*return*/];
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
}); });
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
// Guard 5: fetchLoggedInUser failure — tokens not saved, error propagated
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
it('should not save tokens and should propagate the error when fetchLoggedInUser fails', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
249
|
+
return __generator(this, function (_a) {
|
|
250
|
+
switch (_a.label) {
|
|
251
|
+
case 0:
|
|
252
|
+
;
|
|
253
|
+
utilsModule.fetchLoggedInUser.mockRejectedValue(new Error('Unable to verify the access token'));
|
|
254
|
+
return [4 /*yield*/, expect(authLoginModule.authLogin(viyaTarget, false)).rejects.toThrow('Unable to verify the access token')];
|
|
255
|
+
case 1:
|
|
256
|
+
_a.sent();
|
|
257
|
+
expect(utilsModule.getTokensWithPasswordGrant).toHaveBeenCalled();
|
|
258
|
+
expect(utilsModule.saveTokens).not.toHaveBeenCalled();
|
|
259
|
+
return [2 /*return*/];
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}); });
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
// Guard 6: getTokensWithPasswordGrant failure — tokens not saved, error propagated
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
it('should not save tokens and should propagate the error when getTokensWithPasswordGrant fails', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
267
|
+
return __generator(this, function (_a) {
|
|
268
|
+
switch (_a.label) {
|
|
269
|
+
case 0:
|
|
270
|
+
;
|
|
271
|
+
utilsModule.getTokensWithPasswordGrant.mockRejectedValue(new Error('Login failed for user'));
|
|
272
|
+
return [4 /*yield*/, expect(authLoginModule.authLogin(viyaTarget, false)).rejects.toThrow('Login failed for user')];
|
|
273
|
+
case 1:
|
|
274
|
+
_a.sent();
|
|
275
|
+
expect(utilsModule.saveTokens).not.toHaveBeenCalled();
|
|
276
|
+
expect(utilsModule.fetchLoggedInUser).not.toHaveBeenCalled();
|
|
277
|
+
return [2 /*return*/];
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}); });
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
// Callback coverage: username validator rejects empty input
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
it('should pass a validator to getString that rejects empty usernames', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
285
|
+
var validator;
|
|
286
|
+
return __generator(this, function (_a) {
|
|
287
|
+
switch (_a.label) {
|
|
288
|
+
case 0: return [4 /*yield*/, authLoginModule.authLogin(viyaTarget, false)];
|
|
289
|
+
case 1:
|
|
290
|
+
_a.sent();
|
|
291
|
+
validator = utils_2.getString.mock.calls[0][1];
|
|
292
|
+
expect(validator('')).toBe('Username is required.');
|
|
293
|
+
expect(validator('someuser')).toBe(true);
|
|
294
|
+
return [2 /*return*/];
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
}); });
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
// Callback coverage: password prompt onCancel throws 'Input cancelled.'
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
it('should throw Input cancelled when the password prompt is cancelled', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
302
|
+
return __generator(this, function (_a) {
|
|
303
|
+
switch (_a.label) {
|
|
304
|
+
case 0:
|
|
305
|
+
// Capture the onCancel handler from the prompts() call and invoke it
|
|
306
|
+
// directly, simulating a user pressing Ctrl+C.
|
|
307
|
+
;
|
|
308
|
+
prompts_1.default.mockImplementation(function (_config, opts) {
|
|
309
|
+
opts.onCancel();
|
|
310
|
+
return Promise.resolve({ pass: '' });
|
|
311
|
+
});
|
|
312
|
+
return [4 /*yield*/, expect(authLoginModule.authLogin(viyaTarget, false)).rejects.toThrow('Input cancelled.')];
|
|
313
|
+
case 1:
|
|
314
|
+
_a.sent();
|
|
315
|
+
expect(utilsModule.saveTokens).not.toHaveBeenCalled();
|
|
316
|
+
return [2 /*return*/];
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
}); });
|
|
320
|
+
// ---------------------------------------------------------------------------
|
|
321
|
+
// Branch coverage: id fallback in success log (id || 'unknown user')
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
it('should log "unknown user" when fetchLoggedInUser returns no id', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
324
|
+
return __generator(this, function (_a) {
|
|
325
|
+
switch (_a.label) {
|
|
326
|
+
case 0:
|
|
327
|
+
;
|
|
328
|
+
utilsModule.fetchLoggedInUser.mockResolvedValue({
|
|
329
|
+
id: '',
|
|
330
|
+
name: 'Some Name'
|
|
331
|
+
});
|
|
332
|
+
// The function should still complete — the fallback is in the log message,
|
|
333
|
+
// not a throw. We just verify saveTokens is still called.
|
|
334
|
+
return [4 /*yield*/, authLoginModule.authLogin(viyaTarget, false)];
|
|
335
|
+
case 1:
|
|
336
|
+
// The function should still complete — the fallback is in the log message,
|
|
337
|
+
// not a throw. We just verify saveTokens is still called.
|
|
338
|
+
_a.sent();
|
|
339
|
+
expect(utilsModule.saveTokens).toHaveBeenCalledWith(viyaTarget.name, 'access-123', 'refresh-456');
|
|
340
|
+
return [2 /*return*/];
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
}); });
|
|
344
|
+
// ---------------------------------------------------------------------------
|
|
345
|
+
// Branch coverage: name fallback in success log (name ? ` (${name})` : '')
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
it('should succeed when fetchLoggedInUser returns no name', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
348
|
+
return __generator(this, function (_a) {
|
|
349
|
+
switch (_a.label) {
|
|
350
|
+
case 0:
|
|
351
|
+
;
|
|
352
|
+
utilsModule.fetchLoggedInUser.mockResolvedValue({
|
|
353
|
+
id: 'sastest',
|
|
354
|
+
name: undefined
|
|
355
|
+
});
|
|
356
|
+
return [4 /*yield*/, authLoginModule.authLogin(viyaTarget, false)];
|
|
357
|
+
case 1:
|
|
358
|
+
_a.sent();
|
|
359
|
+
expect(utilsModule.saveTokens).toHaveBeenCalledWith(viyaTarget.name, 'access-123', 'refresh-456');
|
|
360
|
+
return [2 /*return*/];
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
}); });
|
|
364
|
+
});
|
|
@@ -251,10 +251,17 @@ function getBuildInfo(target, streamWeb) {
|
|
|
251
251
|
case 6: return [4 /*yield*/, getDepsContent(dependencyFilePaths, target, macroFolders)];
|
|
252
252
|
case 7:
|
|
253
253
|
dependenciesContent = _a.sent();
|
|
254
|
-
return [4 /*yield*/, getBuildVars(target)
|
|
254
|
+
return [4 /*yield*/, getBuildVars(target)
|
|
255
|
+
/* NOTE: keep %global as the first line - a leading /** comment block is
|
|
256
|
+
stripped by removeHeader() when the final build file is assembled, so
|
|
257
|
+
the guidance below must come AFTER code to survive into the output. */
|
|
258
|
+
];
|
|
255
259
|
case 8:
|
|
256
260
|
buildVars = _a.sent();
|
|
257
|
-
|
|
261
|
+
/* NOTE: keep %global as the first line - a leading /** comment block is
|
|
262
|
+
stripped by removeHeader() when the final build file is assembled, so
|
|
263
|
+
the guidance below must come AFTER code to survive into the output. */
|
|
264
|
+
return [2 /*return*/, "\n%global appLoc;\n\n/**\n * The appLoc represents the metadata or SAS Drive location of the app you\n * are about to deploy\n *\n * To set an alternative appLoc, simply populate the appLoc variable prior\n * to running this program, eg:\n *\n * %let apploc=/my/apploc;\n * %inc thisfile;\n *\n * Do NOT change compiled_apploc - it is not the deploy location, it is the\n * appLoc that was baked into the frontend files at build time. It is used\n * by the swap= parameter of %mv_createfile to rewrite that string to the\n * runtime appLoc when streaming web (html/css/js) files are uploaded.\n * Changing it breaks dynamic appLoc switching (no strings will match).\n *\n */\n\n%let compiled_apploc=".concat(appLoc, ";\n%let appLoc=%sysfunc(coalescec(&appLoc,&compiled_apploc));\n\n\n").concat(serverType === utils_1.ServerType.Sas9
|
|
258
265
|
? "\n/**\n * The serverName represents the SAS 9 logical server context\n * There is no programmatic (SAS code) way to obtain this\n * So it is taken from the sasjsconfig file OR supplied at runtime, eg:\n *\n * %let apploc=/my/apploc;\n * %let serverName=SASAppDC;\n * %inc thisfile;\n *\n */\n\n%global serverName;\n%let compiled_serverName=".concat(serverName, ";\n%let serverName=%sysfunc(coalescec(&serverName,&compiled_serverName));\n")
|
|
259
266
|
: '', "\n\n%let sasjs_clickmeservice=clickme;\n%let syscc=0;\noptions ps=max nonotes nosgen nomprint nomlogic nosource2 nosource noquotelenmax;\n/* user supplied build vars */\n").concat(buildVars, "\n/* user supplied build vars end */\n/* system macro dependencies for build process */\n").concat(dependenciesContent, "\n/* system macro dependencies for build process end*/\n/* system macros for build process */\n").concat((0, utils_1.removeHeader)(buildConfig), "\n/* system macros for build process end */\n")];
|
|
260
267
|
}
|
|
@@ -144,6 +144,9 @@ describe('sasjs build', function () {
|
|
|
144
144
|
expect(finalSasFile.includes(commentLine1.split("'").join("'".repeat(2)))).toBeTruthy();
|
|
145
145
|
expect(commentLine2.split("'").join("'".repeat(2))).toBeTruthy();
|
|
146
146
|
expect(finalSasFile.includes(afterComment)).toBeTruthy();
|
|
147
|
+
// The guidance comment only survives removeHeader() because `%global appLoc;`
|
|
148
|
+
// precedes it (regex ^\s*/(\*){1,2}... does not match) - lock that in.
|
|
149
|
+
expect(finalSasFile.includes('Do NOT change compiled_apploc')).toBeTruthy();
|
|
147
150
|
return [2 /*return*/];
|
|
148
151
|
}
|
|
149
152
|
});
|
|
@@ -73,7 +73,8 @@ function executeDeployScriptSasViya(deployScriptName, target, linesToExecute, lo
|
|
|
73
73
|
fileName: deployScriptName,
|
|
74
74
|
linesOfCode: linesToExecute,
|
|
75
75
|
contextName: contextName,
|
|
76
|
-
authConfig: authConfig
|
|
76
|
+
authConfig: authConfig,
|
|
77
|
+
onTokensRefreshed: (0, config_1.persistTokensRefreshedByAdapter)(target)
|
|
77
78
|
})
|
|
78
79
|
.catch(function (err) {
|
|
79
80
|
process.logger.error('executeScriptSASViya Error', err);
|
|
@@ -69,7 +69,8 @@ var executeOnSasViya = function (target, code) { return __awaiter(void 0, void 0
|
|
|
69
69
|
fileName: 'program.sas',
|
|
70
70
|
linesOfCode: code.split('\n'),
|
|
71
71
|
contextName: contextName,
|
|
72
|
-
authConfig: authConfig
|
|
72
|
+
authConfig: authConfig,
|
|
73
|
+
onTokensRefreshed: (0, utils_2.persistTokensRefreshedByAdapter)(target)
|
|
73
74
|
})];
|
|
74
75
|
case 2:
|
|
75
76
|
log = (_c.sent()).log;
|
|
@@ -202,7 +202,9 @@ function printHelpText() {
|
|
|
202
202
|
name: 'auth',
|
|
203
203
|
title: 'auth',
|
|
204
204
|
description: [
|
|
205
|
-
"lets the user
|
|
205
|
+
"lets the user authenticate against a target.",
|
|
206
|
+
"With the 'login' sub command (SASVIYA targets only), authentication is performed with a SAS username and password - no OAuth client/secret required - and the resulting tokens are saved.",
|
|
207
|
+
"Without a sub command, behaves like 'sasjs add cred' (client/secret based authentication).",
|
|
206
208
|
"",
|
|
207
209
|
"NOTE: This command requires the user to input all the required parameters. More information available in the online documentation: ".concat(chalk_1.default.cyanBright('https://cli.sasjs.io/auth/'))
|
|
208
210
|
]
|
|
@@ -336,7 +338,6 @@ function printHelpText() {
|
|
|
336
338
|
}
|
|
337
339
|
];
|
|
338
340
|
aliases = [
|
|
339
|
-
{ name: 'add', aliases: ['auth'] },
|
|
340
341
|
{ name: 'build', aliases: ['b'] },
|
|
341
342
|
{ name: 'compile', aliases: ['c'] },
|
|
342
343
|
{ name: 'compilebuild', aliases: ['cb'] },
|
|
@@ -169,7 +169,8 @@ function executeOnSasViya(filePath, target, linesToExecute, logFile) {
|
|
|
169
169
|
fileName: path_1.default.basename(filePath),
|
|
170
170
|
linesOfCode: linesToExecute,
|
|
171
171
|
contextName: contextName,
|
|
172
|
-
authConfig: authConfig
|
|
172
|
+
authConfig: authConfig,
|
|
173
|
+
onTokensRefreshed: (0, config_1.persistTokensRefreshedByAdapter)(target)
|
|
173
174
|
})
|
|
174
175
|
.catch(function (err) { return __awaiter(_this, void 0, void 0, function () {
|
|
175
176
|
var log;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.getAllSupportedCommands = exports.commandFactory = void 0;
|
|
4
4
|
var addCommand_1 = require("../../commands/add/addCommand");
|
|
5
|
+
var authCommand_1 = require("../../commands/auth/authCommand");
|
|
5
6
|
var buildCommand_1 = require("../../commands/build/buildCommand");
|
|
6
7
|
var compileBuildCommand_1 = require("../../commands/build/compileBuildCommand");
|
|
7
8
|
var compileCommand_1 = require("../../commands/compile/compileCommand");
|
|
@@ -27,6 +28,7 @@ var fsCommand_1 = require("../../commands/fs/fsCommand");
|
|
|
27
28
|
var snippetsCommand_1 = require("../../commands/snippets/snippetsCommand");
|
|
28
29
|
exports.commandFactory = new Map([
|
|
29
30
|
['add', function (args) { return new addCommand_1.AddCommand(args); }],
|
|
31
|
+
['auth', function (args) { return new authCommand_1.AuthCommand(args); }],
|
|
30
32
|
['build', function (args) { return new buildCommand_1.BuildCommand(args); }],
|
|
31
33
|
['compile', function (args) { return new compileCommand_1.CompileCommand(args); }],
|
|
32
34
|
['compilebuild', function (args) { return new compileBuildCommand_1.CompileBuildCommand(args); }],
|
package/build/utils/auth.d.ts
CHANGED
|
@@ -2,22 +2,6 @@ import { ServerType, Target } from '@sasjs/utils';
|
|
|
2
2
|
import SASjs from '@sasjs/adapter/node';
|
|
3
3
|
export declare const getAuthUrl: (serverType: ServerType, serverUrl: string, clientId: string) => string;
|
|
4
4
|
export declare function getAuthCode(authUrl: string): Promise<string>;
|
|
5
|
-
/**
|
|
6
|
-
* Checks if the Access Token is expired or is expiring in 1 hour. A default Access Token
|
|
7
|
-
* lasts 12 hours. If the Access Token expires, the Refresh Token is used to fetch a new
|
|
8
|
-
* Access Token. In the case that the Refresh Token is expired, 1 hour is enough to let
|
|
9
|
-
* most jobs finish.
|
|
10
|
-
* @param {string} token- token string that will be evaluated
|
|
11
|
-
*/
|
|
12
|
-
export declare function isAccessTokenExpiring(token: string): boolean;
|
|
13
|
-
/**
|
|
14
|
-
* Checks if the Refresh Token is expired or expiring in 30 secs. A default Refresh Token
|
|
15
|
-
* lasts 30 days. Once the Refresh Token expires, the user must re-authenticate (provide
|
|
16
|
-
* credentials in a browser to obtain an authorisation code). 30 seconds is enough time
|
|
17
|
-
* to make a request for a final Access Token.
|
|
18
|
-
* @param {string} token- token string that will be evaluated
|
|
19
|
-
*/
|
|
20
|
-
export declare function isRefreshTokenExpiring(token?: string): boolean;
|
|
21
5
|
/**
|
|
22
6
|
* Exchanges a refresh token for a new access/refresh token pair.
|
|
23
7
|
* SAS Viya's refresh tokens are single-use and rotate on every call: the
|
|
@@ -30,6 +14,41 @@ export declare function refreshTokens(sasjsInstance: SASjs, clientId: string, cl
|
|
|
30
14
|
access_token: string;
|
|
31
15
|
refresh_token: string;
|
|
32
16
|
}>;
|
|
17
|
+
/**
|
|
18
|
+
* The pre-registered, secret-less OAuth client that ships with every SAS Viya
|
|
19
|
+
* deployment (used by the official SAS Viya CLI). Allows authenticating with a
|
|
20
|
+
* SAS username/password (OAuth2 resource owner password grant) when no
|
|
21
|
+
* administrator-registered client/secret is available.
|
|
22
|
+
*/
|
|
23
|
+
export declare const SAS_CLI_CLIENT_ID = "sas.cli";
|
|
24
|
+
/**
|
|
25
|
+
* Fetches an access/refresh token pair from SAS Viya using the resource owner
|
|
26
|
+
* password grant against the built-in `sas.cli` public client. Unlike the
|
|
27
|
+
* client/secret flows, no OAuth client registration is required - the
|
|
28
|
+
* credentials are the user's regular SAS logon credentials.
|
|
29
|
+
*
|
|
30
|
+
* Note: this requires the password grant to be enabled for `sas.cli` (the
|
|
31
|
+
* default) and a local/LDAP account - it cannot work on SSO/SAML/MFA-only
|
|
32
|
+
* estates.
|
|
33
|
+
* @param {Target} target - the SASVIYA target to authenticate against.
|
|
34
|
+
* @param {string} user - the SAS username.
|
|
35
|
+
* @param {string} pass - the SAS password.
|
|
36
|
+
* @returns the access and refresh token pair.
|
|
37
|
+
*/
|
|
38
|
+
export declare function getTokensWithPasswordGrant(target: Target, user: string, pass: string): Promise<{
|
|
39
|
+
access_token: string;
|
|
40
|
+
refresh_token: string;
|
|
41
|
+
}>;
|
|
42
|
+
/**
|
|
43
|
+
* Verifies an access token by fetching the identity it belongs to.
|
|
44
|
+
* @param {Target} target - the SASVIYA target the token was minted for.
|
|
45
|
+
* @param {string} accessToken - the access token to verify.
|
|
46
|
+
* @returns the id and display name of the authenticated user.
|
|
47
|
+
*/
|
|
48
|
+
export declare function fetchLoggedInUser(target: Target, accessToken: string): Promise<{
|
|
49
|
+
id: string;
|
|
50
|
+
name?: string;
|
|
51
|
+
}>;
|
|
33
52
|
export declare function getNewAccessToken(sasjsInstance: SASjs, clientId: string, clientSecret: string, target: Target): Promise<{
|
|
34
53
|
access_token: string;
|
|
35
54
|
refresh_token: string;
|