Merge branch 'B2B'
This commit is contained in:
146
src/app/features/admin/users/facade/admin-users.facade.spec.ts
Normal file
146
src/app/features/admin/users/facade/admin-users.facade.spec.ts
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { of, throwError } from 'rxjs';
|
||||||
|
import { AdminUsersFacade } from './admin-users.facade';
|
||||||
|
import { ADMIN_USERS_GATEWAY } from '../services/admin-users-gateway.token';
|
||||||
|
import { AdminUsersGateway } from '../services/admin-users-gateway.interface';
|
||||||
|
import { AdminUser, AdminUserRoleRecord } from '../models/admin-user.model';
|
||||||
|
|
||||||
|
function user(overrides: Partial<AdminUser> = {}): AdminUser {
|
||||||
|
return { id: 'u1', name: 'Alice', email: 'a@example.com', roleId: 'role1', scope: 'marketplace', status: 'active', ...overrides } as AdminUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('AdminUsersFacade', () => {
|
||||||
|
let facade: AdminUsersFacade;
|
||||||
|
let gateway: jasmine.SpyObj<AdminUsersGateway>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
gateway = jasmine.createSpyObj<AdminUsersGateway>('AdminUsersGateway', [
|
||||||
|
'loadUsers', 'loadRoles', 'loadInvitations', 'loadSessions', 'loadAudit',
|
||||||
|
'setUserRole', 'setUserStatus', 'inviteUser', 'revokeInvitation', 'revokeSession',
|
||||||
|
]);
|
||||||
|
TestBed.configureTestingModule({ providers: [{ provide: ADMIN_USERS_GATEWAY, useValue: gateway }] });
|
||||||
|
facade = TestBed.inject(AdminUsersFacade);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loadAll populates users, roles, and invitations independently', () => {
|
||||||
|
gateway.loadUsers.and.returnValue(of([user()]));
|
||||||
|
gateway.loadRoles.and.returnValue(of([{ id: 'role1', name: 'Manager' } as AdminUserRoleRecord]));
|
||||||
|
gateway.loadInvitations.and.returnValue(of([]));
|
||||||
|
|
||||||
|
facade.loadAll();
|
||||||
|
|
||||||
|
expect(facade.users().length).toBe(1);
|
||||||
|
expect(facade.roles().length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('a users-load failure clears users and sets error, without blocking roles/invitations', () => {
|
||||||
|
gateway.loadUsers.and.returnValue(throwError(() => new Error('x')));
|
||||||
|
gateway.loadRoles.and.returnValue(of([{ id: 'role1', name: 'Manager' } as AdminUserRoleRecord]));
|
||||||
|
gateway.loadInvitations.and.returnValue(of([]));
|
||||||
|
|
||||||
|
facade.loadAll();
|
||||||
|
|
||||||
|
expect(facade.users()).toEqual([]);
|
||||||
|
expect(facade.error()).toBeTrue();
|
||||||
|
expect(facade.roles().length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('roleName', () => {
|
||||||
|
it('resolves a known role id to its display name', () => {
|
||||||
|
gateway.loadUsers.and.returnValue(of([]));
|
||||||
|
gateway.loadRoles.and.returnValue(of([{ id: 'role1', name: 'Manager' } as AdminUserRoleRecord]));
|
||||||
|
gateway.loadInvitations.and.returnValue(of([]));
|
||||||
|
facade.loadAll();
|
||||||
|
|
||||||
|
expect(facade.roleName('role1')).toBe('Manager');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to the raw id when the role is unknown, rather than showing blank', () => {
|
||||||
|
expect(facade.roleName('mystery-role')).toBe('mystery-role');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('invite', () => {
|
||||||
|
it('does not call the gateway for a whitespace-only email', () => {
|
||||||
|
facade.invite(' ', 'role1', 'marketplace');
|
||||||
|
|
||||||
|
expect(gateway.inviteUser).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('trims the email before sending it', () => {
|
||||||
|
gateway.inviteUser.and.returnValue(of({} as any));
|
||||||
|
gateway.loadUsers.and.returnValue(of([]));
|
||||||
|
gateway.loadRoles.and.returnValue(of([]));
|
||||||
|
gateway.loadInvitations.and.returnValue(of([]));
|
||||||
|
|
||||||
|
facade.invite(' a@example.com ', 'role1', 'marketplace');
|
||||||
|
|
||||||
|
expect(gateway.inviteUser).toHaveBeenCalledWith('a@example.com', 'role1', 'marketplace');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets mutationError on failure', () => {
|
||||||
|
gateway.inviteUser.and.returnValue(throwError(() => new Error('x')));
|
||||||
|
|
||||||
|
facade.invite('a@example.com', 'role1', 'marketplace');
|
||||||
|
|
||||||
|
expect(facade.mutationError()).toBe('common.errorDescription');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sessions dialog', () => {
|
||||||
|
it('openSessions sets the target and loads that user\'s sessions', () => {
|
||||||
|
gateway.loadSessions.and.returnValue(of([{ id: 's1' } as any]));
|
||||||
|
|
||||||
|
facade.openSessions(user());
|
||||||
|
|
||||||
|
expect(facade.sessionsTarget()?.id).toBe('u1');
|
||||||
|
expect(facade.sessions().length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('closeSessions clears the target', () => {
|
||||||
|
gateway.loadSessions.and.returnValue(of([]));
|
||||||
|
facade.openSessions(user());
|
||||||
|
|
||||||
|
facade.closeSessions();
|
||||||
|
|
||||||
|
expect(facade.sessionsTarget()).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('revokeSession reloads sessions for the currently open user, not a stale one', () => {
|
||||||
|
gateway.loadSessions.and.returnValue(of([]));
|
||||||
|
gateway.revokeSession.and.returnValue(of(undefined));
|
||||||
|
facade.openSessions(user({ id: 'u1' }));
|
||||||
|
|
||||||
|
facade.revokeSession('s1');
|
||||||
|
|
||||||
|
expect(gateway.loadSessions).toHaveBeenCalledWith('u1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('revokeSession does nothing extra when no sessions dialog is open', () => {
|
||||||
|
gateway.revokeSession.and.returnValue(of(undefined));
|
||||||
|
|
||||||
|
facade.revokeSession('s1');
|
||||||
|
|
||||||
|
expect(gateway.loadSessions).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('audit dialog', () => {
|
||||||
|
it('openAudit loads entries for the target user', () => {
|
||||||
|
gateway.loadAudit.and.returnValue(of([{ id: 'a1' } as any]));
|
||||||
|
|
||||||
|
facade.openAudit(user());
|
||||||
|
|
||||||
|
expect(facade.audit().length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('closeAudit clears the target', () => {
|
||||||
|
gateway.loadAudit.and.returnValue(of([]));
|
||||||
|
facade.openAudit(user());
|
||||||
|
|
||||||
|
facade.closeAudit();
|
||||||
|
|
||||||
|
expect(facade.auditTarget()).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
198
src/app/services/cart.service.spec.ts
Normal file
198
src/app/services/cart.service.spec.ts
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { CartService } from './cart.service';
|
||||||
|
import { LocalStorageService } from '../core/storage/local-storage.service';
|
||||||
|
import { CartItem, DeliveryOption } from '../models';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* cart.service.ts had no spec before this. It feeds totalWithDelivery and
|
||||||
|
* allRequiredDeliveriesSelected directly into cart.component.ts's checkout
|
||||||
|
* gate (isCheckoutDisabled) and into the offers/qty payload sent to
|
||||||
|
* POST /api/v2/storefront/checkout - the exact contract F14-F16 rewired
|
||||||
|
* this session. A regression here would silently let checkout proceed
|
||||||
|
* with missing delivery selections, or compute the wrong cart total.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function cartItem(overrides: Partial<CartItem> = {}): CartItem {
|
||||||
|
return {
|
||||||
|
categoryID: 1,
|
||||||
|
itemID: 1,
|
||||||
|
name: 'Widget',
|
||||||
|
photos: null,
|
||||||
|
description: '',
|
||||||
|
currency: 'RUB',
|
||||||
|
price: 1000,
|
||||||
|
discount: 0,
|
||||||
|
rating: 0,
|
||||||
|
callbacks: null,
|
||||||
|
questions: null,
|
||||||
|
quantity: 1,
|
||||||
|
...overrides,
|
||||||
|
} as CartItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
function delivery(overrides: Partial<DeliveryOption> = {}): DeliveryOption {
|
||||||
|
return { deliveryPrice: 0, deliveryPlace: 'Standard', deliveryTime: '3-5 days', ...overrides };
|
||||||
|
}
|
||||||
|
|
||||||
|
function createService(seedItems: CartItem[] = []): CartService {
|
||||||
|
const storageStub = {
|
||||||
|
getItem: (key: string) => (key === 'marketplace_cart' ? JSON.stringify(seedItems) : null),
|
||||||
|
setItem: () => {},
|
||||||
|
getJSON: () => null,
|
||||||
|
setJSON: () => {},
|
||||||
|
};
|
||||||
|
TestBed.configureTestingModule({ providers: [{ provide: LocalStorageService, useValue: storageStub }] });
|
||||||
|
return TestBed.inject(CartService);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('CartService', () => {
|
||||||
|
describe('totalPrice', () => {
|
||||||
|
it('sums price times quantity across items', () => {
|
||||||
|
const service = createService([cartItem({ itemID: 1, price: 100, quantity: 2 }), cartItem({ itemID: 2, price: 50, quantity: 3 })]);
|
||||||
|
|
||||||
|
expect(service.totalPrice()).toBe(350);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies the item discount before multiplying by quantity', () => {
|
||||||
|
const service = createService([cartItem({ price: 100, discount: 10, quantity: 2 })]);
|
||||||
|
|
||||||
|
// getDiscountedPrice: 100 * (1 - 0.10) = 90, times qty 2 = 180
|
||||||
|
expect(service.totalPrice()).toBe(180);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is 0 for an empty cart', () => {
|
||||||
|
const service = createService([]);
|
||||||
|
|
||||||
|
expect(service.totalPrice()).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('totalDeliveryPrice / totalWithDelivery', () => {
|
||||||
|
it('adds selectedDelivery price times quantity', () => {
|
||||||
|
const service = createService([cartItem({ price: 100, quantity: 2, selectedDelivery: delivery({ deliveryPrice: 20 }) })]);
|
||||||
|
|
||||||
|
expect(service.totalDeliveryPrice()).toBe(40);
|
||||||
|
expect(service.totalWithDelivery()).toBe(240);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('items with no selectedDelivery contribute 0 delivery cost', () => {
|
||||||
|
const service = createService([cartItem({ price: 100, quantity: 1 })]);
|
||||||
|
|
||||||
|
expect(service.totalDeliveryPrice()).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('allRequiredDeliveriesSelected', () => {
|
||||||
|
it('is true when no item requires a delivery selection', () => {
|
||||||
|
const service = createService([cartItem({ deliveryOptions: [] })]);
|
||||||
|
|
||||||
|
expect(service.allRequiredDeliveriesSelected()).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is false when an item has delivery options but none selected', () => {
|
||||||
|
const service = createService([cartItem({ deliveryOptions: [delivery()], selectedDelivery: undefined })]);
|
||||||
|
|
||||||
|
expect(service.allRequiredDeliveriesSelected()).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is true once a required delivery has been selected', () => {
|
||||||
|
const service = createService([cartItem({ deliveryOptions: [delivery()], selectedDelivery: delivery() })]);
|
||||||
|
|
||||||
|
expect(service.allRequiredDeliveriesSelected()).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('digital items never require a delivery selection, regardless of deliveryOptions', () => {
|
||||||
|
const service = createService([cartItem({ deliveryMode: 'digital', deliveryOptions: [delivery()] })]);
|
||||||
|
|
||||||
|
expect(service.allRequiredDeliveriesSelected()).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deliverySelectionRequired: false overrides having deliveryOptions present', () => {
|
||||||
|
const service = createService([cartItem({ deliveryOptions: [delivery()], deliverySelectionRequired: false })]);
|
||||||
|
|
||||||
|
expect(service.allRequiredDeliveriesSelected()).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('this is the exact gate cart.component.ts checks before allowing checkout', () => {
|
||||||
|
const service = createService([cartItem({ deliveryOptions: [delivery()] })]);
|
||||||
|
|
||||||
|
// Mirrors CartComponent.isCheckoutDisabled's third condition.
|
||||||
|
expect(!service.allRequiredDeliveriesSelected()).toBeTrue();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('itemCount', () => {
|
||||||
|
it('sums quantities, not the number of distinct lines', () => {
|
||||||
|
const service = createService([cartItem({ itemID: 1, quantity: 2 }), cartItem({ itemID: 2, quantity: 3 })]);
|
||||||
|
|
||||||
|
expect(service.itemCount()).toBe(5);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateQuantity', () => {
|
||||||
|
it('updates the quantity of the matching line', () => {
|
||||||
|
const service = createService([cartItem({ itemID: 1, quantity: 1 })]);
|
||||||
|
|
||||||
|
service.updateQuantity(1, 5);
|
||||||
|
|
||||||
|
expect(service.items()[0].quantity).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes the line entirely when quantity drops to 0, rather than storing a zero-quantity line', () => {
|
||||||
|
const service = createService([cartItem({ itemID: 1, quantity: 1 })]);
|
||||||
|
|
||||||
|
service.updateQuantity(1, 0);
|
||||||
|
|
||||||
|
expect(service.items().length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setSelectedDelivery', () => {
|
||||||
|
it('sets the delivery option on the matching item only', () => {
|
||||||
|
const service = createService([cartItem({ itemID: 1 }), cartItem({ itemID: 2 })]);
|
||||||
|
|
||||||
|
service.setSelectedDelivery(1, delivery({ deliveryPlace: 'Express' }));
|
||||||
|
|
||||||
|
expect(service.items().find(i => i.itemID === 1)?.selectedDelivery?.deliveryPlace).toBe('Express');
|
||||||
|
expect(service.items().find(i => i.itemID === 2)?.selectedDelivery).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeItem / removeItems', () => {
|
||||||
|
it('removeItem drops the matching line by itemID when no variant is given', () => {
|
||||||
|
const service = createService([cartItem({ itemID: 1 }), cartItem({ itemID: 2 })]);
|
||||||
|
|
||||||
|
service.removeItem(1);
|
||||||
|
|
||||||
|
expect(service.items().map(i => i.itemID)).toEqual([2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removeItems drops every listed id in one call', () => {
|
||||||
|
const service = createService([cartItem({ itemID: 1 }), cartItem({ itemID: 2 }), cartItem({ itemID: 3 })]);
|
||||||
|
|
||||||
|
service.removeItems([1, 3]);
|
||||||
|
|
||||||
|
expect(service.items().map(i => i.itemID)).toEqual([2]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clearCart', () => {
|
||||||
|
it('empties the cart', () => {
|
||||||
|
const service = createService([cartItem()]);
|
||||||
|
|
||||||
|
service.clearCart();
|
||||||
|
|
||||||
|
expect(service.items().length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addItem - existing line fast path', () => {
|
||||||
|
it('increases quantity synchronously when the item is already in the cart, without an API call', async () => {
|
||||||
|
const service = createService([cartItem({ itemID: 1, quantity: 1 })]);
|
||||||
|
|
||||||
|
await service.addItem(1, 2);
|
||||||
|
|
||||||
|
expect(service.items()[0].quantity).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user