Files
vitanovaPackages/packages/auth/test/admin-credentials-login.test.mjs
sdarbinyan 3bc2a42488 feat(auth): add AdminAuthService.loginWithCredentials + rate_limited AuthFailure code
Convenience wrapper around MarketplacesAuthGateway.loginWithCredentials('admin', ...)
that activates the returned session in one call, for apps building a custom admin
login screen. AuthFailure gains a rate_limited code (HTTP 429, parsed Retry-After)
and an optional status field so 401/403/429 surface distinctly without new plumbing.

No credentials, no client-side comparison, no superadmin concept anywhere in this
package - same code path as any other admin credential login. Inert without a
backend implementing POST {credentialsPath} { login, password, mode: 'admin' }.

Additive, backward-compatible. QR/Yandex flows unmodified.
2026-08-23 21:33:52 +04:00

125 lines
4.7 KiB
JavaScript

import '@angular/compiler';
import assert from 'node:assert/strict';
import test from 'node:test';
import { Injector } from '@angular/core';
import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
import { of, throwError } from 'rxjs';
import {
AdminAuthService,
AuthService,
AuthMarketplaceContext,
TelegramSessionApiService,
HttpMarketplacesAuthGateway,
MARKETPLACES_AUTH_GATEWAY,
MARKETPLACES_AUTH_CONFIG,
AUTH_API_URL,
} from '../dist/fesm2022/marketplaces-auth.mjs';
const API_URL = 'https://api.example.com';
const CREDENTIALS_URL = `${API_URL}/auth/credentials/login`;
/** Wires up the real gateway/context/service classes with a scripted HttpClient - no TestBed, no NgModule compiler needed. */
function setup(postResponse) {
const calls = [];
const fakeHttp = { post: (url, body, opts) => { calls.push({ url, body, opts }); return postResponse; } };
const injector = Injector.create({
providers: [
{ provide: HttpClient, useValue: fakeHttp },
{ provide: MARKETPLACES_AUTH_CONFIG, useValue: {
apiUrl: API_URL,
marketplaceDomain: 'admin.tenant-a.example.com',
credentialsPath: '/auth/credentials/login',
} },
{ provide: AUTH_API_URL, useValue: API_URL },
AuthMarketplaceContext,
TelegramSessionApiService,
AuthService,
AdminAuthService,
HttpMarketplacesAuthGateway,
{ provide: MARKETPLACES_AUTH_GATEWAY, useExisting: HttpMarketplacesAuthGateway },
],
});
return { admin: injector.get(AdminAuthService), calls };
}
test('loginWithCredentials: success activates the returned session and stores tokens', () => {
const backendResponse = {
session: {
sessionId: 's1', userId: 1, username: 'root', displayName: 'Root',
active: true, expires: new Date(Date.now() + 60_000).toISOString(),
},
accessToken: 'jwt-access',
refreshToken: 'jwt-refresh',
};
const { admin, calls } = setup(of(backendResponse));
let result;
admin.loginWithCredentials({ login: 'root', password: 'secret' }).subscribe(r => (result = r));
assert.equal(calls.length, 1);
assert.equal(calls[0].url, CREDENTIALS_URL);
assert.deepEqual(calls[0].body, { login: 'root', password: 'secret', mode: 'admin' });
assert.equal(calls[0].opts.headers.get('X-Marketplace-Domain'), 'admin.tenant-a.example.com');
assert.equal(admin.isAuthenticated(), true);
assert.equal(admin.session()?.sessionId, 's1');
// getAdminToken()/setAdminTokens() are guarded no-ops without a `localStorage`
// (e.g. this Node test runner), same as production SSR - token storage
// itself isn't under test here, just that acceptSession() was invoked.
assert.equal(result.accessToken, 'jwt-access');
assert.equal(result.mode, 'admin');
assert.equal(result.method, 'credentials');
});
test('loginWithCredentials: 401 surfaces invalid_credentials and does not mutate session state', () => {
const error = new HttpErrorResponse({ status: 401, statusText: 'Unauthorized', error: { message: 'Invalid login or password' } });
const { admin } = setup(throwError(() => error));
let failure;
admin.loginWithCredentials({ login: 'root', password: 'wrong' }).subscribe({
next: () => assert.fail('should not succeed'),
error: err => (failure = err),
});
assert.equal(failure.code, 'invalid_credentials');
assert.equal(failure.status, 401);
assert.equal(admin.isAuthenticated(), false);
assert.equal(admin.session(), null);
assert.equal(admin.getAdminToken(), null);
});
test('loginWithCredentials: 429 surfaces rate_limited with parsed Retry-After', () => {
const error = new HttpErrorResponse({
status: 429, statusText: 'Too Many Requests', error: { message: 'Too many attempts' },
headers: new HttpHeaders({ 'Retry-After': '30' }),
});
const { admin } = setup(throwError(() => error));
let failure;
admin.loginWithCredentials({ login: 'root', password: 'secret' }).subscribe({
next: () => assert.fail('should not succeed'),
error: err => (failure = err),
});
assert.equal(failure.code, 'rate_limited');
assert.equal(failure.retryAfterSeconds, 30);
assert.equal(admin.isAuthenticated(), false);
});
test('loginWithCredentials: 403 surfaces as a generic backend failure', () => {
const error = new HttpErrorResponse({ status: 403, statusText: 'Forbidden', error: { message: 'Tenant disabled' } });
const { admin } = setup(throwError(() => error));
let failure;
admin.loginWithCredentials({ login: 'root', password: 'secret' }).subscribe({
next: () => assert.fail('should not succeed'),
error: err => (failure = err),
});
assert.equal(failure.code, 'backend');
assert.equal(failure.status, 403);
assert.equal(admin.isAuthenticated(), false);
});