From 36f56ddda29260797dfb1bc48b88df9c9942b12a Mon Sep 17 00:00:00 2001 From: "sasjs-dev[bot]" Date: Fri, 6 Nov 2026 00:18:21 +0000 Subject: [PATCH] test(spec): vendor the login lockout's Retry-After header, and pin it sasjs/server now generates a headers entry for the 429 on /SASLogon/login; the vendored copy was taken before that and states the header only in the operation description. check:routes compares the route table, which carries no headers, so the copy can drop one without a failure - a new spec names the facts the table cannot carry and fails when the block is removed. Route table unchanged (60 operations). --- spec/swagger.yaml | 5 +++++ spec/vendored-document.spec.ts | 35 ++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 spec/vendored-document.spec.ts diff --git a/spec/swagger.yaml b/spec/swagger.yaml index 4120b59..9c52ec5 100644 --- a/spec/swagger.yaml +++ b/spec/swagger.yaml @@ -2723,6 +2723,11 @@ paths: description: 'The credentials are not valid, or password sign-in is disabled for local accounts' '429': description: 'Too many failed attempts for this account' + headers: + Retry-After: + description: Whole seconds until the account may attempt a sign-in again + schema: + type: integer '500': description: 'An unexpected error occurred while signing in' description: "Signs a user in with a username and password and starts a session.\n\nCredentials are checked against the local database or, for an account that\nauthenticates through LDAP, against the directory. A `401` is returned for\nunknown credentials, and also when password sign-in for local accounts is\ndisabled on the server.\n\nA `429` - the account is locked after repeated failures - carries a\n`Retry-After` header in seconds, so a client can show a countdown without\nparsing the message." diff --git a/spec/vendored-document.spec.ts b/spec/vendored-document.spec.ts new file mode 100644 index 0000000..b50cb25 --- /dev/null +++ b/spec/vendored-document.spec.ts @@ -0,0 +1,35 @@ +import { readFileSync } from 'node:fs' +import { join } from 'node:path' +import { parse } from 'yaml' + +/** + * `check:routes` compares the route table derived from the vendored document, + * not the document itself, and the table carries no response headers. A header + * therefore survives a re-vendor only if the person copying the body notices + * it - which is how the sign-in lockout lost its `Retry-After`. These assertions + * name the facts the table cannot carry, so a copy that drops one fails here + * rather than in a consumer that had come to rely on it. + */ +const document = parse(readFileSync(join(__dirname, 'swagger.yaml'), 'utf8')) + +describe('the vendored document', () => { + it('states the Retry-After header the sign-in lockout carries', () => { + const responses = document.paths['/SASLogon/login'].post.responses + + expect(responses['429'].headers['Retry-After']).toEqual({ + description: + 'Whole seconds until the account may attempt a sign-in again', + schema: { type: 'integer' } + }) + }) + + it('names the unit of that header in the operation description', () => { + const description = document.paths['/SASLogon/login'].post.description + + // the prose is wrapped for the document, so compare it with the line breaks + // collapsed rather than pinning where they fall + expect(description.replace(/\s+/g, ' ')).toContain( + 'carries a `Retry-After` header in seconds' + ) + }) +})