Refactor API and enhance Angular integration
- Removed `ciTargetName` from `nx.json`. - Updated `package.json` to include new dependencies: `@types/seedrandom`, `fast-check`, `happy-dom`, and `@nestjs/schedule`. - Modified `pnpm-lock.yaml` to reflect the addition of new packages and their versions. - Improved project documentation in `PROJECT_CONTEXT.md` to clarify the use of Zod schemas and Angular framework decisions. - Introduced new Angular components and patterns in the `.agents/skills/frontend-angular` directory, including examples and reference materials for Angular 21+ features.
This commit is contained in:
68
apps/overlay/src/app/ebs/auth.interceptor.spec.ts
Normal file
68
apps/overlay/src/app/ebs/auth.interceptor.spec.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
||||
import {
|
||||
HttpTestingController,
|
||||
provideHttpClientTesting,
|
||||
} from '@angular/common/http/testing';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { TwitchAuthService } from '../twitch/twitch-auth.service';
|
||||
import { authInterceptor } from './auth.interceptor';
|
||||
import { EbsApiService, EBS_BASE_URL } from './ebs-api.service';
|
||||
|
||||
function makeJwt(): string {
|
||||
const payload = btoa(JSON.stringify({ opaque_user_id: 'U123', channel_id: 'ch1', role: 'viewer', exp: 9999999999 }))
|
||||
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
||||
return `eyJhbGciOiJIUzI1NiJ9.${payload}.sig`;
|
||||
}
|
||||
|
||||
describe('authInterceptor', () => {
|
||||
let controller: HttpTestingController;
|
||||
let authService: TwitchAuthService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
provideHttpClient(withInterceptors([authInterceptor])),
|
||||
provideHttpClientTesting(),
|
||||
{ provide: EBS_BASE_URL, useValue: 'https://test.local' },
|
||||
],
|
||||
});
|
||||
controller = TestBed.inject(HttpTestingController);
|
||||
authService = TestBed.inject(TwitchAuthService);
|
||||
});
|
||||
|
||||
afterEach(() => controller.verify());
|
||||
|
||||
it('omits Authorization header when not yet authorized', () => {
|
||||
const svc = TestBed.inject(EbsApiService);
|
||||
svc.getMissionState().subscribe();
|
||||
|
||||
const req = controller.expectOne('https://test.local/missions/state');
|
||||
expect(req.request.headers.has('Authorization')).toBe(false);
|
||||
req.flush(null);
|
||||
});
|
||||
|
||||
it('attaches Bearer token after onAuthorized fires', () => {
|
||||
const jwt = makeJwt();
|
||||
(window as Window & { Twitch?: unknown }).Twitch = {
|
||||
ext: {
|
||||
onAuthorized: (cb: (a: TwitchAuth) => void) =>
|
||||
cb({ channelId: 'ch1', clientId: 'c1', token: jwt, userId: 'U123' }),
|
||||
onContext: () => undefined,
|
||||
onVisibilityChanged: () => undefined,
|
||||
listen: () => undefined,
|
||||
unlisten: () => undefined,
|
||||
send: () => undefined,
|
||||
},
|
||||
};
|
||||
authService.init();
|
||||
|
||||
const svc = TestBed.inject(EbsApiService);
|
||||
svc.getMissionState().subscribe();
|
||||
|
||||
const req = controller.expectOne('https://test.local/missions/state');
|
||||
expect(req.request.headers.get('Authorization')).toBe(`Bearer ${jwt}`);
|
||||
req.flush(null);
|
||||
|
||||
delete (window as Window & { Twitch?: unknown }).Twitch;
|
||||
});
|
||||
});
|
||||
9
apps/overlay/src/app/ebs/auth.interceptor.ts
Normal file
9
apps/overlay/src/app/ebs/auth.interceptor.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { HttpInterceptorFn } from '@angular/common/http';
|
||||
import { inject } from '@angular/core';
|
||||
import { TwitchAuthService } from '../twitch/twitch-auth.service';
|
||||
|
||||
export const authInterceptor: HttpInterceptorFn = (req, next) => {
|
||||
const token = inject(TwitchAuthService).auth()?.token;
|
||||
if (!token) return next(req);
|
||||
return next(req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }));
|
||||
};
|
||||
78
apps/overlay/src/app/ebs/ebs-api.service.spec.ts
Normal file
78
apps/overlay/src/app/ebs/ebs-api.service.spec.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import {
|
||||
HttpTestingController,
|
||||
provideHttpClientTesting,
|
||||
} from '@angular/common/http/testing';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { EbsApiService, EBS_BASE_URL } from './ebs-api.service';
|
||||
|
||||
const BASE = 'https://test.local';
|
||||
|
||||
describe('EbsApiService', () => {
|
||||
let service: EbsApiService;
|
||||
let controller: HttpTestingController;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
{ provide: EBS_BASE_URL, useValue: BASE },
|
||||
],
|
||||
});
|
||||
service = TestBed.inject(EbsApiService);
|
||||
controller = TestBed.inject(HttpTestingController);
|
||||
});
|
||||
|
||||
afterEach(() => controller.verify());
|
||||
|
||||
describe('getMissionState', () => {
|
||||
it('GETs /missions/state and returns null for no active mission', () => {
|
||||
let result: unknown;
|
||||
service.getMissionState().subscribe((v) => (result = v));
|
||||
|
||||
controller.expectOne(`${BASE}/missions/state`).flush(null);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('throws ZodError when server returns invalid shape', () => {
|
||||
let error: unknown;
|
||||
service.getMissionState().subscribe({ error: (e) => (error = e) });
|
||||
|
||||
controller.expectOne(`${BASE}/missions/state`).flush({ bad: 'data' });
|
||||
expect(error).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('startMission', () => {
|
||||
it('POSTs to /missions/start with difficulty', () => {
|
||||
service.startMission({ difficulty: 2 }).subscribe({ error: () => undefined });
|
||||
|
||||
const req = controller.expectOne(`${BASE}/missions/start`);
|
||||
expect(req.request.method).toBe('POST');
|
||||
expect(req.request.body).toEqual({ difficulty: 2 });
|
||||
req.flush({ bad: 'shape' });
|
||||
});
|
||||
|
||||
it('throws ZodError for invalid difficulty before sending request', () => {
|
||||
expect(() => service.startMission({ difficulty: 99 })).toThrow();
|
||||
controller.expectNone(`${BASE}/missions/start`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('choosePerk', () => {
|
||||
it('POSTs to /missions/choose-perk with perkKey', () => {
|
||||
service.choosePerk({ perkKey: 'iron_will' }).subscribe({ error: () => undefined });
|
||||
|
||||
const req = controller.expectOne(`${BASE}/missions/choose-perk`);
|
||||
expect(req.request.method).toBe('POST');
|
||||
expect(req.request.body).toEqual({ perkKey: 'iron_will' });
|
||||
req.flush({ bad: 'shape' });
|
||||
});
|
||||
|
||||
it('throws ZodError for empty perkKey before sending request', () => {
|
||||
expect(() => service.choosePerk({ perkKey: '' })).toThrow();
|
||||
controller.expectNone(`${BASE}/missions/choose-perk`);
|
||||
});
|
||||
});
|
||||
});
|
||||
41
apps/overlay/src/app/ebs/ebs-api.service.ts
Normal file
41
apps/overlay/src/app/ebs/ebs-api.service.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { inject, Injectable, InjectionToken } from '@angular/core';
|
||||
import {
|
||||
ChoosePerkRequest,
|
||||
ChoosePerkRequestSchema,
|
||||
MissionStateResponse,
|
||||
MissionStateResponseSchema,
|
||||
MissionSchema,
|
||||
StartMissionRequest,
|
||||
StartMissionRequestSchema,
|
||||
SurvivorSchema,
|
||||
} from '@fog-explorer/api-interfaces';
|
||||
import { map, Observable } from 'rxjs';
|
||||
|
||||
export const EBS_BASE_URL = new InjectionToken<string>('EBS_BASE_URL');
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class EbsApiService {
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly baseUrl = inject(EBS_BASE_URL, { optional: true }) ?? 'https://localhost:3000';
|
||||
|
||||
getMissionState(): Observable<MissionStateResponse> {
|
||||
return this.http
|
||||
.get(`${this.baseUrl}/missions/state`)
|
||||
.pipe(map((body) => MissionStateResponseSchema.parse(body)));
|
||||
}
|
||||
|
||||
startMission(req: StartMissionRequest) {
|
||||
StartMissionRequestSchema.parse(req);
|
||||
return this.http
|
||||
.post(`${this.baseUrl}/missions/start`, req)
|
||||
.pipe(map((body) => MissionSchema.parse(body)));
|
||||
}
|
||||
|
||||
choosePerk(req: ChoosePerkRequest) {
|
||||
ChoosePerkRequestSchema.parse(req);
|
||||
return this.http
|
||||
.post(`${this.baseUrl}/missions/choose-perk`, req)
|
||||
.pipe(map((body) => SurvivorSchema.parse(body)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user