11import { describe , it , expect , vi , beforeEach } from "vitest"
2- import { PUT } from "../route"
2+ import { PUT , GET } from "../route"
33import { requireAuth , requireRole } from "@/lib/api/auth"
44import { NextRequest } from "next/server"
5+ import { ApiErrors } from "@/lib/api/errors"
6+ import {
7+ getAdminSettings ,
8+ updateAdminSettings ,
9+ } from "@/lib/admin-settings-store"
10+ import { emitAuditEvent } from "@/lib/api/audit"
11+
12+ vi . mock ( "@/lib/supabase/server" , ( ) => ( {
13+ createClient : vi . fn ( ) ,
14+ } ) )
515
616vi . mock ( "@/lib/api/auth" , ( ) => ( {
717 requireAuth : vi . fn ( ) ,
818 requireRole : vi . fn ( ) ,
919 createRequestContext : vi . fn ( ) . mockReturnValue ( { requestId : "test-admin-id" } ) ,
1020} ) )
1121
22+ vi . mock ( "@/lib/admin-settings-store" , ( ) => ( {
23+ getAdminSettings : vi . fn ( ) ,
24+ updateAdminSettings : vi . fn ( ) ,
25+ resetAdminSettingsStore : vi . fn ( ) ,
26+ getCachedAdminSettings : vi . fn ( ) ,
27+ } ) )
28+
29+ vi . mock ( "@/lib/api/audit" , ( ) => ( {
30+ emitAuditEvent : vi . fn ( ) ,
31+ } ) )
32+
1233describe ( "Admin Settings API Route" , ( ) => {
1334 const mockOwner = { id : "user_owner_123" , email : "owner@example.com" }
1435
1536 beforeEach ( ( ) => {
1637 vi . clearAllMocks ( )
1738 vi . mocked ( requireAuth ) . mockResolvedValue ( mockOwner as any )
1839 vi . mocked ( requireRole ) . mockResolvedValue ( true as any )
40+ vi . mocked ( getAdminSettings ) . mockResolvedValue ( {
41+ maintenanceMode : false ,
42+ enableRegistration : true ,
43+ rateLimitThreshold : 100 ,
44+ } )
1945 } )
2046
21- it ( "should update settings successfully with valid parameters " , async ( ) => {
47+ it ( "persists settings and returns the saved state " , async ( ) => {
2248 const validBody = {
2349 maintenanceMode : true ,
2450 enableRegistration : false ,
2551 rateLimitThreshold : 100 ,
2652 }
53+ const saved = { ...validBody }
54+ vi . mocked ( updateAdminSettings ) . mockResolvedValue ( saved )
2755
2856 const request = new NextRequest ( "http://localhost/api/admin/settings" , {
2957 method : "PUT" ,
@@ -36,13 +64,18 @@ describe("Admin Settings API Route", () => {
3664 expect ( response . status ) . toBe ( 200 )
3765 expect ( body . success ) . toBe ( true )
3866 expect ( body . data . updated ) . toBe ( true )
39- expect ( body . data . settings ) . toEqual ( validBody )
67+ expect ( body . data . settings ) . toEqual ( saved )
68+ expect ( updateAdminSettings ) . toHaveBeenCalledWith ( validBody )
4069 } )
4170
42- it ( "should support partial settings updates" , async ( ) => {
43- const partialBody = {
71+ it ( "returns merged saved state for partial updates, not only the submitted body" , async ( ) => {
72+ const partialBody = { maintenanceMode : false }
73+ const saved = {
4474 maintenanceMode : false ,
75+ enableRegistration : true ,
76+ rateLimitThreshold : 100 ,
4577 }
78+ vi . mocked ( updateAdminSettings ) . mockResolvedValue ( saved )
4679
4780 const request = new NextRequest ( "http://localhost/api/admin/settings" , {
4881 method : "PUT" ,
@@ -53,63 +86,129 @@ describe("Admin Settings API Route", () => {
5386 const body = await response . json ( )
5487
5588 expect ( response . status ) . toBe ( 200 )
56- expect ( body . success ) . toBe ( true )
57- expect ( body . data . settings ) . toEqual ( partialBody )
89+ expect ( body . data . settings ) . toEqual ( saved )
90+ expect ( body . data . settings ) . not . toEqual ( partialBody )
5891 } )
5992
60- it ( "should reject setting a negative rateLimitThreshold" , async ( ) => {
61- const invalidBody = {
62- rateLimitThreshold : - 10 ,
63- }
93+ it ( "emits an audit event for privileged settings changes" , async ( ) => {
94+ const validBody = { maintenanceMode : true }
95+ vi . mocked ( updateAdminSettings ) . mockResolvedValue ( {
96+ maintenanceMode : true ,
97+ enableRegistration : true ,
98+ rateLimitThreshold : 100 ,
99+ } )
100+
101+ const request = new NextRequest ( "http://localhost/api/admin/settings" , {
102+ method : "PUT" ,
103+ body : JSON . stringify ( validBody ) ,
104+ } )
105+
106+ await PUT ( request )
107+
108+ expect ( emitAuditEvent ) . toHaveBeenCalledWith (
109+ expect . objectContaining ( {
110+ userId : mockOwner . id ,
111+ action : "admin.settings_update" ,
112+ resourceType : "admin_settings" ,
113+ metadata : expect . objectContaining ( {
114+ route : "/api/admin/settings" ,
115+ requestId : "test-admin-id" ,
116+ changedFields : "maintenanceMode" ,
117+ maintenanceMode : true ,
118+ } ) ,
119+ } )
120+ )
121+ } )
122+
123+ it ( "rejects unauthenticated users" , async ( ) => {
124+ vi . mocked ( requireAuth ) . mockRejectedValue ( ApiErrors . unauthorized ( ) )
125+
126+ const request = new NextRequest ( "http://localhost/api/admin/settings" , {
127+ method : "PUT" ,
128+ body : JSON . stringify ( { maintenanceMode : true } ) ,
129+ } )
130+
131+ const response = await PUT ( request )
132+ const body = await response . json ( )
133+
134+ expect ( response . status ) . toBe ( 401 )
135+ expect ( body . error . code ) . toBe ( "UNAUTHORIZED" )
136+ expect ( updateAdminSettings ) . not . toHaveBeenCalled ( )
137+ } )
138+
139+ it ( "rejects non-owner users" , async ( ) => {
140+ vi . mocked ( requireRole ) . mockRejectedValue (
141+ ApiErrors . forbidden ( "Requires one of: owner" )
142+ )
143+
144+ const request = new NextRequest ( "http://localhost/api/admin/settings" , {
145+ method : "PUT" ,
146+ body : JSON . stringify ( { maintenanceMode : true } ) ,
147+ } )
148+
149+ const response = await PUT ( request )
150+ const body = await response . json ( )
151+
152+ expect ( response . status ) . toBe ( 403 )
153+ expect ( body . error . code ) . toBe ( "FORBIDDEN" )
154+ expect ( updateAdminSettings ) . not . toHaveBeenCalled ( )
155+ } )
64156
157+ it ( "should reject setting a negative rateLimitThreshold" , async ( ) => {
65158 const request = new NextRequest ( "http://localhost/api/admin/settings" , {
66159 method : "PUT" ,
67- body : JSON . stringify ( invalidBody ) ,
160+ body : JSON . stringify ( { rateLimitThreshold : - 10 } ) ,
68161 } )
69162
70163 const response = await PUT ( request )
71164 const body = await response . json ( )
72165
73166 expect ( response . status ) . toBe ( 400 )
74- expect ( body . success ) . toBe ( false )
75167 expect ( body . error . code ) . toBe ( "VALIDATION_ERROR" )
76168 expect ( body . error . field ) . toBe ( "rateLimitThreshold" )
77169 } )
78170
79171 it ( "should reject setting rateLimitThreshold to 0" , async ( ) => {
80- const invalidBody = {
81- rateLimitThreshold : 0 ,
82- }
83-
84172 const request = new NextRequest ( "http://localhost/api/admin/settings" , {
85173 method : "PUT" ,
86- body : JSON . stringify ( invalidBody ) ,
174+ body : JSON . stringify ( { rateLimitThreshold : 0 } ) ,
87175 } )
88176
89177 const response = await PUT ( request )
90178 const body = await response . json ( )
91179
92180 expect ( response . status ) . toBe ( 400 )
93- expect ( body . success ) . toBe ( false )
94181 expect ( body . error . code ) . toBe ( "VALIDATION_ERROR" )
95182 } )
96183
97184 it ( "should reject invalid data types" , async ( ) => {
98- const invalidBody = {
99- maintenanceMode : "yes" , // should be boolean
100- }
101-
102185 const request = new NextRequest ( "http://localhost/api/admin/settings" , {
103186 method : "PUT" ,
104- body : JSON . stringify ( invalidBody ) ,
187+ body : JSON . stringify ( { maintenanceMode : "yes" } ) ,
105188 } )
106189
107190 const response = await PUT ( request )
108191 const body = await response . json ( )
109192
110193 expect ( response . status ) . toBe ( 400 )
111- expect ( body . success ) . toBe ( false )
112194 expect ( body . error . code ) . toBe ( "VALIDATION_ERROR" )
113195 expect ( body . error . field ) . toBe ( "maintenanceMode" )
114196 } )
197+
198+ it ( "GET returns persisted settings for owners" , async ( ) => {
199+ const settings = {
200+ maintenanceMode : true ,
201+ enableRegistration : false ,
202+ rateLimitThreshold : 50 ,
203+ }
204+ vi . mocked ( getAdminSettings ) . mockResolvedValue ( settings )
205+
206+ const response = await GET (
207+ new NextRequest ( "http://localhost/api/admin/settings" )
208+ )
209+ const body = await response . json ( )
210+
211+ expect ( response . status ) . toBe ( 200 )
212+ expect ( body . data . settings ) . toEqual ( settings )
213+ } )
115214} )
0 commit comments