window.open(url, '_blank') hands the opened page a window.opener reference. The URL is built from config.serverUrl, which on Viya is a different origin from the app, so the SAS log page could navigate this tab - a log page is exactly the sort of thing that gets rendered from backend content. 'noopener' in the window features drops the reference in every browser that supports it. The two existing specs asserted the call's two arguments, so they now assert the third; the client suite is 603/603 with handsontable 18.1.1 from the lockfile.
208 lines
6.8 KiB
TypeScript
208 lines
6.8 KiB
TypeScript
import { DEBUG_STATE_KEY, SasService } from './sas.service'
|
|
|
|
/**
|
|
* SasService's constructor is a plain 8-argument constructor with an empty
|
|
* body, and `request()` touches only a handful of collaborators, so this
|
|
* stubs exactly that surface - no TestBed/DI needed, same precedent as
|
|
* app.service.spec.ts and va-filter.service.spec.ts.
|
|
*
|
|
* The SASjs adapter is not built by the constructor either (sasServiceInit
|
|
* does that), so it is assigned directly - and its `request` spy is where the
|
|
* outgoing body is observable, which is the whole point of these tests.
|
|
*/
|
|
const buildService = () => {
|
|
const sasjsAdapter: any = {
|
|
request: jasmine.createSpy('request').and.resolveTo({}),
|
|
getSasRequests: () => [],
|
|
setDebugState: jasmine.createSpy('setDebugState'),
|
|
getSasjsConfig: () => ({
|
|
serverUrl: 'https://sas.example.com',
|
|
appLoc: '/Public/app/dc',
|
|
serverType: 'SASVIYA'
|
|
})
|
|
}
|
|
const loggerService: any = {
|
|
logRequestData: jasmine.createSpy('logRequestData')
|
|
}
|
|
const userService: any = { user: undefined }
|
|
const eventService: any = {
|
|
showAbortModal: jasmine.createSpy('showAbortModal'),
|
|
startupDataLoaded: jasmine.createSpy('startupDataLoaded')
|
|
}
|
|
const router: any = { navigateByUrl: jasmine.createSpy('navigateByUrl') }
|
|
|
|
const service = new SasService(
|
|
{} as any, // appStoreService
|
|
userService,
|
|
eventService,
|
|
{} as any, // sasjsService
|
|
{} as any, // sasViyaService
|
|
loggerService,
|
|
{} as any, // startupCheckService
|
|
router
|
|
)
|
|
;(service as any).sasjsAdapter = sasjsAdapter
|
|
|
|
return { service, sasjsAdapter }
|
|
}
|
|
|
|
/** The body the adapter was asked to send for the most recent request. */
|
|
const sentBody = (sasjsAdapter: any) =>
|
|
sasjsAdapter.request.calls.mostRecent().args[1]
|
|
|
|
describe('SasService diagnostics payload', () => {
|
|
it('gives the startup service a body, so browser_info can ride on it', async () => {
|
|
const { service, sasjsAdapter } = buildService()
|
|
|
|
// Both startup service call sites pass null - there is nothing to send -
|
|
// and the service is the one whose log a support ticket is read from.
|
|
await service.request('public/startupservice', null)
|
|
|
|
const body = sentBody(sasjsAdapter)
|
|
expect(body).toBeTruthy()
|
|
expect(body.browser_info.length).toBe(1)
|
|
expect(body.browser_info[0].user_agent).toBe(navigator.userAgent)
|
|
expect('timezone' in body.browser_info[0]).toBe(true)
|
|
expect('tz_offset' in body.browser_info[0]).toBe(true)
|
|
})
|
|
|
|
it('leaves a non-diagnostics service payload alone', async () => {
|
|
const { service, sasjsAdapter } = buildService()
|
|
|
|
await service.request('public/viewlibs', null)
|
|
|
|
expect(sentBody(sasjsAdapter)).toBeNull()
|
|
})
|
|
|
|
it('adds browser_info to a body that already carries content', async () => {
|
|
const { service, sasjsAdapter } = buildService()
|
|
|
|
await service.request('editors/getdata', { table: 'MPE_X_TEST' })
|
|
|
|
const body = sentBody(sasjsAdapter)
|
|
expect(body.table).toBe('MPE_X_TEST')
|
|
expect(body.browser_info.length).toBe(1)
|
|
})
|
|
|
|
it('does not overwrite a browser_info the caller supplied', async () => {
|
|
const { service, sasjsAdapter } = buildService()
|
|
const supplied = [{ url: 'supplied by the caller' }]
|
|
|
|
await service.request('editors/getdata', { browser_info: supplied })
|
|
|
|
expect(sentBody(sasjsAdapter).browser_info).toBe(supplied)
|
|
})
|
|
})
|
|
|
|
/**
|
|
* The `<sasjs>` tag is re-read on every load, so the debug toggle has to survive
|
|
* a refresh on its own.
|
|
*/
|
|
describe('SasService debug state', () => {
|
|
/** Builds a service whose init can actually run (real adapter, stub deps). */
|
|
const initService = (tagDebug = false) => {
|
|
const appStoreService: any = {
|
|
getDcAdapterSettings: () => ({
|
|
serverUrl: '',
|
|
appLoc: '/Public/app/dc',
|
|
serverType: 'SAS9',
|
|
loginMechanism: '',
|
|
dcPath: '',
|
|
debug: tagDebug,
|
|
useComputeApi: null,
|
|
runAsTask: true,
|
|
contextName: ''
|
|
})
|
|
}
|
|
|
|
const service = new SasService(
|
|
appStoreService,
|
|
{ user: undefined } as any,
|
|
{ showInfoModal: jasmine.createSpy('showInfoModal') } as any,
|
|
{ setup: jasmine.createSpy('setup') } as any,
|
|
{ setup: jasmine.createSpy('setup') } as any,
|
|
{ logRequestData: jasmine.createSpy('logRequestData') } as any,
|
|
{ updateStep: jasmine.createSpy('updateStep') } as any,
|
|
{ navigateByUrl: jasmine.createSpy('navigateByUrl') } as any
|
|
)
|
|
|
|
service.sasServiceInit()
|
|
|
|
return service
|
|
}
|
|
|
|
beforeEach(() => localStorage.removeItem(DEBUG_STATE_KEY))
|
|
afterEach(() => localStorage.removeItem(DEBUG_STATE_KEY))
|
|
|
|
it('stores the state when the toggle changes', () => {
|
|
const { service, sasjsAdapter } = buildService()
|
|
|
|
service.setDebugState(true)
|
|
|
|
expect(localStorage.getItem(DEBUG_STATE_KEY)).toBe('true')
|
|
expect(sasjsAdapter.setDebugState).toHaveBeenCalledWith(true)
|
|
})
|
|
|
|
it('stores a switch back off, rather than clearing it', () => {
|
|
const { service } = buildService()
|
|
|
|
service.setDebugState(true)
|
|
service.setDebugState(false)
|
|
|
|
expect(localStorage.getItem(DEBUG_STATE_KEY)).toBe('false')
|
|
})
|
|
|
|
it('reinstates the stored state on the next load', () => {
|
|
localStorage.setItem(DEBUG_STATE_KEY, 'true')
|
|
|
|
expect(initService().getSasjsConfig().debug).toBe(true)
|
|
})
|
|
|
|
it('leaves the tag value alone when nothing is stored', () => {
|
|
expect(initService(true).getSasjsConfig().debug).toBe(true)
|
|
})
|
|
|
|
it('reinstates an off state over a tag that turns debug on', () => {
|
|
localStorage.setItem(DEBUG_STATE_KEY, 'false')
|
|
|
|
expect(initService(true).getSasjsConfig().debug).toBe(false)
|
|
})
|
|
})
|
|
|
|
/**
|
|
* The URL shape the deploy configurator already uses for makedata: the service
|
|
* is run by the JES / STP web app in a browser tab, so the SAS log comes back
|
|
* as the page body instead of through the adapter.
|
|
*/
|
|
describe('SasService openServiceInNewWindow', () => {
|
|
it('opens the service URL built from the live adapter config', () => {
|
|
const { service } = buildService()
|
|
spyOn(service, 'getExecutionPath').and.returnValue('/SASJobExecution')
|
|
const openSpy = spyOn(window, 'open')
|
|
|
|
service.openServiceInNewWindow('public/startupservice', {
|
|
_contextname: 'ctx'
|
|
})
|
|
|
|
expect(openSpy).toHaveBeenCalledWith(
|
|
'https://sas.example.com/SASJobExecution/?_program=/Public/app/dc/services/public/startupservice&_contextname=ctx&_debug=131',
|
|
'_blank',
|
|
'noopener'
|
|
)
|
|
})
|
|
|
|
it('opens without parameters when none are given', () => {
|
|
const { service } = buildService()
|
|
spyOn(service, 'getExecutionPath').and.returnValue('/SASJobExecution')
|
|
const openSpy = spyOn(window, 'open')
|
|
|
|
service.openServiceInNewWindow('public/startupservice')
|
|
|
|
expect(openSpy).toHaveBeenCalledWith(
|
|
'https://sas.example.com/SASJobExecution/?_program=/Public/app/dc/services/public/startupservice&_debug=131',
|
|
'_blank',
|
|
'noopener'
|
|
)
|
|
})
|
|
})
|