|
| 1 | +/** |
| 2 | + * Regression coverage for #3339. |
| 3 | + * |
| 4 | + * `setPagination()` / `pagination()` key themselves on a caller-supplied handle name (default |
| 5 | + * `"query"`), which used to be written straight into `request.wheels`. CFML struct keys are |
| 6 | + * case-insensitive, so a handle matching a framework-owned key collided with it in both |
| 7 | + * directions: |
| 8 | + * |
| 9 | + * 1. Write: `setPagination(handle="tenant")` overwrote the resolved tenant context with a |
| 10 | + * pagination struct. `handle="$queryCache"` did the same to the per-request finder cache. |
| 11 | + * 2. Read: `pagination()` only validates the handle when `showErrorInformation` is on, so in |
| 12 | + * production an unknown handle that happened to name a framework key returned that key's |
| 13 | + * struct as though it were pagination state. |
| 14 | + * |
| 15 | + * Handles now live under the reserved `request.wheels.$pagination` sub-struct. Same fix shape as |
| 16 | + * #3336, which moved the finder cache to `request.wheels.$queryCache`. |
| 17 | + */ |
| 18 | +component extends="wheels.WheelsTest" { |
| 19 | + |
| 20 | + function run() { |
| 21 | + |
| 22 | + g = application.wo; |
| 23 | + |
| 24 | + describe("pagination handle / framework key collision (##3339)", () => { |
| 25 | + |
| 26 | + // The whole core suite runs inside a single request, so request.wheels is shared across |
| 27 | + // spec files. Only ever remove this spec's own handles — deleting the $pagination |
| 28 | + // namespace wholesale would destroy handles other specs set up. |
| 29 | + ownHandles = "articles,comments,tenant,$queryCache,noSuchHandleXYZ"; |
| 30 | + |
| 31 | + // Ensure the namespace inline rather than calling g.$ensurePaginationStore(): a |
| 32 | + // zero-argument dotted call in statement position breaks Adobe CF 2025's parser. |
| 33 | + beforeEach(() => { |
| 34 | + originalShowErr = application.wheels.showErrorInformation; |
| 35 | + originalCacheSetting = application.wheels.cacheQueriesDuringRequest; |
| 36 | + StructDelete(request.wheels, "tenant"); |
| 37 | + if (!StructKeyExists(request.wheels, "$pagination")) { |
| 38 | + request.wheels["$pagination"] = {}; |
| 39 | + } |
| 40 | + paginationStore = request.wheels["$pagination"]; |
| 41 | + for (var h in ListToArray(ownHandles)) { |
| 42 | + StructDelete(paginationStore, h, false); |
| 43 | + } |
| 44 | + }) |
| 45 | + |
| 46 | + afterEach(() => { |
| 47 | + application.wheels.showErrorInformation = originalShowErr; |
| 48 | + application.wheels.cacheQueriesDuringRequest = originalCacheSetting; |
| 49 | + StructDelete(request.wheels, "tenant"); |
| 50 | + if (!StructKeyExists(request.wheels, "$pagination")) { |
| 51 | + request.wheels["$pagination"] = {}; |
| 52 | + } |
| 53 | + paginationStore = request.wheels["$pagination"]; |
| 54 | + for (var h in ListToArray(ownHandles)) { |
| 55 | + StructDelete(paginationStore, h, false); |
| 56 | + } |
| 57 | + }) |
| 58 | + |
| 59 | + it("stores handles under the reserved namespace, not the bare key", () => { |
| 60 | + g.setPagination(totalRecords = 100, currentPage = 2, perPage = 10, handle = "articles"); |
| 61 | + |
| 62 | + expect(StructKeyExists(request.wheels, "$pagination")).toBeTrue(); |
| 63 | + expect(StructKeyExists(request.wheels["$pagination"], "articles")).toBeTrue(); |
| 64 | + expect(StructKeyExists(request.wheels, "articles")).toBeFalse(); |
| 65 | + }) |
| 66 | + |
| 67 | + it("round-trips pagination data through the namespace", () => { |
| 68 | + g.setPagination(totalRecords = 100, currentPage = 2, perPage = 10, handle = "articles"); |
| 69 | + var pg = g.pagination("articles"); |
| 70 | + |
| 71 | + expect(pg.totalRecords).toBe(100); |
| 72 | + expect(pg.currentPage).toBe(2); |
| 73 | + expect(pg.perPage).toBe(10); |
| 74 | + expect(pg.totalPages).toBe(10); |
| 75 | + }) |
| 76 | + |
| 77 | + // Write direction — a handle named after a framework key must not clobber it. |
| 78 | + it("does not overwrite resolved tenant context when a handle is named tenant", () => { |
| 79 | + request.wheels.tenant = {id = "acme", dataSource = "tenant_acme", config = {}, "$locked" = true}; |
| 80 | + |
| 81 | + g.setPagination(totalRecords = 50, currentPage = 1, perPage = 25, handle = "tenant"); |
| 82 | + |
| 83 | + expect(IsDefined("request.wheels.tenant")).toBeTrue(); |
| 84 | + expect(request.wheels.tenant.id).toBe("acme"); |
| 85 | + expect(request.wheels.tenant.dataSource).toBe("tenant_acme"); |
| 86 | + expect(g.$tenantDataSource()).toBe("tenant_acme"); |
| 87 | + }) |
| 88 | + |
| 89 | + it("does not overwrite the finder cache namespace when a handle is named $queryCache", () => { |
| 90 | + application.wheels.cacheQueriesDuringRequest = true; |
| 91 | + model("author").findAll(where = "lastName = 'Djurner'"); |
| 92 | + var cachedBefore = StructCount(request.wheels["$queryCache"]["author"]); |
| 93 | + |
| 94 | + g.setPagination(totalRecords = 50, currentPage = 1, perPage = 25, handle = "$queryCache"); |
| 95 | + |
| 96 | + expect(StructKeyExists(request.wheels["$queryCache"], "author")).toBeTrue(); |
| 97 | + expect(StructCount(request.wheels["$queryCache"]["author"])).toBe(cachedBefore); |
| 98 | + }) |
| 99 | + |
| 100 | + // Read direction — the case showErrorInformation hides in production. |
| 101 | + it("does not return a framework struct for an unknown handle when errors are hidden", () => { |
| 102 | + application.wheels.showErrorInformation = false; |
| 103 | + request.wheels.tenant = {id = "acme", dataSource = "tenant_acme", config = {}, "$locked" = true}; |
| 104 | + |
| 105 | + // Pre-fix this returned the tenant struct as though it were pagination data. |
| 106 | + // It must now fail to resolve rather than hand back foreign state. |
| 107 | + var result = {returnedTenant = false, threw = false}; |
| 108 | + try { |
| 109 | + var pg = g.pagination("tenant"); |
| 110 | + result.returnedTenant = IsStruct(pg) && StructKeyExists(pg, "dataSource"); |
| 111 | + } catch (any e) { |
| 112 | + result.threw = true; |
| 113 | + } |
| 114 | + |
| 115 | + expect(result.returnedTenant).toBeFalse(); |
| 116 | + expect(result.threw).toBeTrue(); |
| 117 | + }) |
| 118 | + |
| 119 | + it("still throws Wheels.QueryHandleNotFound for an unknown handle in development", () => { |
| 120 | + application.wheels.showErrorInformation = true; |
| 121 | + |
| 122 | + expect(function() { |
| 123 | + g.pagination("noSuchHandleXYZ"); |
| 124 | + }).toThrow("Wheels.QueryHandleNotFound"); |
| 125 | + }) |
| 126 | + |
| 127 | + it("keeps distinct handles isolated from each other", () => { |
| 128 | + g.setPagination(totalRecords = 100, currentPage = 1, perPage = 10, handle = "articles"); |
| 129 | + g.setPagination(totalRecords = 30, currentPage = 3, perPage = 5, handle = "comments"); |
| 130 | + |
| 131 | + expect(g.pagination("articles").totalRecords).toBe(100); |
| 132 | + expect(g.pagination("comments").totalRecords).toBe(30); |
| 133 | + expect(g.pagination("comments").currentPage).toBe(3); |
| 134 | + }) |
| 135 | + |
| 136 | + }) |
| 137 | + |
| 138 | + } |
| 139 | +} |
0 commit comments