Mock build: cover the rest of the sasjs/server API from one shared description, not a second implementation
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 24/100
- Issue type
- Feature
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- express, github-actions, javascript, node.js, openapi, typescript
- Domain
- api, authentication, backend, databases, developer-experience, testing
Research direction
Start with api/public/swagger.yaml, mockSite/assets/sw.js, and the existing mock specs that load the worker; inspect the server controller routes and the *.spec.server.* CI lane for how live responses are exercised. The proposal is broad: generate a shared route table and service layer, add identity fixtures and capability handling, then run one request corpus against both hosts. Done means the mock and server conform across the 60 operations, with unsupported capabilities and remaining work reported explicitly.
Written by the indexing model from the issue text.
Description
Context
A mockWeb build runs a project's JavaScript services in the browser behind a service worker that answers /SASjsApi/* on the site's own origin.
Today it covers the program path (stp/execute, code/execute, a stub session), the read half of the drive, and - since #1551 - the drive CRUD calls.
Two parity gaps have already been found by review rather than by a test: the webout() block (#1540) and the session header seed (#1542).
Both were cases where the mock and sasjs/server answered the same request differently, and neither was visible until someone compared them by hand.
Problem
Every endpoint is implemented twice - once as an express route plus controller in sasjs/server, once as a branch in mockSite/assets/sw.js - and there is no artefact that says what "the same API" means.
So the mock drifts toward "the calls our demo app happens to make", and each new route is a fresh judgement call about which statuses and bodies to copy.
Proposal
1. One route table, generated from the OpenAPI document
sasjs/server already publishes a description: api/public/swagger.yaml, generated by tsoa spec from the controller decorators (43 paths, 60 operations).
Do not hand-author a fourth description. Generate the table from that document, and keep a small overlay for the parts the document cannot express: the response bodies, which capability a route needs, and the handlers that only exist on one host.
CI then holds the three of them to each other - the document, the mounted express router, and the corpus in point 3 - so the next @Response typo fails a test instead of shipping.
That check has value immediately: today POST /SASjsApi/drive/file is decorated @Response(403, 'File already exists') while saveFile throws 409, PATCH /drive/file is decorated 403 while updateFile throws 404, and GET /drive/file is documented 204 because its body is written by the controller rather than returned.
The mock in #1551 follows the implementation, because that is what a client sees.
2. A service layer over injected capabilities
The drive is the clean case: the semantics are pure functions over { fs, path, driveRoot }, and the mock's FS shim already matches node's API for the calls that matter.
The same shape applies to the rest - program resolution, the session record (id, state, log, output), the file-tree builder, and the identity store - once storage and the current subject are injected rather than imported.
Both hosts call the same functions: express passes node's fs and the authenticated user, the worker passes its shim and the demo subject.
3. A conformance suite instead of two spec files
One corpus of cases - request in, status and body out - run twice: against a live sasjs/server in the existing *.spec.server.* CI lane, and against the mock worker in-process (the mock's specs already load sw.js and the runtime unmodified).
Then a route the mock does not implement is a named skip with a reason, not a silent 404, and a behaviour change on either side fails the other's tests.
4. Capabilities, not assumptions
info already reports runTimes: ['js'].
Each route should declare what it needs - a JS runtime, a filesystem, a SAS engine, an identity store - so the host answers what it can and says why for what it cannot, rather than a blanket 404.
Identity is mocked, from a fixture - not absent
The identity and administration routes are in scope. They have to be: the SASjs Adapter runs the sign-in flow for serverType: Sasjs even in a demo, calling POST /SASjsApi/auth/token and then POST /SASjsApi/auth/refresh, so a mock build that answers neither cannot get an app past its own login screen.
An app whose menus and actions come from GET /SASjsApi/permission or info/myAuthorizedRoutes also needs those answers just to render.
What "mocked" means here, and what it must not mean:
- A fixed demo identity, declared. The host holds a seeded set of users, groups and permission rules - one admin and at least one restricted user - and answers from it. Tokens are opaque strings the host accepts, exactly as the server's are opaque to the client.
- Every route answered, including the refusals. A mock that returns
200to everything teaches an app nothing and hides the bugs the demo exists to surface.401,403and the permission list must be real answers from the fixture. - The subject is switchable. A reviewer wants to see the app as the restricted user, not only as the admin -
window.SASjsMock.as('user2')(or a seeded query parameter) re-issues a token for that user and resets the app. That is the single most useful thing this can offer a review environment. - Writes persist like the drive. Creating a user or a permission rule lands in the same per-visitor IndexedDB the drive uses, so the demo survives a reload and can be captured in a snapshot.
- Never a boundary, and said so. Passwords are not verified (a fixed demo password, or none), tokens are not signed, and every page that describes the build says the identity data is a fixture rather than an authorisation service. A mock build holding anything real belongs behind a real server - that warning is already on the page and gets one sentence about the fixture.
This is the same rule as the SYSPROCESSID placeholder: state what is fake, do not make something that merely looks real.
Scope - all 60 operations
| Tag | Ops | State |
|---|---|---|
| Drive | 11 | CRUD and the two reads done (#1551); deploy and deploy/upload outstanding |
| Session | 5 | a stub answering {state:'completed'}; no session record, so log/output cannot answer |
| Code | 4 | execute done; trigger, lint, format outstanding |
| STP | 3 | POST execute done; GET execute and trigger outstanding |
| Info | 3 | / done; authorizedRoutes, myAuthorizedRoutes outstanding |
| User | 8 | none |
| Group | 7 | none |
| Auth | 4 | none - and without token/refresh an adapter login cannot complete |
| Permission | 4 | none |
| Web | 4 | none - /SASLogon/login, /authorize, /logout are the browser sign-in flow |
| Admin | 3 | none - diagnostics, log list, log tail (the last two can answer from the session record) |
| Auth Config | 2 | none |
| Client | 2 | none |
Order
auth/token,auth/refresh,auth/logout,SASLogon/login- nothing else can be exercised until a client can hold a token.user,group,permission,info/authorizedRoutes,info/myAuthorizedRoutes- the reads an app bootstraps from, then the writes the admin screens make.sessionas a real record - it is whatlog,outputandadmin/logsread from.stpGET executeandtrigger,code trigger.drive/deployanddeploy/upload, reusing the file-tree writer.code lintandformat-@sasjs/lintis already a dependency of the CLI, so this is a worker-side call rather than a re-implementation.admin,authConfig,client- the thin remaining surface over the store and the session record.
Where the shared code goes
@sasjs/api: the generated route table, the services over injected capabilities, the identity fixture format, and the conformance corpus with its fixtures.
Both hosts depend on it - sasjs/server and the cli mock - so the response bodies, statuses and path rules cannot diverge without a failing test.
The doc sites are the third consumer, and they are already a consumer of the description: server.sasjs.io checks in docs/swagger.yaml (today byte-identical to the server's api/public/swagger.yaml) and renders it with mkdocs-swagger-ui-tag, guarded by scripts/check-server-docs.mjs.
That means the published reference currently advertises the same disagreements the overlay records - 403 for a drive create that throws 409, 204 for a file read that returns a body - so a reader of the docs and a reader of the code are told different things.
Two things follow. ROUTES can generate the docs' "what a mock build answers" table from implemented and capabilities, which stays true as parity lands instead of being maintained by hand. And every correction in the overlay is a bug in the server's decorators, to be fixed at source: the field is a ratchet that should empty out, not a feature.
Identity defaults
The build always ships an identity fixture, so a mock works with no configuration at all: small and legible, one admin, one restricted user, two groups, a handful of permission rules.
There is exactly one file and one load path in the output - host/identity.json, written by the build and fetched by the runtime at boot - the same arrangement webout-prelude.json already uses, so the runtime reads a file rather than an object literal that would drift from the documented format.
A project replaces it by dropping mock-identity.json beside the build, exactly as mock-snapshot.json replaces the drive seed; the build copies whichever it has to host/identity.json, so "which wins" is decided once, at build time, and never at run time.
The file carries a format stamp the way the drive does, so a store persisted by an older build is refused with a named reason instead of being misread.
Persistence follows the drive's rule, which is behaviour a reader of this project already knows: what is in IndexedDB wins until window.SASjsMock.reset(), which restores the drive and the identity together.
Switching the subject is a console call - window.SASjsMock.as('user2') - and a panel in the demo chrome is a later nicety, not a prerequisite.
Deploy cap
POST /drive/deploy writes a whole file tree in one request, so it is the one route where a caller can exhaust the visitor's storage.
Cap it, and refuse before writing anything: validate the tree, count its entries and sum their decoded sizes, and only then write, so a refused deploy leaves the drive exactly as it was.
Limits, carried as one exported constant alongside DRIVE_FORMAT so the check, the specs and the docs read the same numbers: 5,000 entries, 50 MB decoded in total, 10 MB for a single file.
Those are chosen against what a browser will actually hold - IndexedDB allows far more, but the runtime keeps a decoded copy in memory and the snapshot download is a JSON of the same drive - and they sit above any demo estate, so a legitimate deploy of a built app fits.
A refusal is 413 in the server's own envelope, naming the limit that was exceeded ({"status":"failure","message":"The file tree exceeds the browser host limit: 6412 entries (maximum 5000)."}), because "deployment failed" sends the reader looking for a bug in their tree.
Deploy is re-seeding, not boot
A deploy route cannot be how a mock site starts, because the API does not exist until the site does.
sasjs build writes the host from the target's configuration, and the drive must already hold the compiled services before the first request can be served - so the boot seed is a build output, and no request can populate it.
What deploy covers is everything after boot: a reset, a restored snapshot, an updated tree after a rebuild. Those are the cases where "the same route production uses" is worth having.
What this buys: one app, every host
The point of the parity work is not the mock for its own sake. It is that one frontend and one corpus run against every host a SASjs app can have, so a failure says where the defect is:
| Host | Engine | Services | A red lane here means |
|---|---|---|---|
| Mock build | none | JavaScript | the frontend, the adapter, or the contract itself |
| SASjs Server, JS runtime | node | JavaScript | a server-side JS difference - the runtime's fs, the session, the headers |
| SASjs Server, SAS runtime | SAS | SAS | SAS behaviour, or the service as compiled |
| Viya | SAS | SAS / jobs | Viya specifics - contexts, files service, job state |
| SAS9 | SAS | SAS (STP) | SAS9 specifics - _webout, filerefs, the STP server |
With the mock lane green, a red lane everywhere else is a backend defect, and the same test that passed in the browser points at the line to read.
That is also why the corpus has to be shared rather than rewritten per host: two corpora would just reproduce the drift this issue exists to remove.
Two things the matrix still cannot tell you, and the docs should say so: SAS execution semantics (formats, PROC behaviour, macro quoting) are simply not present in the browser, and a fixture identity is not an identity provider - LDAP, OAuth and real password policy have no browser-side equivalent.
- Dominant language
- TypeScript
- Stars
- 41
- Forks
- 6
- Avg merge
- 3h 46m
- Merged PRs (30d)
- 86
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from sasjs/cli
-
Difficulty 4/5 3-5 days Newbie friendliness 48/100
Maintainers usually reply within 1 day
-
Fetch parent session log when job fails without recording logMay be free again @YuryShkoda claimed this 1163 days ago, and no pull request is open. Open
sasjs/cli#1350 · 1 reaction · 1 assignee ·
Maintainers usually reply within 1 day
-
sasjs deploy for SAS 9 is failingMay be free again @krishna-acondy claimed this 1800 days ago, and no pull request is open. Opensasjs deploy
Maintainers usually reply within 1 day
-
enhancement sponsorship needed
Difficulty 4/5 3-5 days Newbie friendliness 35/100
Maintainers usually reply within 1 day
-
enhancement sponsorship needed
Difficulty 5/5 Over a week Newbie friendliness 25/100
Maintainers usually reply within 1 day
Similar issues
-
Autocomplete still accepts an option after becoming disabledPossibly taken @Xapollon430 claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 67/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
Difficulty 1/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
reduxjs/react-redux#2416 · 1 comment ·