125 lines
4.7 KiB
JavaScript
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);
|
||
|
|
});
|