1
0

goframe.test.ts 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. /**
  2. * GoFrame route → controller-method coverage (#747), end to end.
  3. *
  4. * GoFrame binds routes reflectively, so the route declared in a request type's
  5. * `g.Meta` tag has no static edge to the controller method that serves it, and
  6. * the method name is NOT derivable from the request type (`DeptSearchReq` is
  7. * served by `List`). This indexes a fixture through the full pipeline and
  8. * checks: the `g.Meta` tags become route nodes; each route joins to its handler
  9. * by the request type in the method signature (the naming-mismatch case
  10. * included); a response (`mime`-only) `g.Meta` makes no route; a route with no
  11. * handler is left unlinked (silent beats wrong); and the response type never
  12. * produces a spurious edge.
  13. */
  14. import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  15. import * as fs from 'node:fs';
  16. import * as path from 'node:path';
  17. import * as os from 'node:os';
  18. import { CodeGraph } from '../src';
  19. describe('GoFrame route synthesizer', () => {
  20. let dir: string;
  21. beforeEach(() => { dir = fs.mkdtempSync(path.join(os.tmpdir(), 'goframe-')); });
  22. afterEach(() => { fs.rmSync(dir, { recursive: true, force: true }); });
  23. it('joins each g.Meta route to its controller method by the request-type signature', async () => {
  24. fs.writeFileSync(path.join(dir, 'go.mod'), 'module example.com/app\n\nrequire github.com/gogf/gf/v2 v2.7.0\n');
  25. fs.mkdirSync(path.join(dir, 'api', 'system'), { recursive: true });
  26. fs.writeFileSync(
  27. path.join(dir, 'api', 'system', 'dept.go'),
  28. `package system
  29. import "github.com/gogf/gf/v2/frame/g"
  30. type DeptSearchReq struct {
  31. g.Meta \`path:"/dept/list" tags:"Dept" method:"get" summary:"list"\`
  32. DeptName string
  33. }
  34. type DeptSearchRes struct {
  35. g.Meta \`mime:"application/json"\`
  36. List []string
  37. }
  38. type DeptAddReq struct {
  39. g.Meta \`path:"/dept/add" method:"post"\`
  40. Name string
  41. }
  42. type DeptAddRes struct{}
  43. // A declared route whose handler does not exist in this codebase.
  44. type OrphanReq struct {
  45. g.Meta \`path:"/orphan" method:"get"\`
  46. }
  47. type OrphanRes struct{}
  48. `
  49. );
  50. fs.mkdirSync(path.join(dir, 'internal', 'controller'), { recursive: true });
  51. fs.writeFileSync(
  52. path.join(dir, 'internal', 'controller', 'dept.go'),
  53. `package controller
  54. import (
  55. "context"
  56. "example.com/app/api/system"
  57. )
  58. type sysDeptController struct{}
  59. // NB: method name (List) differs from the request type (DeptSearchReq) — the join
  60. // must be by signature, not name.
  61. func (c *sysDeptController) List(ctx context.Context, req *system.DeptSearchReq) (res *system.DeptSearchRes, err error) {
  62. return helper(ctx)
  63. }
  64. func (c *sysDeptController) Add(ctx context.Context, req *system.DeptAddReq) (res *system.DeptAddRes, err error) {
  65. return
  66. }
  67. // Returns the response type but takes no request type — must NOT be linked.
  68. func helper(ctx context.Context) (res *system.DeptSearchRes, err error) {
  69. return
  70. }
  71. `
  72. );
  73. const cg = await CodeGraph.init(dir, { silent: true });
  74. await cg.indexAll();
  75. const db = (cg as any).db.db;
  76. const routes = db.prepare(`SELECT name FROM nodes WHERE kind='route' ORDER BY name`).all();
  77. const edges = db
  78. .prepare(
  79. `SELECT json_extract(e.metadata,'$.route') route, json_extract(e.metadata,'$.requestType') reqType,
  80. e.kind, t.name target_name, t.kind target_kind
  81. FROM edges e JOIN nodes t ON t.id = e.target
  82. WHERE json_extract(e.metadata,'$.synthesizedBy') = 'goframe-route'
  83. ORDER BY route`
  84. )
  85. .all();
  86. cg.close?.();
  87. // Three routes from path-bearing g.Meta; the mime-only response g.Meta makes none.
  88. expect(routes.map((r: any) => r.name)).toEqual(['GET /dept/list', 'GET /orphan', 'POST /dept/add']);
  89. // Two route→handler edges — the orphan route stays unlinked (silent beats wrong).
  90. expect(edges).toHaveLength(2);
  91. const byRoute = Object.fromEntries(edges.map((e: any) => [e.route, e]));
  92. // Naming mismatch resolved by signature: GET /dept/list → List.
  93. expect(byRoute['GET /dept/list'].target_name).toBe('List');
  94. expect(byRoute['GET /dept/list'].reqType).toBe('DeptSearchReq');
  95. expect(byRoute['POST /dept/add'].target_name).toBe('Add');
  96. // It is a dynamic-dispatch `calls` hop to a real method, never to the helper.
  97. expect(edges.every((e: any) => e.kind === 'calls' && e.target_kind === 'method')).toBe(true);
  98. expect(edges.some((e: any) => e.target_name === 'helper')).toBe(false);
  99. expect(byRoute['GET /orphan']).toBeUndefined();
  100. });
  101. it('disambiguates identical bare request types across modules by package qualifier', async () => {
  102. fs.writeFileSync(path.join(dir, 'go.mod'), 'module example.com/app\n\nrequire github.com/gogf/gf/v2 v2.7.0\n');
  103. // Two modules that BOTH define `type ListReq struct` — the collision a large
  104. // GoFrame app has dozens of. The package qualifier in the handler signature
  105. // (`*cash.ListReq` vs `*order.ListReq`) is the only thing that tells them apart.
  106. for (const mod of ['cash', 'order']) {
  107. fs.mkdirSync(path.join(dir, 'api', mod), { recursive: true });
  108. fs.writeFileSync(
  109. path.join(dir, 'api', mod, `${mod}.go`),
  110. `package ${mod}
  111. import "github.com/gogf/gf/v2/frame/g"
  112. type ListReq struct {
  113. g.Meta \`path:"/${mod}/list" method:"get"\`
  114. }
  115. type ListRes struct{}
  116. `
  117. );
  118. fs.mkdirSync(path.join(dir, 'internal', 'controller', mod), { recursive: true });
  119. fs.writeFileSync(
  120. path.join(dir, 'internal', 'controller', mod, `${mod}.go`),
  121. `package ${mod}
  122. import (
  123. "context"
  124. "example.com/app/api/${mod}"
  125. )
  126. type c${mod} struct{}
  127. func (c *c${mod}) List(ctx context.Context, req *${mod}.ListReq) (res *${mod}.ListRes, err error) {
  128. return
  129. }
  130. `
  131. );
  132. }
  133. const cg = await CodeGraph.init(dir, { silent: true });
  134. await cg.indexAll();
  135. const db = (cg as any).db.db;
  136. const rows = db
  137. .prepare(
  138. `SELECT json_extract(e.metadata,'$.route') route, t.file_path handler_file
  139. FROM edges e JOIN nodes t ON t.id = e.target
  140. WHERE json_extract(e.metadata,'$.synthesizedBy') = 'goframe-route'
  141. ORDER BY route`
  142. )
  143. .all();
  144. cg.close?.();
  145. expect(rows).toHaveLength(2);
  146. // Each route binds to ITS OWN module's handler, never the other's.
  147. const byRoute = Object.fromEntries(rows.map((r: any) => [r.route, r.handler_file]));
  148. expect(byRoute['GET /cash/list']).toContain('controller/cash/');
  149. expect(byRoute['GET /order/list']).toContain('controller/order/');
  150. });
  151. });