@proteinjs/user-server 1.22.0 → 1.22.2
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/CHANGELOG.md +24 -0
- package/dist/generated/index.js +1 -1
- package/dist/generated/index.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -2
- package/dist/index.js.map +1 -1
- package/dist/src/authentication/PasswordHasher.d.ts +9 -0
- package/dist/src/authentication/PasswordHasher.d.ts.map +1 -1
- package/dist/src/authentication/PasswordHasher.js +25 -0
- package/dist/src/authentication/PasswordHasher.js.map +1 -1
- package/dist/src/authentication/PasswordResetToken.d.ts +43 -12
- package/dist/src/authentication/PasswordResetToken.d.ts.map +1 -1
- package/dist/src/authentication/PasswordResetToken.js +86 -21
- package/dist/src/authentication/PasswordResetToken.js.map +1 -1
- package/dist/src/authentication/authenticate.d.ts +15 -0
- package/dist/src/authentication/authenticate.d.ts.map +1 -1
- package/dist/src/authentication/authenticate.js +55 -20
- package/dist/src/authentication/authenticate.js.map +1 -1
- package/dist/src/routes/devLogin.d.ts +1 -1
- package/dist/src/routes/devLogin.js +1 -1
- package/dist/src/routes/executePasswordReset.d.ts +6 -1
- package/dist/src/routes/executePasswordReset.d.ts.map +1 -1
- package/dist/src/routes/executePasswordReset.js +16 -7
- package/dist/src/routes/executePasswordReset.js.map +1 -1
- package/dist/src/routes/initiatePasswordReset.d.ts +10 -2
- package/dist/src/routes/initiatePasswordReset.d.ts.map +1 -1
- package/dist/src/routes/initiatePasswordReset.js +55 -34
- package/dist/src/routes/initiatePasswordReset.js.map +1 -1
- package/dist/src/routes/login.d.ts +8 -0
- package/dist/src/routes/login.d.ts.map +1 -1
- package/dist/src/routes/login.js +63 -22
- package/dist/src/routes/login.js.map +1 -1
- package/dist/src/throttle/ClientAddress.d.ts +35 -0
- package/dist/src/throttle/ClientAddress.d.ts.map +1 -0
- package/dist/src/throttle/ClientAddress.js +48 -0
- package/dist/src/throttle/ClientAddress.js.map +1 -0
- package/dist/src/throttle/PasswordResetThrottle.d.ts +39 -0
- package/dist/src/throttle/PasswordResetThrottle.d.ts.map +1 -0
- package/dist/src/throttle/PasswordResetThrottle.js +59 -0
- package/dist/src/throttle/PasswordResetThrottle.js.map +1 -0
- package/dist/src/throttle/RequestDigests.d.ts +42 -0
- package/dist/src/throttle/RequestDigests.d.ts.map +1 -0
- package/dist/src/throttle/RequestDigests.js +96 -0
- package/dist/src/throttle/RequestDigests.js.map +1 -0
- package/dist/src/throttle/SignInThrottle.d.ts +67 -0
- package/dist/src/throttle/SignInThrottle.d.ts.map +1 -0
- package/dist/src/throttle/SignInThrottle.js +91 -0
- package/dist/src/throttle/SignInThrottle.js.map +1 -0
- package/dist/src/throttle/SlidingWindow.d.ts +51 -0
- package/dist/src/throttle/SlidingWindow.d.ts.map +1 -0
- package/dist/src/throttle/SlidingWindow.js +85 -0
- package/dist/src/throttle/SlidingWindow.js.map +1 -0
- package/dist/test/ClientAddress.test.d.ts +2 -0
- package/dist/test/ClientAddress.test.d.ts.map +1 -0
- package/dist/test/ClientAddress.test.js +51 -0
- package/dist/test/ClientAddress.test.js.map +1 -0
- package/dist/test/DbTraffic.d.ts +29 -0
- package/dist/test/DbTraffic.d.ts.map +1 -0
- package/dist/test/DbTraffic.js +106 -0
- package/dist/test/DbTraffic.js.map +1 -0
- package/dist/test/DevLoginBootstrapAdmin.test.js +3 -4
- package/dist/test/DevLoginBootstrapAdmin.test.js.map +1 -1
- package/dist/test/DevLoginStaleCookieFirstHit.test.js +1 -1
- package/dist/test/DevLoginStaleCookieFirstHit.test.js.map +1 -1
- package/dist/test/ExecutePasswordReset.test.js +513 -124
- package/dist/test/ExecutePasswordReset.test.js.map +1 -1
- package/dist/test/InitiatePasswordReset.test.d.ts +2 -0
- package/dist/test/InitiatePasswordReset.test.d.ts.map +1 -0
- package/dist/test/InitiatePasswordReset.test.js +783 -0
- package/dist/test/InitiatePasswordReset.test.js.map +1 -0
- package/dist/test/LogCapture.d.ts +22 -0
- package/dist/test/LogCapture.d.ts.map +1 -0
- package/dist/test/LogCapture.js +127 -0
- package/dist/test/LogCapture.js.map +1 -0
- package/dist/test/LoginThrottle.integration.test.d.ts +2 -0
- package/dist/test/LoginThrottle.integration.test.d.ts.map +1 -0
- package/dist/test/LoginThrottle.integration.test.js +616 -0
- package/dist/test/LoginThrottle.integration.test.js.map +1 -0
- package/dist/test/MalformedResetTokens.d.ts +11 -0
- package/dist/test/MalformedResetTokens.d.ts.map +1 -0
- package/dist/test/MalformedResetTokens.js +35 -0
- package/dist/test/MalformedResetTokens.js.map +1 -0
- package/dist/test/RequestDigests.test.d.ts +2 -0
- package/dist/test/RequestDigests.test.d.ts.map +1 -0
- package/dist/test/RequestDigests.test.js +73 -0
- package/dist/test/RequestDigests.test.js.map +1 -0
- package/dist/test/SlidingWindow.test.d.ts +2 -0
- package/dist/test/SlidingWindow.test.d.ts.map +1 -0
- package/dist/test/SlidingWindow.test.js +72 -0
- package/dist/test/SlidingWindow.test.js.map +1 -0
- package/dist/test/ValidateResetToken.test.js +156 -33
- package/dist/test/ValidateResetToken.test.js.map +1 -1
- package/dist/test/emulatorLock.test.d.ts +2 -0
- package/dist/test/emulatorLock.test.d.ts.map +1 -0
- package/dist/test/emulatorLock.test.js +32 -0
- package/dist/test/emulatorLock.test.js.map +1 -0
- package/dist/test/emulatorLockName.test.d.ts +2 -0
- package/dist/test/emulatorLockName.test.d.ts.map +1 -0
- package/dist/test/emulatorLockName.test.js +36 -0
- package/dist/test/emulatorLockName.test.js.map +1 -0
- package/generated/index.ts +1 -1
- package/index.ts +6 -1
- package/jest.config.js +2 -2
- package/package.json +3 -3
- package/src/authentication/PasswordHasher.ts +18 -1
- package/src/authentication/PasswordResetToken.ts +74 -20
- package/src/authentication/authenticate.ts +36 -9
- package/src/routes/devLogin.ts +1 -1
- package/src/routes/executePasswordReset.ts +10 -2
- package/src/routes/initiatePasswordReset.ts +46 -25
- package/src/routes/login.ts +39 -8
- package/src/throttle/ClientAddress.ts +47 -0
- package/src/throttle/PasswordResetThrottle.ts +62 -0
- package/src/throttle/RequestDigests.ts +97 -0
- package/src/throttle/SignInThrottle.ts +98 -0
- package/src/throttle/SlidingWindow.ts +90 -0
- package/test/ClientAddress.test.ts +54 -0
- package/test/DbTraffic.ts +43 -0
- package/test/DevLoginBootstrapAdmin.test.ts +3 -4
- package/test/DevLoginStaleCookieFirstHit.test.ts +1 -1
- package/test/ExecutePasswordReset.test.ts +308 -70
- package/test/InitiatePasswordReset.test.ts +456 -0
- package/test/LogCapture.ts +54 -0
- package/test/LoginThrottle.integration.test.ts +339 -0
- package/test/MalformedResetTokens.ts +27 -0
- package/test/RequestDigests.test.ts +81 -0
- package/test/SlidingWindow.test.ts +85 -0
- package/test/ValidateResetToken.test.ts +108 -24
- package/test/emulatorLock.js +5 -6
- package/test/emulatorLock.test.ts +32 -0
- package/test/emulatorLockName.test.ts +33 -0
- package/test/setup.js +2 -0
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
1
2
|
import moment from 'moment';
|
|
2
3
|
import { getDbAsSystem } from '@proteinjs/db';
|
|
3
4
|
import { tables } from '@proteinjs/user';
|
|
4
5
|
import { validateResetPasswordToken } from '../src/routes/validateResetPasswordToken';
|
|
5
6
|
import { PasswordResetToken } from '../src/authentication/PasswordResetToken';
|
|
6
7
|
import { UserServerTestEnvironment } from './UserServerTestEnvironment';
|
|
8
|
+
import { DbTraffic } from './DbTraffic';
|
|
9
|
+
import { LogCapture } from './LogCapture';
|
|
10
|
+
import { MalformedResetTokens } from './MalformedResetTokens';
|
|
7
11
|
|
|
8
12
|
const testEnv = new UserServerTestEnvironment();
|
|
9
13
|
|
|
@@ -14,8 +18,15 @@ const testEnv = new UserServerTestEnvironment();
|
|
|
14
18
|
* with the stored credential. The email only ever rides a VALID token's response — the
|
|
15
19
|
* token was delivered to that very inbox, so it reveals nothing the holder doesn't know —
|
|
16
20
|
* while invalid/expired verdicts stay email-free (no account-probing oracle). A query value
|
|
17
|
-
* that is not a well-formed token
|
|
18
|
-
* invalid without a lookup
|
|
21
|
+
* that is not a well-formed token — every type a parsed query string can deliver, and every
|
|
22
|
+
* near-miss cut from a live token — is invalid without a lookup: nothing reaches the database
|
|
23
|
+
* (no lookup built, no statement run), which the verdict alone could not prove, since a wrongly
|
|
24
|
+
* admitted value still matches no row. The row stores the token's SHA-256 digest, never the
|
|
25
|
+
* token: the stored digest presented as a token is invalid — it has a token's shape, so the one
|
|
26
|
+
* lookup it builds is by the digest OF the presented value — and so is a token an earlier
|
|
27
|
+
* release stored in clear. No token, no leading characters of one and no full digest reach the
|
|
28
|
+
* log. Live tokens are seeded through `PasswordResetToken.mint`, so the suite moves with what
|
|
29
|
+
* the owner stores.
|
|
19
30
|
*/
|
|
20
31
|
|
|
21
32
|
type RouteOutcome = { status?: number; body?: any };
|
|
@@ -35,15 +46,19 @@ const invokeValidate = async (query: Record<string, unknown>): Promise<RouteOutc
|
|
|
35
46
|
return outcome;
|
|
36
47
|
};
|
|
37
48
|
|
|
38
|
-
const
|
|
49
|
+
const sha256Hex = (value: string) => createHash('sha256').update(value).digest('hex');
|
|
39
50
|
|
|
40
|
-
|
|
51
|
+
/** A well-formed token that was never issued to anyone. */
|
|
52
|
+
const unissuedToken = () => sha256Hex(`never issued ${Math.random()}`);
|
|
53
|
+
|
|
54
|
+
/** A user with a token minted by the owner; `expiration` (when given) replaces the expiry the mint stored. */
|
|
55
|
+
const armResetToken = async (email: string, expiration?: moment.Moment): Promise<string> => {
|
|
41
56
|
const user = await testEnv.createUser({ name: 'Reset User', email });
|
|
42
|
-
await
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
57
|
+
const token = await new PasswordResetToken().mint(user);
|
|
58
|
+
if (expiration !== undefined) {
|
|
59
|
+
await getDbAsSystem().update(tables.User, { id: user.id, passwordResetTokenExpiration: expiration });
|
|
60
|
+
}
|
|
61
|
+
return token;
|
|
47
62
|
};
|
|
48
63
|
|
|
49
64
|
describe('validateResetPasswordToken route', () => {
|
|
@@ -56,8 +71,7 @@ describe('validateResetPasswordToken route', () => {
|
|
|
56
71
|
});
|
|
57
72
|
|
|
58
73
|
it('a valid token resolves isValid WITH the account email (the reset form identifier)', async () => {
|
|
59
|
-
const token =
|
|
60
|
-
await armResetToken('reset-valid@test.local', token, moment().add(1, 'hour'));
|
|
74
|
+
const token = await armResetToken('reset-valid@test.local');
|
|
61
75
|
|
|
62
76
|
const outcome = await invokeValidate({ token });
|
|
63
77
|
|
|
@@ -66,8 +80,7 @@ describe('validateResetPasswordToken route', () => {
|
|
|
66
80
|
});
|
|
67
81
|
|
|
68
82
|
it('an expired token resolves invalid and leaks no email', async () => {
|
|
69
|
-
const token =
|
|
70
|
-
await armResetToken('reset-expired@test.local', token, moment().subtract(1, 'minute'));
|
|
83
|
+
const token = await armResetToken('reset-expired@test.local', moment().subtract(1, 'minute'));
|
|
71
84
|
|
|
72
85
|
const outcome = await invokeValidate({ token });
|
|
73
86
|
|
|
@@ -77,28 +90,99 @@ describe('validateResetPasswordToken route', () => {
|
|
|
77
90
|
});
|
|
78
91
|
|
|
79
92
|
it('an unknown token resolves invalid and leaks no email', async () => {
|
|
80
|
-
const outcome = await invokeValidate({ token:
|
|
93
|
+
const outcome = await invokeValidate({ token: unissuedToken() });
|
|
81
94
|
|
|
82
95
|
expect(outcome.status).toBe(200);
|
|
83
96
|
expect(outcome.body.isValid).toBe(false);
|
|
84
97
|
expect(outcome.body.email).toBeUndefined();
|
|
85
98
|
});
|
|
86
99
|
|
|
87
|
-
it
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const outcome = await
|
|
100
|
+
it('the stored digest presented as a token resolves invalid — the row holds nothing that can be presented', async () => {
|
|
101
|
+
const token = await armResetToken('reset-digest@test.local');
|
|
102
|
+
const row = await getDbAsSystem().get(tables.User, { email: 'reset-digest@test.local' });
|
|
103
|
+
expect(row.passwordResetToken).toBe(sha256Hex(token));
|
|
104
|
+
|
|
105
|
+
const { result: outcome, traffic } = await DbTraffic.during(
|
|
106
|
+
testEnv.spannerDriver,
|
|
107
|
+
async () => await invokeValidate({ token: row.passwordResetToken })
|
|
108
|
+
);
|
|
93
109
|
|
|
94
110
|
expect(outcome.status).toBe(200);
|
|
95
111
|
expect(outcome.body).toEqual({ isValid: false, message: 'Invalid token' });
|
|
112
|
+
expect(traffic.lookups).toEqual([{ passwordResetToken: sha256Hex(sha256Hex(token)) }]);
|
|
113
|
+
expect(traffic.statements).toBe(1);
|
|
114
|
+
expect(traffic.writes).toBe(0);
|
|
96
115
|
});
|
|
97
116
|
|
|
98
|
-
it('a
|
|
99
|
-
const
|
|
117
|
+
it('a token an earlier release stored in clear resolves invalid — no migration, the person asks again', async () => {
|
|
118
|
+
const clearToken = unissuedToken();
|
|
119
|
+
const user = await testEnv.createUser({ name: 'Reset User', email: 'reset-stored-in-clear@test.local' });
|
|
120
|
+
await getDbAsSystem().update(tables.User, {
|
|
121
|
+
id: user.id,
|
|
122
|
+
passwordResetToken: clearToken,
|
|
123
|
+
passwordResetTokenExpiration: moment().add(1, 'hour'),
|
|
124
|
+
});
|
|
100
125
|
|
|
101
|
-
|
|
102
|
-
|
|
126
|
+
const outcome = await invokeValidate({ token: clearToken });
|
|
127
|
+
|
|
128
|
+
expect(outcome.status).toBe(200);
|
|
129
|
+
expect(outcome.body).toEqual({ isValid: false, message: 'Invalid token' });
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('a query without a well-formed token is invalid without a lookup', () => {
|
|
133
|
+
let liveToken: string;
|
|
134
|
+
|
|
135
|
+
beforeAll(async () => {
|
|
136
|
+
liveToken = await armResetToken('reset-shapes@test.local');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it.each(MalformedResetTokens.CASES)(
|
|
140
|
+
'%s: invalid, no email, and nothing reaches the database',
|
|
141
|
+
async (_label, tokenField) => {
|
|
142
|
+
const query = tokenField(liveToken);
|
|
143
|
+
|
|
144
|
+
const { result: outcome, traffic } = await DbTraffic.during(
|
|
145
|
+
testEnv.spannerDriver,
|
|
146
|
+
async () => await invokeValidate(query)
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// Nothing usable in the query is a 400; anything else that is not a token is an invalid token.
|
|
150
|
+
expect(outcome).toEqual(
|
|
151
|
+
query.token
|
|
152
|
+
? { status: 200, body: { isValid: false, message: 'Invalid token' } }
|
|
153
|
+
: { status: 400, body: { isValid: false, message: 'No token provided' } }
|
|
154
|
+
);
|
|
155
|
+
expect(traffic).toEqual(DbTraffic.NONE);
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
it('after every refusal the live token is still valid', async () => {
|
|
160
|
+
const outcome = await invokeValidate({ token: liveToken });
|
|
161
|
+
|
|
162
|
+
expect(outcome.body).toEqual({ isValid: true, email: 'reset-shapes@test.local' });
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('no token, no leading characters of one and no full digest reach the log — every verdict', async () => {
|
|
167
|
+
const live = await armResetToken('reset-log-live@test.local');
|
|
168
|
+
const expired = await armResetToken('reset-log-expired@test.local', moment().subtract(1, 'minute'));
|
|
169
|
+
const unissued = unissuedToken();
|
|
170
|
+
|
|
171
|
+
const log = await LogCapture.during(async () => {
|
|
172
|
+
await invokeValidate({ token: unissued });
|
|
173
|
+
await invokeValidate({ token: live.toUpperCase() });
|
|
174
|
+
await invokeValidate({ token: sha256Hex(live) });
|
|
175
|
+
await invokeValidate({ token: expired });
|
|
176
|
+
await invokeValidate({ token: live });
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// The capture saw the route's lines: the refusals above logged.
|
|
180
|
+
expect(log.text).toContain('Invalid reset token used');
|
|
181
|
+
expect(log.text).toContain('Expired reset token used');
|
|
182
|
+
expect(log.lines.length).toBeGreaterThanOrEqual(4);
|
|
183
|
+
// The digest presented as a token is covered by `live`: its full digest is what was presented.
|
|
184
|
+
for (const token of [unissued, live, expired]) {
|
|
185
|
+
log.expectFreeOf(token);
|
|
186
|
+
}
|
|
103
187
|
});
|
|
104
188
|
});
|
package/test/emulatorLock.js
CHANGED
|
@@ -7,11 +7,10 @@
|
|
|
7
7
|
* FAILED_PRECONDITION). This lock closes that gap for this package: a lockfile in os.tmpdir()
|
|
8
8
|
* keyed by the emulator host string, held for the whole jest run via globalSetup/globalTeardown.
|
|
9
9
|
*
|
|
10
|
-
* The lockfile PATH FORMULA must stay byte-identical
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* on @proteinjs/user) — future extraction target: @proteinjs/db-driver-spanner/test, beside
|
|
10
|
+
* The lockfile PATH FORMULA must stay byte-identical in every copy of this lock (other
|
|
11
|
+
* repositories' suites, a consumer application's suites) so runs pointed at the same emulator
|
|
12
|
+
* actually contend on the same file. The logic is duplicated here because no shared package
|
|
13
|
+
* carries it yet — future extraction target: @proteinjs/db-driver-spanner/test, beside
|
|
15
14
|
* SpannerEmulatorProvisioner.
|
|
16
15
|
*/
|
|
17
16
|
const crypto = require('crypto');
|
|
@@ -30,7 +29,7 @@ function lockFilePathFor(host) {
|
|
|
30
29
|
.replace(/^-+|-+$/g, '')
|
|
31
30
|
.slice(0, 64);
|
|
32
31
|
const hash = crypto.createHash('sha1').update(host).digest('hex').slice(0, 8);
|
|
33
|
-
return path.join(os.tmpdir(), `
|
|
32
|
+
return path.join(os.tmpdir(), `spanner-emulator-${slug}-${hash}.lock`);
|
|
34
33
|
}
|
|
35
34
|
|
|
36
35
|
function isPidAlive(pid) {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
6
|
+
const { lockFilePathFor } = require('./emulatorLock') as { lockFilePathFor: (host: string) => string };
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The lockfile path is a CONTRACT between every copy of the emulator lock: two jest runs only
|
|
10
|
+
* serialize when they compute the same file for the same emulator host. The formula is pinned
|
|
11
|
+
* here byte for byte — a neutral `spanner-emulator-` name (no application's name belongs in a
|
|
12
|
+
* shared lock), the sanitized host slug, and the first 8 hex characters of the host's sha1.
|
|
13
|
+
*/
|
|
14
|
+
describe('emulatorLock.lockFilePathFor', () => {
|
|
15
|
+
const expectedFor = (host: string, slug: string) =>
|
|
16
|
+
path.join(
|
|
17
|
+
os.tmpdir(),
|
|
18
|
+
`spanner-emulator-${slug}-${crypto.createHash('sha1').update(host).digest('hex').slice(0, 8)}.lock`
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
it('names the lock by the emulator alone: spanner-emulator-<slug>-<sha1[0..8]>.lock in os.tmpdir()', () => {
|
|
22
|
+
expect(lockFilePathFor('localhost:9010')).toBe(expectedFor('localhost:9010', 'localhost-9010'));
|
|
23
|
+
expect(lockFilePathFor('127.0.0.1:9710')).toBe(expectedFor('127.0.0.1:9710', '127-0-0-1-9710'));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('gives two emulators two files, and one emulator the same file every time', () => {
|
|
27
|
+
expect(lockFilePathFor('localhost:9010')).toBe(lockFilePathFor('localhost:9010'));
|
|
28
|
+
expect(lockFilePathFor('localhost:9010')).not.toBe(lockFilePathFor('localhost:9011'));
|
|
29
|
+
// The hash keeps hosts apart even when their slugs collide.
|
|
30
|
+
expect(lockFilePathFor('localhost:9010')).not.toBe(lockFilePathFor('localhost-9010'));
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
5
|
+
const { lockFilePathFor } = require('./emulatorLock') as { lockFilePathFor: (host: string) => string };
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The emulator lock's FILE NAME is a contract between every copy of the lock: two jest runs
|
|
9
|
+
* serialize on a shared emulator only when they compute the same file for the same host. No
|
|
10
|
+
* shared package carries the lock yet, so the contract is pinned by value — this table is the
|
|
11
|
+
* same, row for row, in every repository that keeps a copy. A change to a row here is a change
|
|
12
|
+
* to all of them, made in the same window; a copy that drifts stops contending, silently.
|
|
13
|
+
*/
|
|
14
|
+
// [emulator host, lock file name, what the row pins]
|
|
15
|
+
const LOCK_FILE_NAMES: [string, string, string][] = [
|
|
16
|
+
['localhost:9010', 'spanner-emulator-localhost-9010-0091e79a.lock', 'the default shared emulator'],
|
|
17
|
+
[
|
|
18
|
+
'localhost-9010',
|
|
19
|
+
'spanner-emulator-localhost-9010-493b66d1.lock',
|
|
20
|
+
'the same slug as the row above: the hash alone keeps two hosts apart',
|
|
21
|
+
],
|
|
22
|
+
[
|
|
23
|
+
'[emulator.a-host-name-long-enough-to-run-past-the-sixty-four-cap.example.test]:9010',
|
|
24
|
+
'spanner-emulator-emulator-a-host-name-long-enough-to-run-past-the-sixty-four-cap--fe958a1a.lock',
|
|
25
|
+
'edge dashes trimmed first, the slug capped at 64 second (the cap lands on a dash, and it stays)',
|
|
26
|
+
],
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
describe('the emulator lock file name (the contract every copy of the lock shares)', () => {
|
|
30
|
+
it.each(LOCK_FILE_NAMES)('%s locks on %s (%s)', (host, fileName) => {
|
|
31
|
+
expect(lockFilePathFor(host)).toBe(path.join(os.tmpdir(), fileName));
|
|
32
|
+
});
|
|
33
|
+
});
|
package/test/setup.js
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
process.env.SPANNER_EMULATOR_HOST = process.env.SPANNER_EMULATOR_HOST || 'localhost:9010';
|
|
2
2
|
process.env.DB_LOG_LEVEL = 'error';
|
|
3
|
+
// The key the throttled doors' digests (RequestDigests) derive from; a process without one refuses to digest.
|
|
4
|
+
process.env.SESSION_SECRET = process.env.SESSION_SECRET || 'user-server-test-secret';
|