create-astram-app 0.4.0 → 0.5.0

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.
@@ -1,5 +1,13 @@
1
- import type { INestApplication } from '@nestjs/common';
1
+ import { Controller, Get, type INestApplication, type ModuleMetadata } from '@nestjs/common';
2
+ import { DiscoveryModule } from '@nestjs/core';
2
3
  import { Test } from '@nestjs/testing';
4
+ import { checkCompleteness, createModuleRegistry } from '@astram/core';
5
+ import {
6
+ CatalogGatabilityError,
7
+ CatalogReconciler,
8
+ OperationInventoryScanner,
9
+ ShieldModule,
10
+ } from '@astram/guard';
3
11
  import request from 'supertest';
4
12
  import { afterAll, beforeAll, describe, expect, it } from 'vitest';
5
13
  import { AppModule } from '../src/app.module';
@@ -46,11 +54,70 @@ describe('__APP_NAME__ — framework enforcement + health (e2e)', () => {
46
54
  message: expect.stringContaining('public route reachable'),
47
55
  });
48
56
  });
57
+ });
58
+
59
+ /**
60
+ * A deliberately UNCLASSIFIED controller (no @Public(), no @Classified()). It lives
61
+ * only in this test file — never in src/ — because a live unclassified route would
62
+ * abort a real boot (see demo.controller.ts). Used to prove both deny-by-default layers.
63
+ */
64
+ @Controller('probe')
65
+ class UnclassifiedProbeController {
66
+ @Get('unclassified')
67
+ unclassifiedRoute(): { message: string } {
68
+ return { message: 'this body must never be reached — the guard denies first' };
69
+ }
70
+ }
71
+
72
+ /** Reflect every mounted route of `imports`/`controllers` into an OperationInventory. */
73
+ async function scanRoutes(opts: Pick<ModuleMetadata, 'imports' | 'controllers'>) {
74
+ const moduleRef = await Test.createTestingModule({
75
+ imports: [DiscoveryModule, ...(opts.imports ?? [])],
76
+ controllers: opts.controllers ?? [],
77
+ providers: [OperationInventoryScanner],
78
+ }).compile();
79
+ const inventory = moduleRef.get(OperationInventoryScanner).scan('app');
80
+ await moduleRef.close();
81
+ return inventory;
82
+ }
49
83
 
50
- it('GET /demo/unclassified is DENIED (403) — deny-by-default (ADR-0002)', async () => {
51
- const res = await request(app.getHttpServer()).get('/demo/unclassified');
52
- expect(res.status).toBe(403);
53
- // The guard denies BEFORE the handler body runs.
54
- expect(res.body.message).toContain('no access classification');
84
+ describe('__APP_NAME__ — deny-by-default (ADR-0002)', () => {
85
+ it('BOOT layer: an unclassified route ABORTS boot via verifyGatable (fail-closed)', async () => {
86
+ const inventory = await scanRoutes({ controllers: [UnclassifiedProbeController] });
87
+ // The same check the identity boot provider runs: a forgotten declaration means the
88
+ // app refuses to start, not that the route silently serves traffic.
89
+ expect(() => new CatalogReconciler().verifyGatable(inventory, createModuleRegistry())).toThrow(
90
+ CatalogGatabilityError,
91
+ );
92
+ expect(() => new CatalogReconciler().verifyGatable(inventory, createModuleRegistry())).toThrow(
93
+ /UnclassifiedProbeController#unclassifiedRoute/,
94
+ );
95
+ });
96
+
97
+ it('BOOT layer: every route in YOUR AppModule is @Public() or @Classified() (none stranded)', async () => {
98
+ // Fails the moment a route is added without a classification — the same routes the
99
+ // boot check would strand. (Registering @Classified keys is the identity module's job,
100
+ // so only the "forgotten declaration" half is asserted here.)
101
+ const inventory = await scanRoutes({ imports: [AppModule] });
102
+ expect(inventory.operations.length).toBeGreaterThan(0);
103
+ const { strandedOperations } = checkCompleteness(inventory, createModuleRegistry());
104
+ expect(strandedOperations.map((o) => o.operationId)).toEqual([]);
105
+ });
106
+
107
+ it('REQUEST layer: an unclassified route is DENIED (403) before its handler runs', async () => {
108
+ const moduleRef = await Test.createTestingModule({
109
+ imports: [ShieldModule.forRoot()],
110
+ controllers: [UnclassifiedProbeController],
111
+ }).compile();
112
+ const probeApp = moduleRef.createNestApplication();
113
+ await probeApp.init();
114
+ try {
115
+ const res = await request(probeApp.getHttpServer()).get('/probe/unclassified');
116
+ expect(res.status).toBe(403);
117
+ // The guard denies BEFORE the handler body runs.
118
+ expect(res.body.message).toContain('no access classification');
119
+ } finally {
120
+ await probeApp.close();
121
+ }
55
122
  });
56
123
  });