api_contract_test.go 107 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957
  1. //go:build unit
  2. package server_test
  3. import (
  4. "bytes"
  5. "context"
  6. "errors"
  7. "io"
  8. "math"
  9. "net/http"
  10. "net/http/httptest"
  11. "sort"
  12. "testing"
  13. "time"
  14. "github.com/Wei-Shaw/sub2api/internal/config"
  15. "github.com/Wei-Shaw/sub2api/internal/handler"
  16. adminhandler "github.com/Wei-Shaw/sub2api/internal/handler/admin"
  17. "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
  18. "github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
  19. "github.com/Wei-Shaw/sub2api/internal/server/middleware"
  20. "github.com/Wei-Shaw/sub2api/internal/service"
  21. "github.com/gin-gonic/gin"
  22. "github.com/stretchr/testify/require"
  23. )
  24. func TestAPIContracts(t *testing.T) {
  25. gin.SetMode(gin.TestMode)
  26. tests := []struct {
  27. name string
  28. setup func(t *testing.T, deps *contractDeps)
  29. method string
  30. path string
  31. body string
  32. headers map[string]string
  33. wantStatus int
  34. wantJSON string
  35. }{
  36. {
  37. name: "GET /api/v1/auth/me",
  38. method: http.MethodGet,
  39. path: "/api/v1/auth/me",
  40. wantStatus: http.StatusOK,
  41. wantJSON: `{
  42. "code": 0,
  43. "message": "success",
  44. "data": {
  45. "id": 1,
  46. "email": "alice@example.com",
  47. "email_bound": true,
  48. "username": "alice",
  49. "role": "user",
  50. "balance": 12.5,
  51. "frozen_balance": 0,
  52. "concurrency": 5,
  53. "rpm_limit": 0,
  54. "status": "active",
  55. "allowed_groups": null,
  56. "created_at": "2025-01-02T03:04:05Z",
  57. "updated_at": "2025-01-02T03:04:05Z",
  58. "balance_notify_enabled": false,
  59. "balance_notify_threshold_type": "",
  60. "balance_notify_threshold": null,
  61. "balance_notify_extra_emails": null,
  62. "total_recharged": 0,
  63. "linuxdo_bound": false,
  64. "oidc_bound": false,
  65. "wechat_bound": false,
  66. "dingtalk_bound": false,
  67. "identities": {
  68. "email": {
  69. "provider": "email",
  70. "provider_key": "email",
  71. "bound": true,
  72. "bound_count": 1,
  73. "can_bind": false,
  74. "can_unbind": false,
  75. "display_name": "alice@example.com",
  76. "subject_hint": "a***e@example.com",
  77. "note_key": "profile.authBindings.notes.emailManagedFromProfile",
  78. "note": "Primary account email is managed from the profile form."
  79. },
  80. "linuxdo": {
  81. "provider": "linuxdo",
  82. "bound": false,
  83. "bound_count": 0,
  84. "can_bind": true,
  85. "can_unbind": false,
  86. "bind_start_path": "/api/v1/auth/oauth/linuxdo/bind/start?intent=bind_current_user&redirect=%2Fsettings%2Fprofile"
  87. },
  88. "oidc": {
  89. "provider": "oidc",
  90. "bound": false,
  91. "bound_count": 0,
  92. "can_bind": true,
  93. "can_unbind": false,
  94. "bind_start_path": "/api/v1/auth/oauth/oidc/bind/start?intent=bind_current_user&redirect=%2Fsettings%2Fprofile"
  95. },
  96. "wechat": {
  97. "provider": "wechat",
  98. "bound": false,
  99. "bound_count": 0,
  100. "can_bind": true,
  101. "can_unbind": false,
  102. "bind_start_path": "/api/v1/auth/oauth/wechat/bind/start?intent=bind_current_user&redirect=%2Fsettings%2Fprofile"
  103. },
  104. "dingtalk": {
  105. "provider": "dingtalk",
  106. "bound": false,
  107. "bound_count": 0,
  108. "can_bind": true,
  109. "can_unbind": false,
  110. "bind_start_path": "/api/v1/auth/oauth/dingtalk/bind/start?intent=bind_current_user&redirect=%2Fsettings%2Fprofile"
  111. }
  112. },
  113. "identity_bindings": {
  114. "email": {
  115. "provider": "email",
  116. "provider_key": "email",
  117. "bound": true,
  118. "bound_count": 1,
  119. "can_bind": false,
  120. "can_unbind": false,
  121. "display_name": "alice@example.com",
  122. "subject_hint": "a***e@example.com",
  123. "note_key": "profile.authBindings.notes.emailManagedFromProfile",
  124. "note": "Primary account email is managed from the profile form."
  125. },
  126. "linuxdo": {
  127. "provider": "linuxdo",
  128. "bound": false,
  129. "bound_count": 0,
  130. "can_bind": true,
  131. "can_unbind": false,
  132. "bind_start_path": "/api/v1/auth/oauth/linuxdo/bind/start?intent=bind_current_user&redirect=%2Fsettings%2Fprofile"
  133. },
  134. "oidc": {
  135. "provider": "oidc",
  136. "bound": false,
  137. "bound_count": 0,
  138. "can_bind": true,
  139. "can_unbind": false,
  140. "bind_start_path": "/api/v1/auth/oauth/oidc/bind/start?intent=bind_current_user&redirect=%2Fsettings%2Fprofile"
  141. },
  142. "wechat": {
  143. "provider": "wechat",
  144. "bound": false,
  145. "bound_count": 0,
  146. "can_bind": true,
  147. "can_unbind": false,
  148. "bind_start_path": "/api/v1/auth/oauth/wechat/bind/start?intent=bind_current_user&redirect=%2Fsettings%2Fprofile"
  149. },
  150. "dingtalk": {
  151. "provider": "dingtalk",
  152. "bound": false,
  153. "bound_count": 0,
  154. "can_bind": true,
  155. "can_unbind": false,
  156. "bind_start_path": "/api/v1/auth/oauth/dingtalk/bind/start?intent=bind_current_user&redirect=%2Fsettings%2Fprofile"
  157. }
  158. },
  159. "auth_bindings": {
  160. "email": {
  161. "provider": "email",
  162. "provider_key": "email",
  163. "bound": true,
  164. "bound_count": 1,
  165. "can_bind": false,
  166. "can_unbind": false,
  167. "display_name": "alice@example.com",
  168. "subject_hint": "a***e@example.com",
  169. "note_key": "profile.authBindings.notes.emailManagedFromProfile",
  170. "note": "Primary account email is managed from the profile form."
  171. },
  172. "linuxdo": {
  173. "provider": "linuxdo",
  174. "bound": false,
  175. "bound_count": 0,
  176. "can_bind": true,
  177. "can_unbind": false,
  178. "bind_start_path": "/api/v1/auth/oauth/linuxdo/bind/start?intent=bind_current_user&redirect=%2Fsettings%2Fprofile"
  179. },
  180. "oidc": {
  181. "provider": "oidc",
  182. "bound": false,
  183. "bound_count": 0,
  184. "can_bind": true,
  185. "can_unbind": false,
  186. "bind_start_path": "/api/v1/auth/oauth/oidc/bind/start?intent=bind_current_user&redirect=%2Fsettings%2Fprofile"
  187. },
  188. "wechat": {
  189. "provider": "wechat",
  190. "bound": false,
  191. "bound_count": 0,
  192. "can_bind": true,
  193. "can_unbind": false,
  194. "bind_start_path": "/api/v1/auth/oauth/wechat/bind/start?intent=bind_current_user&redirect=%2Fsettings%2Fprofile"
  195. },
  196. "dingtalk": {
  197. "provider": "dingtalk",
  198. "bound": false,
  199. "bound_count": 0,
  200. "can_bind": true,
  201. "can_unbind": false,
  202. "bind_start_path": "/api/v1/auth/oauth/dingtalk/bind/start?intent=bind_current_user&redirect=%2Fsettings%2Fprofile"
  203. }
  204. },
  205. "run_mode": "standard"
  206. }
  207. }`,
  208. },
  209. {
  210. name: "POST /api/v1/keys",
  211. method: http.MethodPost,
  212. path: "/api/v1/keys",
  213. body: `{"name":"Key One","custom_key":"sk_custom_1234567890"}`,
  214. headers: map[string]string{
  215. "Content-Type": "application/json",
  216. },
  217. wantStatus: http.StatusOK,
  218. wantJSON: `{
  219. "code": 0,
  220. "message": "success",
  221. "data": {
  222. "id": 100,
  223. "user_id": 1,
  224. "key": "sk_custom_1234567890",
  225. "name": "Key One",
  226. "group_id": null,
  227. "status": "active",
  228. "ip_whitelist": null,
  229. "ip_blacklist": null,
  230. "last_used_at": null,
  231. "last_used_ip": null,
  232. "current_concurrency": 0,
  233. "quota": 0,
  234. "quota_used": 0,
  235. "rate_limit_5h": 0,
  236. "rate_limit_1d": 0,
  237. "rate_limit_7d": 0,
  238. "usage_5h": 0,
  239. "usage_1d": 0,
  240. "usage_7d": 0,
  241. "window_5h_start": null,
  242. "window_1d_start": null,
  243. "window_7d_start": null,
  244. "expires_at": null,
  245. "created_at": "2025-01-02T03:04:05Z",
  246. "updated_at": "2025-01-02T03:04:05Z"
  247. }
  248. }`,
  249. },
  250. {
  251. name: "GET /api/v1/keys (paginated)",
  252. setup: func(t *testing.T, deps *contractDeps) {
  253. t.Helper()
  254. deps.apiKeyRepo.MustSeed(&service.APIKey{
  255. ID: 100,
  256. UserID: 1,
  257. Key: "sk_custom_1234567890",
  258. Name: "Key One",
  259. Status: service.StatusActive,
  260. CreatedAt: deps.now,
  261. UpdatedAt: deps.now,
  262. })
  263. },
  264. method: http.MethodGet,
  265. path: "/api/v1/keys?page=1&page_size=10",
  266. wantStatus: http.StatusOK,
  267. wantJSON: `{
  268. "code": 0,
  269. "message": "success",
  270. "data": {
  271. "items": [
  272. {
  273. "id": 100,
  274. "user_id": 1,
  275. "key": "sk_custom_1234567890",
  276. "name": "Key One",
  277. "group_id": null,
  278. "status": "active",
  279. "ip_whitelist": null,
  280. "ip_blacklist": null,
  281. "last_used_at": null,
  282. "last_used_ip": null,
  283. "current_concurrency": 0,
  284. "quota": 0,
  285. "quota_used": 0,
  286. "rate_limit_5h": 0,
  287. "rate_limit_1d": 0,
  288. "rate_limit_7d": 0,
  289. "usage_5h": 0,
  290. "usage_1d": 0,
  291. "usage_7d": 0,
  292. "window_5h_start": null,
  293. "window_1d_start": null,
  294. "window_7d_start": null,
  295. "expires_at": null,
  296. "created_at": "2025-01-02T03:04:05Z",
  297. "updated_at": "2025-01-02T03:04:05Z"
  298. }
  299. ],
  300. "total": 1,
  301. "page": 1,
  302. "page_size": 10,
  303. "pages": 1
  304. }
  305. }`,
  306. },
  307. {
  308. name: "GET /api/v1/groups/available",
  309. setup: func(t *testing.T, deps *contractDeps) {
  310. t.Helper()
  311. // 普通用户可见的分组列表不应包含内部字段(如 model_routing/account_count),
  312. // 也不得包含利润控制配置——它与同响应的 rate_multiplier 相乘即可反推上游成本上限。
  313. deps.groupRepo.SetActive([]service.Group{
  314. {
  315. ID: 10,
  316. Name: "Group One",
  317. Description: "desc",
  318. Platform: service.PlatformAnthropic,
  319. RateMultiplier: 1.5,
  320. PeakRateMultiplier: 1.0,
  321. IsExclusive: false,
  322. Status: service.StatusActive,
  323. SubscriptionType: service.SubscriptionTypeStandard,
  324. ProfitControlEnabled: true,
  325. ProfitMinMargin: 0.3,
  326. ProfitSafetyBuffer: 0.05,
  327. ModelRoutingEnabled: true,
  328. ModelRouting: map[string][]int64{
  329. "claude-3-*": []int64{101, 102},
  330. },
  331. AccountCount: 2,
  332. CreatedAt: deps.now,
  333. UpdatedAt: deps.now,
  334. },
  335. })
  336. deps.userSubRepo.SetActiveByUserID(1, nil)
  337. },
  338. method: http.MethodGet,
  339. path: "/api/v1/groups/available",
  340. wantStatus: http.StatusOK,
  341. wantJSON: `{
  342. "code": 0,
  343. "message": "success",
  344. "data": [
  345. {
  346. "id": 10,
  347. "name": "Group One",
  348. "description": "desc",
  349. "platform": "anthropic",
  350. "rate_multiplier": 1.5,
  351. "peak_rate_enabled": false,
  352. "peak_start": "",
  353. "peak_end": "",
  354. "peak_rate_multiplier": 1,
  355. "is_exclusive": false,
  356. "status": "active",
  357. "subscription_type": "standard",
  358. "daily_limit_usd": null,
  359. "weekly_limit_usd": null,
  360. "monthly_limit_usd": null,
  361. "long_context_pricing_enabled": false,
  362. "image_price_1k": null,
  363. "image_price_2k": null,
  364. "image_price_4k": null,
  365. "video_price_480p": null,
  366. "video_price_720p": null,
  367. "video_price_1080p": null,
  368. "web_search_price_per_call": null,
  369. "search_price_per_1k": null,
  370. "audio_tts_price_per_million_chars": null,
  371. "audio_stt_price_per_hour": null,
  372. "audio_realtime_price_per_min": null,
  373. "allow_image_generation": false,
  374. "allow_batch_image_generation": false,
  375. "batch_image_discount_multiplier": 0,
  376. "batch_image_hold_multiplier": 0,
  377. "image_rate_independent": false,
  378. "image_rate_multiplier": 0,
  379. "video_rate_independent": false,
  380. "video_rate_multiplier": 0,
  381. "claude_code_only": false,
  382. "allow_messages_dispatch": false,
  383. "allow_live": false,
  384. "security_policy_enabled": false,
  385. "security_policy_mode": "",
  386. "security_policy_email_enabled": false,
  387. "fallback_group_id": null,
  388. "fallback_group_id_on_invalid_request": null,
  389. "require_oauth_only": false,
  390. "require_privacy_set": false,
  391. "max_reasoning_effort": "",
  392. "max_reasoning_effort_over_limit": "",
  393. "reasoning_effort_mappings": null,
  394. "rpm_limit": 0,
  395. "created_at": "2025-01-02T03:04:05Z",
  396. "updated_at": "2025-01-02T03:04:05Z"
  397. }
  398. ]
  399. }`,
  400. },
  401. {
  402. name: "GET /api/v1/subscriptions",
  403. setup: func(t *testing.T, deps *contractDeps) {
  404. t.Helper()
  405. // 普通用户订阅接口不应包含 assigned_* / notes 等管理员字段。
  406. deps.userSubRepo.SetByUserID(1, []service.UserSubscription{
  407. {
  408. ID: 501,
  409. UserID: 1,
  410. GroupID: 10,
  411. StartsAt: deps.now,
  412. ExpiresAt: time.Date(2099, 1, 2, 3, 4, 5, 0, time.UTC), // 使用未来日期避免 normalizeSubscriptionStatus 标记为过期
  413. Status: service.SubscriptionStatusActive,
  414. DailyUsageUSD: 1.23,
  415. WeeklyUsageUSD: 2.34,
  416. MonthlyUsageUSD: 3.45,
  417. AssignedBy: ptr(int64(999)),
  418. AssignedAt: deps.now,
  419. Notes: "admin-note",
  420. CreatedAt: deps.now,
  421. UpdatedAt: deps.now,
  422. },
  423. })
  424. },
  425. method: http.MethodGet,
  426. path: "/api/v1/subscriptions",
  427. wantStatus: http.StatusOK,
  428. wantJSON: `{
  429. "code": 0,
  430. "message": "success",
  431. "data": [
  432. {
  433. "id": 501,
  434. "user_id": 1,
  435. "group_id": 10,
  436. "starts_at": "2025-01-02T03:04:05Z",
  437. "expires_at": "2099-01-02T03:04:05Z",
  438. "status": "active",
  439. "daily_window_start": null,
  440. "weekly_window_start": null,
  441. "monthly_window_start": null,
  442. "daily_usage_usd": 1.23,
  443. "weekly_usage_usd": 2.34,
  444. "monthly_usage_usd": 3.45,
  445. "created_at": "2025-01-02T03:04:05Z",
  446. "updated_at": "2025-01-02T03:04:05Z"
  447. }
  448. ]
  449. }`,
  450. },
  451. {
  452. name: "GET /api/v1/redeem/history",
  453. setup: func(t *testing.T, deps *contractDeps) {
  454. t.Helper()
  455. // 普通用户兑换历史不应包含 notes 等内部字段。
  456. deps.redeemRepo.SetByUser(1, []service.RedeemCode{
  457. {
  458. ID: 900,
  459. Code: "CODE-123",
  460. Type: service.RedeemTypeBalance,
  461. Value: 1.25,
  462. Status: service.StatusUsed,
  463. UsedBy: ptr(int64(1)),
  464. UsedAt: ptr(deps.now),
  465. Notes: "internal-note",
  466. CreatedAt: deps.now,
  467. },
  468. })
  469. },
  470. method: http.MethodGet,
  471. path: "/api/v1/redeem/history",
  472. wantStatus: http.StatusOK,
  473. wantJSON: `{
  474. "code": 0,
  475. "message": "success",
  476. "data": [
  477. {
  478. "id": 900,
  479. "code": "CODE-123",
  480. "type": "balance",
  481. "value": 1.25,
  482. "status": "used",
  483. "used_by": 1,
  484. "used_at": "2025-01-02T03:04:05Z",
  485. "created_at": "2025-01-02T03:04:05Z",
  486. "group_id": null,
  487. "validity_days": 0
  488. }
  489. ]
  490. }`,
  491. },
  492. {
  493. name: "GET /api/v1/usage/stats",
  494. setup: func(t *testing.T, deps *contractDeps) {
  495. t.Helper()
  496. deps.usageRepo.SetUserLogs(1, []service.UsageLog{
  497. {
  498. ID: 1,
  499. UserID: 1,
  500. APIKeyID: 100,
  501. AccountID: 200,
  502. Model: "claude-3",
  503. InputTokens: 10,
  504. OutputTokens: 20,
  505. CacheCreationTokens: 1,
  506. CacheReadTokens: 2,
  507. TotalCost: 0.5,
  508. ActualCost: 0.5,
  509. DurationMs: ptr(100),
  510. CreatedAt: deps.now,
  511. },
  512. {
  513. ID: 2,
  514. UserID: 1,
  515. APIKeyID: 100,
  516. AccountID: 200,
  517. Model: "claude-3",
  518. InputTokens: 5,
  519. OutputTokens: 15,
  520. TotalCost: 0.25,
  521. ActualCost: 0.25,
  522. DurationMs: ptr(300),
  523. CreatedAt: deps.now,
  524. },
  525. })
  526. },
  527. method: http.MethodGet,
  528. path: "/api/v1/usage/stats?start_date=2025-01-01&end_date=2025-01-02",
  529. wantStatus: http.StatusOK,
  530. wantJSON: `{
  531. "code": 0,
  532. "message": "success",
  533. "data": {
  534. "total_requests": 2,
  535. "total_input_tokens": 15,
  536. "total_output_tokens": 35,
  537. "total_cache_tokens": 3,
  538. "total_cache_creation_tokens": 1,
  539. "total_cache_read_tokens": 2,
  540. "total_tokens": 53,
  541. "total_cost": 0.75,
  542. "total_actual_cost": 0.75,
  543. "average_duration_ms": 200
  544. }
  545. }`,
  546. },
  547. {
  548. name: "GET /api/v1/usage (paginated)",
  549. setup: func(t *testing.T, deps *contractDeps) {
  550. t.Helper()
  551. deps.usageRepo.SetUserLogs(1, []service.UsageLog{
  552. {
  553. ID: 1,
  554. UserID: 1,
  555. APIKeyID: 100,
  556. AccountID: 200,
  557. AccountRateMultiplier: ptr(0.5),
  558. RequestID: "req_123",
  559. Model: "claude-3",
  560. InputTokens: 10,
  561. OutputTokens: 20,
  562. CacheCreationTokens: 1,
  563. CacheReadTokens: 2,
  564. TotalCost: 0.5,
  565. ActualCost: 0.5,
  566. RateMultiplier: 1,
  567. BillingType: service.BillingTypeBalance,
  568. Stream: true,
  569. DurationMs: ptr(100),
  570. FirstTokenMs: ptr(50),
  571. CreatedAt: deps.now,
  572. },
  573. })
  574. },
  575. method: http.MethodGet,
  576. path: "/api/v1/usage?page=1&page_size=10",
  577. wantStatus: http.StatusOK,
  578. wantJSON: `{
  579. "code": 0,
  580. "message": "success",
  581. "data": {
  582. "items": [
  583. {
  584. "id": 1,
  585. "user_id": 1,
  586. "api_key_id": 100,
  587. "account_id": 200,
  588. "request_id": "req_123",
  589. "model": "claude-3",
  590. "request_type": "stream",
  591. "native_compaction_v2": false,
  592. "openai_ws_mode": false,
  593. "group_id": null,
  594. "subscription_id": null,
  595. "input_tokens": 10,
  596. "output_tokens": 20,
  597. "cache_creation_tokens": 1,
  598. "cache_read_tokens": 2,
  599. "cache_creation_5m_tokens": 0,
  600. "cache_creation_1h_tokens": 0,
  601. "input_cost": 0,
  602. "output_cost": 0,
  603. "cache_creation_cost": 0,
  604. "cache_read_cost": 0,
  605. "total_cost": 0.5,
  606. "actual_cost": 0.5,
  607. "rate_multiplier": 1,
  608. "long_context_billing_applied": false,
  609. "billing_type": 0,
  610. "stream": true,
  611. "duration_ms": 100,
  612. "first_token_ms": 50,
  613. "image_count": 0,
  614. "image_size": null,
  615. "image_input_size": null,
  616. "image_output_size": null,
  617. "image_input_tokens": 0,
  618. "image_input_cost": 0,
  619. "image_output_tokens": 0,
  620. "image_output_cost": 0,
  621. "image_size_source": null,
  622. "image_size_breakdown": null,
  623. "media_type": null,
  624. "cache_ttl_overridden": false,
  625. "created_at": "2025-01-02T03:04:05Z",
  626. "user_agent": null
  627. }
  628. ],
  629. "total": 1,
  630. "page": 1,
  631. "page_size": 10,
  632. "pages": 1
  633. }
  634. }`,
  635. },
  636. {
  637. name: "GET /api/v1/admin/settings",
  638. setup: func(t *testing.T, deps *contractDeps) {
  639. t.Helper()
  640. deps.settingRepo.SetAll(map[string]string{
  641. service.SettingKeyRegistrationEnabled: "true",
  642. service.SettingKeyEmailVerifyEnabled: "false",
  643. service.SettingKeyRegistrationEmailSuffixWhitelist: "[]",
  644. service.SettingKeyPromoCodeEnabled: "true",
  645. service.SettingKeySMTPHost: "smtp.example.com",
  646. service.SettingKeySMTPPort: "587",
  647. service.SettingKeySMTPUsername: "user",
  648. service.SettingKeySMTPPassword: "secret",
  649. service.SettingKeySMTPFrom: "no-reply@example.com",
  650. service.SettingKeySMTPFromName: "Sub2API",
  651. service.SettingKeySMTPUseTLS: "true",
  652. service.SettingKeyTurnstileEnabled: "true",
  653. service.SettingKeyTurnstileSiteKey: "site-key",
  654. service.SettingKeyTurnstileSecretKey: "secret-key",
  655. service.SettingKeyOIDCConnectEnabled: "false",
  656. service.SettingKeyOIDCConnectProviderName: "OIDC",
  657. service.SettingKeyOIDCConnectClientID: "",
  658. service.SettingKeyOIDCConnectIssuerURL: "",
  659. service.SettingKeyOIDCConnectDiscoveryURL: "",
  660. service.SettingKeyOIDCConnectAuthorizeURL: "",
  661. service.SettingKeyOIDCConnectTokenURL: "",
  662. service.SettingKeyOIDCConnectUserInfoURL: "",
  663. service.SettingKeyOIDCConnectJWKSURL: "",
  664. service.SettingKeyOIDCConnectScopes: "openid email profile",
  665. service.SettingKeyOIDCConnectRedirectURL: "",
  666. service.SettingKeyOIDCConnectFrontendRedirectURL: "/auth/oidc/callback",
  667. service.SettingKeyOIDCConnectTokenAuthMethod: "client_secret_post",
  668. service.SettingKeyOIDCConnectUsePKCE: "true",
  669. service.SettingKeyOIDCConnectValidateIDToken: "true",
  670. service.SettingKeyOIDCConnectAllowedSigningAlgs: "RS256,ES256,PS256",
  671. service.SettingKeyOIDCConnectClockSkewSeconds: "120",
  672. service.SettingKeyOIDCConnectRequireEmailVerified: "false",
  673. service.SettingKeyOIDCConnectUserInfoEmailPath: "",
  674. service.SettingKeyOIDCConnectUserInfoIDPath: "",
  675. service.SettingKeyOIDCConnectUserInfoUsernamePath: "",
  676. service.SettingKeySiteName: "Sub2API",
  677. service.SettingKeySiteLogo: "",
  678. service.SettingKeySiteSubtitle: "Subtitle",
  679. service.SettingKeyAPIBaseURL: "https://api.example.com",
  680. service.SettingKeyContactInfo: "support",
  681. service.SettingKeyDocURL: "https://docs.example.com",
  682. service.SettingKeyDefaultConcurrency: "5",
  683. service.SettingKeyDefaultBalance: "1.25",
  684. service.SettingKeyTableDefaultPageSize: "20",
  685. service.SettingKeyTablePageSizeOptions: "[10,20,50,100]",
  686. service.SettingKeyOpsMonitoringEnabled: "false",
  687. service.SettingKeyOpsRealtimeMonitoringEnabled: "true",
  688. service.SettingKeyOpsQueryModeDefault: "auto",
  689. service.SettingKeyOpsMetricsIntervalSeconds: "60",
  690. service.SettingPaymentVisibleMethodAlipaySource: service.VisibleMethodSourceEasyPayAlipay,
  691. service.SettingPaymentVisibleMethodWxpaySource: service.VisibleMethodSourceOfficialWechat,
  692. service.SettingPaymentVisibleMethodAlipayEnabled: "true",
  693. service.SettingPaymentVisibleMethodWxpayEnabled: "false",
  694. service.SettingKeyOpenAILowUpstreamRatePriorityEnabled: "true",
  695. service.SettingKeyOpenAIOAuthSchedulingRateMultiplier: "0.05",
  696. "openai_advanced_scheduler_enabled": "true",
  697. service.SettingKeyOpenAIAdvancedSchedulerStickyWeightedEnabled: "false",
  698. service.SettingKeyOpenAIAdvancedSchedulerSubscriptionPriorityEnabled: "false",
  699. })
  700. },
  701. method: http.MethodGet,
  702. path: "/api/v1/admin/settings",
  703. wantStatus: http.StatusOK,
  704. wantJSON: `{
  705. "code": 0,
  706. "message": "success",
  707. "data": {
  708. "registration_enabled": true,
  709. "email_verify_enabled": false,
  710. "registration_email_suffix_whitelist": [],
  711. "registration_email_domain_quota_enabled": false,
  712. "promo_code_enabled": true,
  713. "password_reset_enabled": false,
  714. "frontend_url": "",
  715. "totp_enabled": false,
  716. "totp_encryption_key_configured": false,
  717. "passkey_enabled": false,
  718. "passkey_configured": false,
  719. "passkey_rp_id": "",
  720. "passkey_rp_origins": [],
  721. "session_binding_enabled": false,
  722. "step_up_enabled": false,
  723. "audit_log_retention_days": 180,
  724. "login_agreement_enabled": false,
  725. "login_agreement_mode": "modal",
  726. "login_agreement_updated_at": "2026-03-31",
  727. "login_agreement_documents": [
  728. {"id": "terms", "title": "服务条款", "content_md": ""},
  729. {"id": "usage-policy", "title": "使用政策", "content_md": ""},
  730. {"id": "supported-regions", "title": "支持的国家和地区", "content_md": ""},
  731. {"id": "service-specific-terms", "title": "服务特定条款", "content_md": ""}
  732. ],
  733. "smtp_host": "smtp.example.com",
  734. "smtp_port": 587,
  735. "smtp_username": "user",
  736. "smtp_password_configured": true,
  737. "smtp_from_email": "no-reply@example.com",
  738. "smtp_from_name": "Sub2API",
  739. "smtp_use_tls": true,
  740. "turnstile_enabled": true,
  741. "turnstile_site_key": "site-key",
  742. "turnstile_secret_key_configured": true,
  743. "tencent_captcha_enabled": false,
  744. "tencent_captcha_app_id": "",
  745. "tencent_captcha_app_secret_key_configured": false,
  746. "tencent_captcha_cloud_secret_id_configured": false,
  747. "tencent_captcha_cloud_secret_key_configured": false,
  748. "tencent_captcha_region": "cn",
  749. "aliyun_captcha_enabled": false,
  750. "aliyun_captcha_access_key_id": "",
  751. "aliyun_captcha_access_key_secret_configured": false,
  752. "aliyun_captcha_scene_id": "",
  753. "aliyun_captcha_prefix": "",
  754. "aliyun_captcha_region": "cn",
  755. "linuxdo_connect_enabled": false,
  756. "linuxdo_connect_client_id": "",
  757. "linuxdo_connect_client_secret_configured": false,
  758. "linuxdo_connect_redirect_url": "",
  759. "dingtalk_connect_enabled": false,
  760. "dingtalk_connect_bypass_registration": false,
  761. "dingtalk_connect_client_id": "",
  762. "dingtalk_connect_client_secret_configured": false,
  763. "dingtalk_connect_redirect_url": "",
  764. "dingtalk_connect_internal_corp_id": "",
  765. "dingtalk_connect_corp_restriction_policy": "",
  766. "dingtalk_connect_sync_corp_email": false,
  767. "dingtalk_connect_sync_corp_email_attr_key": "dingtalk_email",
  768. "dingtalk_connect_sync_corp_email_attr_name": "钉钉企业邮箱",
  769. "dingtalk_connect_sync_dept": false,
  770. "dingtalk_connect_sync_dept_attr_key": "dingtalk_department",
  771. "dingtalk_connect_sync_dept_attr_name": "钉钉部门",
  772. "dingtalk_connect_sync_display_name": false,
  773. "dingtalk_connect_sync_display_name_attr_key": "dingtalk_name",
  774. "dingtalk_connect_sync_display_name_attr_name": "钉钉姓名",
  775. "oidc_connect_enabled": false,
  776. "oidc_connect_provider_name": "OIDC",
  777. "oidc_connect_client_id": "",
  778. "oidc_connect_client_secret_configured": false,
  779. "oidc_connect_issuer_url": "",
  780. "oidc_connect_discovery_url": "",
  781. "oidc_connect_authorize_url": "",
  782. "oidc_connect_token_url": "",
  783. "oidc_connect_userinfo_url": "",
  784. "oidc_connect_jwks_url": "",
  785. "oidc_connect_scopes": "openid email profile",
  786. "oidc_connect_redirect_url": "",
  787. "oidc_connect_frontend_redirect_url": "/auth/oidc/callback",
  788. "oidc_connect_token_auth_method": "client_secret_post",
  789. "oidc_connect_use_pkce": true,
  790. "oidc_connect_validate_id_token": true,
  791. "oidc_connect_allowed_signing_algs": "RS256,ES256,PS256",
  792. "oidc_connect_clock_skew_seconds": 120,
  793. "oidc_connect_require_email_verified": false,
  794. "oidc_connect_userinfo_email_path": "",
  795. "oidc_connect_userinfo_id_path": "",
  796. "oidc_connect_userinfo_username_path": "",
  797. "github_oauth_enabled": false,
  798. "github_oauth_client_id": "",
  799. "github_oauth_client_secret_configured": false,
  800. "github_oauth_redirect_url": "",
  801. "github_oauth_frontend_redirect_url": "/auth/oauth/callback",
  802. "google_oauth_enabled": false,
  803. "google_oauth_client_id": "",
  804. "google_oauth_client_secret_configured": false,
  805. "google_oauth_redirect_url": "",
  806. "google_oauth_frontend_redirect_url": "/auth/oauth/callback",
  807. "ops_monitoring_enabled": false,
  808. "ops_realtime_monitoring_enabled": true,
  809. "ops_query_mode_default": "auto",
  810. "ops_metrics_interval_seconds": 60,
  811. "site_name": "Sub2API",
  812. "site_logo": "",
  813. "site_subtitle": "Subtitle",
  814. "api_base_url": "https://api.example.com",
  815. "api_key_acl_trust_forwarded_ip": false,
  816. "forwarded_client_ip_headers": [],
  817. "contact_info": "support",
  818. "doc_url": "https://docs.example.com",
  819. "auth_source_default_email_balance": 0,
  820. "auth_source_default_email_concurrency": 5,
  821. "auth_source_default_email_subscriptions": [],
  822. "auth_source_default_email_grant_on_signup": false,
  823. "auth_source_default_email_grant_on_first_bind": false,
  824. "auth_source_default_github_balance": 0,
  825. "auth_source_default_github_concurrency": 5,
  826. "auth_source_default_github_subscriptions": [],
  827. "auth_source_default_github_grant_on_signup": false,
  828. "auth_source_default_github_grant_on_first_bind": false,
  829. "auth_source_default_google_balance": 0,
  830. "auth_source_default_google_concurrency": 5,
  831. "auth_source_default_google_subscriptions": [],
  832. "auth_source_default_google_grant_on_signup": false,
  833. "auth_source_default_google_grant_on_first_bind": false,
  834. "auth_source_default_linuxdo_balance": 0,
  835. "auth_source_default_linuxdo_concurrency": 5,
  836. "auth_source_default_linuxdo_subscriptions": [],
  837. "auth_source_default_linuxdo_grant_on_signup": false,
  838. "auth_source_default_linuxdo_grant_on_first_bind": false,
  839. "auth_source_default_oidc_balance": 0,
  840. "auth_source_default_oidc_concurrency": 5,
  841. "auth_source_default_oidc_subscriptions": [],
  842. "auth_source_default_oidc_grant_on_signup": false,
  843. "auth_source_default_oidc_grant_on_first_bind": false,
  844. "auth_source_default_wechat_balance": 0,
  845. "auth_source_default_wechat_concurrency": 5,
  846. "auth_source_default_wechat_subscriptions": [],
  847. "auth_source_default_wechat_grant_on_signup": false,
  848. "auth_source_default_wechat_grant_on_first_bind": false,
  849. "auth_source_default_dingtalk_balance": 0,
  850. "auth_source_default_dingtalk_concurrency": 5,
  851. "auth_source_default_dingtalk_subscriptions": [],
  852. "auth_source_default_dingtalk_grant_on_signup": false,
  853. "auth_source_default_dingtalk_grant_on_first_bind": false,
  854. "force_email_on_third_party_signup": false,
  855. "default_concurrency": 5,
  856. "default_balance": 1.25,
  857. "default_platform_quotas": {"anthropic":{"daily":null,"weekly":null,"monthly":null},"antigravity":{"daily":null,"weekly":null,"monthly":null},"deepseek":{"daily":null,"weekly":null,"monthly":null},"gemini":{"daily":null,"weekly":null,"monthly":null},"grok":{"daily":null,"weekly":null,"monthly":null},"kimi":{"daily":null,"weekly":null,"monthly":null},"minimax":{"daily":null,"weekly":null,"monthly":null},"openai":{"daily":null,"weekly":null,"monthly":null},"zhipu":{"daily":null,"weekly":null,"monthly":null}},
  858. "auth_source_default_email_platform_quotas": null,
  859. "auth_source_default_github_platform_quotas": null,
  860. "auth_source_default_google_platform_quotas": null,
  861. "auth_source_default_linuxdo_platform_quotas": null,
  862. "auth_source_default_oidc_platform_quotas": null,
  863. "auth_source_default_wechat_platform_quotas": null,
  864. "auth_source_default_dingtalk_platform_quotas": null,
  865. "affiliate_rebate_rate": 20,
  866. "affiliate_rebate_freeze_hours": 0,
  867. "affiliate_rebate_duration_days": 0,
  868. "affiliate_rebate_per_invitee_cap": 0,
  869. "affiliate_admin_recharge_enabled": false,
  870. "default_user_rpm_limit": 0,
  871. "default_subscriptions": [],
  872. "enable_model_fallback": false,
  873. "fallback_model_anthropic": "claude-3-5-sonnet-20241022",
  874. "fallback_model_antigravity": "gemini-2.5-pro",
  875. "fallback_model_gemini": "gemini-2.5-pro",
  876. "fallback_model_openai": "gpt-4o",
  877. "enable_identity_patch": true,
  878. "identity_patch_prompt": "",
  879. "invitation_code_enabled": false,
  880. "home_content": "",
  881. "hide_ccs_import_button": false,
  882. "grok_default_text_model": "grok-4.6",
  883. "grok_default_base_url_mode": "cli",
  884. "grok_cross_client_model_map_enabled": true,
  885. "purchase_subscription_enabled": false,
  886. "purchase_subscription_url": "",
  887. "table_default_page_size": 20,
  888. "table_page_size_options": [10, 20, 50, 100],
  889. "min_claude_code_version": "",
  890. "max_claude_code_version": "",
  891. "min_codex_version": "",
  892. "max_codex_version": "",
  893. "codex_cli_only_blacklist": "",
  894. "codex_cli_only_whitelist": "",
  895. "compact_home_enabled": false,
  896. "codex_cli_only_allow_app_server_clients": false,
  897. "codex_cli_only_engine_fingerprint_signals": "[{\"type\":\"header_prefix\",\"match\":[\"x-codex-\"],\"required\":true},{\"type\":\"header_exact\",\"match\":[\"session-id\",\"session_id\"],\"required\":false},{\"type\":\"header_exact\",\"match\":[\"thread-id\",\"thread_id\"],\"required\":false},{\"type\":\"body_path\",\"match\":[\"client_metadata.x-codex-window-id\",\"client_metadata.x-codex-installation-id\"],\"required\":false}]",
  898. "allow_ungrouped_key_scheduling": false,
  899. "backend_mode_enabled": false,
  900. "enable_cch_signing": false,
  901. "enable_claude_oauth_system_prompt_injection": true,
  902. "claude_oauth_system_prompt": "",
  903. "claude_oauth_system_prompt_blocks": "",
  904. "enable_anthropic_cache_ttl_1h_injection": false,
  905. "rewrite_message_cache_control": false,
  906. "enable_client_dateline_normalization": true,
  907. "antigravity_user_agent_version": "",
  908. "enable_fingerprint_unification": true,
  909. "enable_metadata_passthrough": false,
  910. "web_search_emulation_enabled": false,
  911. "payment_visible_method_alipay_source": "easypay_alipay",
  912. "payment_visible_method_wxpay_source": "official_wxpay",
  913. "payment_visible_method_alipay_enabled": true,
  914. "payment_visible_method_wxpay_enabled": false,
  915. "openai_low_upstream_rate_priority_enabled": true,
  916. "openai_oauth_scheduling_rate_multiplier": 0.05,
  917. "openai_ttft_mode": "semantic",
  918. "openai_advanced_scheduler_enabled": true,
  919. "openai_advanced_scheduler_sticky_weighted_enabled": false,
  920. "openai_advanced_scheduler_subscription_priority_enabled": false,
  921. "openai_advanced_scheduler_lb_top_k": "",
  922. "openai_advanced_scheduler_weight_priority": "",
  923. "openai_advanced_scheduler_weight_load": "",
  924. "openai_advanced_scheduler_weight_queue": "",
  925. "openai_advanced_scheduler_weight_error_rate": "",
  926. "openai_advanced_scheduler_weight_ttft": "",
  927. "openai_advanced_scheduler_weight_reset": "",
  928. "openai_advanced_scheduler_weight_quota_headroom": "",
  929. "openai_advanced_scheduler_weight_upstream_cost": "",
  930. "openai_advanced_scheduler_weight_previous_response": "",
  931. "openai_advanced_scheduler_weight_session_sticky": "",
  932. "openai_advanced_scheduler_effective_lb_top_k": "7",
  933. "openai_advanced_scheduler_effective_weight_priority": "1",
  934. "openai_advanced_scheduler_effective_weight_load": "1",
  935. "openai_advanced_scheduler_effective_weight_queue": "0.7",
  936. "openai_advanced_scheduler_effective_weight_error_rate": "0.8",
  937. "openai_advanced_scheduler_effective_weight_ttft": "0.5",
  938. "openai_advanced_scheduler_effective_weight_reset": "0",
  939. "openai_advanced_scheduler_effective_weight_quota_headroom": "0",
  940. "openai_advanced_scheduler_effective_weight_upstream_cost": "0",
  941. "openai_advanced_scheduler_effective_weight_previous_response": "5",
  942. "openai_advanced_scheduler_effective_weight_session_sticky": "3",
  943. "openai_codex_user_agent": "",
  944. "openai_codex_client_version": "",
  945. "openai_codex_client_version_synced": "",
  946. "openai_codex_version_auto_sync_enabled": true,
  947. "openai_fast_policy_settings": {
  948. "rules": []
  949. },
  950. "custom_menu_items": [],
  951. "custom_endpoints": [],
  952. "payment_enabled": false,
  953. "payment_min_amount": 0,
  954. "payment_max_amount": 0,
  955. "payment_daily_limit": 0,
  956. "payment_order_timeout_minutes": 0,
  957. "payment_max_pending_orders": 0,
  958. "payment_balance_disabled": false,
  959. "payment_balance_recharge_multiplier": 0,
  960. "payment_subscription_usd_to_cny_rate": 0,
  961. "payment_recharge_fee_rate": 0,
  962. "payment_load_balance_strategy": "",
  963. "payment_product_name_prefix": "",
  964. "payment_product_name_suffix": "",
  965. "payment_help_image_url": "",
  966. "payment_help_text": "",
  967. "payment_enabled_types": null,
  968. "payment_cancel_rate_limit_enabled": false,
  969. "payment_cancel_rate_limit_max": 0,
  970. "payment_cancel_rate_limit_window": 0,
  971. "payment_cancel_rate_limit_unit": "",
  972. "payment_cancel_rate_limit_window_mode": "",
  973. "payment_alipay_force_qrcode": false,
  974. "payment_alipay_mobile_precreate_deep_link": false,
  975. "balance_low_notify_enabled": false,
  976. "account_quota_notify_enabled": false,
  977. "account_scheduling_thresholds": {"anthropic":100,"grok":100,"openai":100},
  978. "subscription_expiry_notify_enabled": true,
  979. "balance_low_notify_threshold": 0,
  980. "balance_low_notify_recharge_url": "",
  981. "account_quota_notify_emails": [],
  982. "channel_monitor_enabled": true,
  983. "channel_monitor_mode": "v1",
  984. "channel_monitor_hide_throughput": true,
  985. "channel_monitor_show_quota": false,
  986. "channel_monitor_hide_user_ranking": false,
  987. "channel_monitor_default_interval_seconds": 60,
  988. "available_channels_enabled": false,
  989. "model_plaza_enabled": false,
  990. "model_plaza_require_auth": false,
  991. "model_plaza_description": "",
  992. "plugin_management_enabled": false,
  993. "risk_control_enabled": false,
  994. "cyber_session_block_enabled": false,
  995. "cyber_session_block_ttl_seconds": 3600,
  996. "affiliate_enabled": false,
  997. "wechat_connect_enabled": false,
  998. "wechat_connect_app_id": "",
  999. "wechat_connect_app_secret_configured": false,
  1000. "wechat_connect_mode": "open",
  1001. "wechat_connect_open_enabled": false,
  1002. "wechat_connect_open_app_id": "",
  1003. "wechat_connect_open_app_secret_configured": false,
  1004. "wechat_connect_mp_enabled": false,
  1005. "wechat_connect_mp_app_id": "",
  1006. "wechat_connect_mp_app_secret_configured": false,
  1007. "wechat_connect_mobile_enabled": false,
  1008. "wechat_connect_mobile_app_id": "",
  1009. "wechat_connect_mobile_app_secret_configured": false,
  1010. "wechat_connect_redirect_url": "",
  1011. "wechat_connect_frontend_redirect_url": "/auth/wechat/callback",
  1012. "wechat_connect_scopes": "snsapi_login",
  1013. "allow_user_view_error_requests": false
  1014. }
  1015. }`,
  1016. },
  1017. {
  1018. name: "GET /api/v1/admin/settings falls back to config oauth defaults",
  1019. setup: func(t *testing.T, deps *contractDeps) {
  1020. t.Helper()
  1021. deps.cfg.OIDC = config.OIDCConnectConfig{
  1022. Enabled: true,
  1023. ProviderName: "ConfigOIDC",
  1024. ClientID: "oidc-config-client",
  1025. ClientSecret: "oidc-config-secret",
  1026. IssuerURL: "https://issuer.example.com",
  1027. RedirectURL: "https://api.example.com/api/v1/auth/oauth/oidc/callback",
  1028. FrontendRedirectURL: "/auth/oidc/callback",
  1029. Scopes: "openid email profile",
  1030. TokenAuthMethod: "client_secret_post",
  1031. UsePKCE: true,
  1032. ValidateIDToken: true,
  1033. AllowedSigningAlgs: "RS256,ES256,PS256",
  1034. ClockSkewSeconds: 120,
  1035. }
  1036. deps.cfg.WeChat = config.WeChatConnectConfig{
  1037. Enabled: true,
  1038. OpenEnabled: true,
  1039. OpenAppID: "wx-open-config",
  1040. OpenAppSecret: "wx-open-secret",
  1041. Mode: "open",
  1042. Scopes: "snsapi_login",
  1043. FrontendRedirectURL: "/auth/wechat/callback",
  1044. }
  1045. deps.settingRepo.SetAll(map[string]string{
  1046. service.SettingKeyRegistrationEnabled: "true",
  1047. service.SettingKeyEmailVerifyEnabled: "false",
  1048. service.SettingKeyRegistrationEmailSuffixWhitelist: "[]",
  1049. })
  1050. },
  1051. method: http.MethodGet,
  1052. path: "/api/v1/admin/settings",
  1053. wantStatus: http.StatusOK,
  1054. wantJSON: `{
  1055. "code": 0,
  1056. "message": "success",
  1057. "data": {
  1058. "registration_enabled": true,
  1059. "email_verify_enabled": false,
  1060. "registration_email_suffix_whitelist": [],
  1061. "registration_email_domain_quota_enabled": false,
  1062. "promo_code_enabled": true,
  1063. "password_reset_enabled": false,
  1064. "frontend_url": "",
  1065. "invitation_code_enabled": false,
  1066. "totp_enabled": false,
  1067. "totp_encryption_key_configured": false,
  1068. "passkey_enabled": false,
  1069. "passkey_configured": false,
  1070. "passkey_rp_id": "",
  1071. "passkey_rp_origins": [],
  1072. "session_binding_enabled": false,
  1073. "step_up_enabled": false,
  1074. "audit_log_retention_days": 180,
  1075. "login_agreement_enabled": false,
  1076. "login_agreement_mode": "modal",
  1077. "login_agreement_updated_at": "2026-03-31",
  1078. "login_agreement_documents": [
  1079. {"id": "terms", "title": "服务条款", "content_md": ""},
  1080. {"id": "usage-policy", "title": "使用政策", "content_md": ""},
  1081. {"id": "supported-regions", "title": "支持的国家和地区", "content_md": ""},
  1082. {"id": "service-specific-terms", "title": "服务特定条款", "content_md": ""}
  1083. ],
  1084. "smtp_host": "",
  1085. "smtp_port": 587,
  1086. "smtp_username": "",
  1087. "smtp_password_configured": false,
  1088. "smtp_from_email": "",
  1089. "smtp_from_name": "",
  1090. "smtp_use_tls": false,
  1091. "turnstile_enabled": false,
  1092. "turnstile_site_key": "",
  1093. "turnstile_secret_key_configured": false,
  1094. "tencent_captcha_enabled": false,
  1095. "tencent_captcha_app_id": "",
  1096. "tencent_captcha_app_secret_key_configured": false,
  1097. "tencent_captcha_cloud_secret_id_configured": false,
  1098. "tencent_captcha_cloud_secret_key_configured": false,
  1099. "tencent_captcha_region": "cn",
  1100. "aliyun_captcha_enabled": false,
  1101. "aliyun_captcha_access_key_id": "",
  1102. "aliyun_captcha_access_key_secret_configured": false,
  1103. "aliyun_captcha_scene_id": "",
  1104. "aliyun_captcha_prefix": "",
  1105. "aliyun_captcha_region": "cn",
  1106. "linuxdo_connect_enabled": false,
  1107. "linuxdo_connect_client_id": "",
  1108. "linuxdo_connect_client_secret_configured": false,
  1109. "linuxdo_connect_redirect_url": "",
  1110. "dingtalk_connect_enabled": false,
  1111. "dingtalk_connect_bypass_registration": false,
  1112. "dingtalk_connect_client_id": "",
  1113. "dingtalk_connect_client_secret_configured": false,
  1114. "dingtalk_connect_redirect_url": "",
  1115. "dingtalk_connect_internal_corp_id": "",
  1116. "dingtalk_connect_corp_restriction_policy": "",
  1117. "dingtalk_connect_sync_corp_email": false,
  1118. "dingtalk_connect_sync_corp_email_attr_key": "dingtalk_email",
  1119. "dingtalk_connect_sync_corp_email_attr_name": "钉钉企业邮箱",
  1120. "dingtalk_connect_sync_dept": false,
  1121. "dingtalk_connect_sync_dept_attr_key": "dingtalk_department",
  1122. "dingtalk_connect_sync_dept_attr_name": "钉钉部门",
  1123. "dingtalk_connect_sync_display_name": false,
  1124. "dingtalk_connect_sync_display_name_attr_key": "dingtalk_name",
  1125. "dingtalk_connect_sync_display_name_attr_name": "钉钉姓名",
  1126. "oidc_connect_enabled": true,
  1127. "oidc_connect_provider_name": "ConfigOIDC",
  1128. "oidc_connect_client_id": "oidc-config-client",
  1129. "oidc_connect_client_secret_configured": true,
  1130. "oidc_connect_issuer_url": "https://issuer.example.com",
  1131. "oidc_connect_discovery_url": "",
  1132. "oidc_connect_authorize_url": "",
  1133. "oidc_connect_token_url": "",
  1134. "oidc_connect_userinfo_url": "",
  1135. "oidc_connect_jwks_url": "",
  1136. "oidc_connect_scopes": "openid email profile",
  1137. "oidc_connect_redirect_url": "https://api.example.com/api/v1/auth/oauth/oidc/callback",
  1138. "oidc_connect_frontend_redirect_url": "/auth/oidc/callback",
  1139. "oidc_connect_token_auth_method": "client_secret_post",
  1140. "oidc_connect_use_pkce": true,
  1141. "oidc_connect_validate_id_token": true,
  1142. "oidc_connect_allowed_signing_algs": "RS256,ES256,PS256",
  1143. "oidc_connect_clock_skew_seconds": 120,
  1144. "oidc_connect_require_email_verified": false,
  1145. "oidc_connect_userinfo_email_path": "",
  1146. "oidc_connect_userinfo_id_path": "",
  1147. "oidc_connect_userinfo_username_path": "",
  1148. "github_oauth_enabled": false,
  1149. "github_oauth_client_id": "",
  1150. "github_oauth_client_secret_configured": false,
  1151. "github_oauth_redirect_url": "",
  1152. "github_oauth_frontend_redirect_url": "/auth/oauth/callback",
  1153. "google_oauth_enabled": false,
  1154. "google_oauth_client_id": "",
  1155. "google_oauth_client_secret_configured": false,
  1156. "google_oauth_redirect_url": "",
  1157. "google_oauth_frontend_redirect_url": "/auth/oauth/callback",
  1158. "site_name": "Sub2API",
  1159. "site_logo": "",
  1160. "site_subtitle": "Subscription to API Conversion Platform",
  1161. "api_base_url": "",
  1162. "api_key_acl_trust_forwarded_ip": false,
  1163. "forwarded_client_ip_headers": [],
  1164. "contact_info": "",
  1165. "doc_url": "",
  1166. "home_content": "",
  1167. "hide_ccs_import_button": false,
  1168. "grok_default_text_model": "grok-4.6",
  1169. "grok_default_base_url_mode": "cli",
  1170. "grok_cross_client_model_map_enabled": true,
  1171. "purchase_subscription_enabled": false,
  1172. "purchase_subscription_url": "",
  1173. "table_default_page_size": 20,
  1174. "table_page_size_options": [10, 20, 50],
  1175. "default_platform_quotas": {"anthropic":{"daily":null,"weekly":null,"monthly":null},"antigravity":{"daily":null,"weekly":null,"monthly":null},"deepseek":{"daily":null,"weekly":null,"monthly":null},"gemini":{"daily":null,"weekly":null,"monthly":null},"grok":{"daily":null,"weekly":null,"monthly":null},"kimi":{"daily":null,"weekly":null,"monthly":null},"minimax":{"daily":null,"weekly":null,"monthly":null},"openai":{"daily":null,"weekly":null,"monthly":null},"zhipu":{"daily":null,"weekly":null,"monthly":null}},
  1176. "auth_source_default_email_platform_quotas": null,
  1177. "auth_source_default_github_platform_quotas": null,
  1178. "auth_source_default_google_platform_quotas": null,
  1179. "auth_source_default_linuxdo_platform_quotas": null,
  1180. "auth_source_default_oidc_platform_quotas": null,
  1181. "auth_source_default_wechat_platform_quotas": null,
  1182. "auth_source_default_dingtalk_platform_quotas": null,
  1183. "custom_menu_items": [],
  1184. "custom_endpoints": [],
  1185. "default_concurrency": 0,
  1186. "default_balance": 0,
  1187. "affiliate_rebate_rate": 20,
  1188. "affiliate_rebate_freeze_hours": 0,
  1189. "affiliate_rebate_duration_days": 0,
  1190. "affiliate_rebate_per_invitee_cap": 0,
  1191. "affiliate_admin_recharge_enabled": false,
  1192. "default_user_rpm_limit": 0,
  1193. "default_subscriptions": [],
  1194. "enable_model_fallback": false,
  1195. "fallback_model_anthropic": "claude-3-5-sonnet-20241022",
  1196. "fallback_model_openai": "gpt-4o",
  1197. "fallback_model_gemini": "gemini-2.5-pro",
  1198. "fallback_model_antigravity": "gemini-2.5-pro",
  1199. "enable_identity_patch": true,
  1200. "identity_patch_prompt": "",
  1201. "ops_monitoring_enabled": false,
  1202. "ops_realtime_monitoring_enabled": true,
  1203. "ops_query_mode_default": "auto",
  1204. "ops_metrics_interval_seconds": 60,
  1205. "min_claude_code_version": "",
  1206. "max_claude_code_version": "",
  1207. "allow_ungrouped_key_scheduling": false,
  1208. "backend_mode_enabled": false,
  1209. "enable_fingerprint_unification": true,
  1210. "enable_metadata_passthrough": false,
  1211. "enable_cch_signing": false,
  1212. "enable_claude_oauth_system_prompt_injection": true,
  1213. "claude_oauth_system_prompt": "",
  1214. "claude_oauth_system_prompt_blocks": "",
  1215. "enable_anthropic_cache_ttl_1h_injection": false,
  1216. "rewrite_message_cache_control": false,
  1217. "enable_client_dateline_normalization": true,
  1218. "antigravity_user_agent_version": "",
  1219. "min_codex_version": "",
  1220. "max_codex_version": "",
  1221. "codex_cli_only_blacklist": "",
  1222. "codex_cli_only_whitelist": "",
  1223. "compact_home_enabled": false,
  1224. "codex_cli_only_allow_app_server_clients": false,
  1225. "codex_cli_only_engine_fingerprint_signals": "[{\"type\":\"header_prefix\",\"match\":[\"x-codex-\"],\"required\":true},{\"type\":\"header_exact\",\"match\":[\"session-id\",\"session_id\"],\"required\":false},{\"type\":\"header_exact\",\"match\":[\"thread-id\",\"thread_id\"],\"required\":false},{\"type\":\"body_path\",\"match\":[\"client_metadata.x-codex-window-id\",\"client_metadata.x-codex-installation-id\"],\"required\":false}]",
  1226. "web_search_emulation_enabled": false,
  1227. "payment_visible_method_alipay_source": "",
  1228. "payment_visible_method_wxpay_source": "",
  1229. "payment_visible_method_alipay_enabled": false,
  1230. "payment_visible_method_wxpay_enabled": false,
  1231. "openai_low_upstream_rate_priority_enabled": false,
  1232. "openai_oauth_scheduling_rate_multiplier": 1,
  1233. "openai_ttft_mode": "semantic",
  1234. "openai_advanced_scheduler_enabled": false,
  1235. "openai_advanced_scheduler_sticky_weighted_enabled": false,
  1236. "openai_advanced_scheduler_subscription_priority_enabled": false,
  1237. "openai_advanced_scheduler_lb_top_k": "",
  1238. "openai_advanced_scheduler_weight_priority": "",
  1239. "openai_advanced_scheduler_weight_load": "",
  1240. "openai_advanced_scheduler_weight_queue": "",
  1241. "openai_advanced_scheduler_weight_error_rate": "",
  1242. "openai_advanced_scheduler_weight_ttft": "",
  1243. "openai_advanced_scheduler_weight_reset": "",
  1244. "openai_advanced_scheduler_weight_quota_headroom": "",
  1245. "openai_advanced_scheduler_weight_upstream_cost": "",
  1246. "openai_advanced_scheduler_weight_previous_response": "",
  1247. "openai_advanced_scheduler_weight_session_sticky": "",
  1248. "openai_advanced_scheduler_effective_lb_top_k": "7",
  1249. "openai_advanced_scheduler_effective_weight_priority": "1",
  1250. "openai_advanced_scheduler_effective_weight_load": "1",
  1251. "openai_advanced_scheduler_effective_weight_queue": "0.7",
  1252. "openai_advanced_scheduler_effective_weight_error_rate": "0.8",
  1253. "openai_advanced_scheduler_effective_weight_ttft": "0.5",
  1254. "openai_advanced_scheduler_effective_weight_reset": "0",
  1255. "openai_advanced_scheduler_effective_weight_quota_headroom": "0",
  1256. "openai_advanced_scheduler_effective_weight_upstream_cost": "0",
  1257. "openai_advanced_scheduler_effective_weight_previous_response": "5",
  1258. "openai_advanced_scheduler_effective_weight_session_sticky": "3",
  1259. "openai_codex_user_agent": "",
  1260. "openai_codex_client_version": "",
  1261. "openai_codex_client_version_synced": "",
  1262. "openai_codex_version_auto_sync_enabled": true,
  1263. "openai_fast_policy_settings": {
  1264. "rules": []
  1265. },
  1266. "payment_enabled": false,
  1267. "payment_min_amount": 0,
  1268. "payment_max_amount": 0,
  1269. "payment_daily_limit": 0,
  1270. "payment_order_timeout_minutes": 0,
  1271. "payment_max_pending_orders": 0,
  1272. "payment_enabled_types": null,
  1273. "payment_balance_disabled": false,
  1274. "payment_balance_recharge_multiplier": 0,
  1275. "payment_subscription_usd_to_cny_rate": 0,
  1276. "payment_recharge_fee_rate": 0,
  1277. "payment_load_balance_strategy": "",
  1278. "payment_product_name_prefix": "",
  1279. "payment_product_name_suffix": "",
  1280. "payment_help_image_url": "",
  1281. "payment_help_text": "",
  1282. "payment_cancel_rate_limit_enabled": false,
  1283. "payment_cancel_rate_limit_max": 0,
  1284. "payment_cancel_rate_limit_window": 0,
  1285. "payment_cancel_rate_limit_unit": "",
  1286. "payment_cancel_rate_limit_window_mode": "",
  1287. "payment_alipay_force_qrcode": false,
  1288. "payment_alipay_mobile_precreate_deep_link": false,
  1289. "balance_low_notify_enabled": false,
  1290. "account_quota_notify_enabled": false,
  1291. "account_scheduling_thresholds": {"anthropic":100,"grok":100,"openai":100},
  1292. "subscription_expiry_notify_enabled": true,
  1293. "balance_low_notify_threshold": 0,
  1294. "balance_low_notify_recharge_url": "",
  1295. "account_quota_notify_emails": [],
  1296. "channel_monitor_enabled": true,
  1297. "channel_monitor_mode": "v1",
  1298. "channel_monitor_hide_throughput": true,
  1299. "channel_monitor_show_quota": false,
  1300. "channel_monitor_hide_user_ranking": false,
  1301. "channel_monitor_default_interval_seconds": 60,
  1302. "available_channels_enabled": false,
  1303. "model_plaza_enabled": false,
  1304. "model_plaza_require_auth": false,
  1305. "model_plaza_description": "",
  1306. "plugin_management_enabled": false,
  1307. "risk_control_enabled": false,
  1308. "cyber_session_block_enabled": false,
  1309. "cyber_session_block_ttl_seconds": 3600,
  1310. "affiliate_enabled": false,
  1311. "wechat_connect_enabled": true,
  1312. "wechat_connect_app_id": "wx-open-config",
  1313. "wechat_connect_app_secret_configured": true,
  1314. "wechat_connect_mode": "open",
  1315. "wechat_connect_open_enabled": true,
  1316. "wechat_connect_open_app_id": "wx-open-config",
  1317. "wechat_connect_open_app_secret_configured": true,
  1318. "wechat_connect_mp_enabled": false,
  1319. "wechat_connect_mp_app_id": "wx-open-config",
  1320. "wechat_connect_mp_app_secret_configured": true,
  1321. "wechat_connect_mobile_enabled": false,
  1322. "wechat_connect_mobile_app_id": "wx-open-config",
  1323. "wechat_connect_mobile_app_secret_configured": true,
  1324. "wechat_connect_redirect_url": "",
  1325. "wechat_connect_frontend_redirect_url": "/auth/wechat/callback",
  1326. "wechat_connect_scopes": "snsapi_login",
  1327. "auth_source_default_email_balance": 0,
  1328. "auth_source_default_email_concurrency": 5,
  1329. "auth_source_default_email_subscriptions": [],
  1330. "auth_source_default_email_grant_on_signup": false,
  1331. "auth_source_default_email_grant_on_first_bind": false,
  1332. "auth_source_default_github_balance": 0,
  1333. "auth_source_default_github_concurrency": 5,
  1334. "auth_source_default_github_subscriptions": [],
  1335. "auth_source_default_github_grant_on_signup": false,
  1336. "auth_source_default_github_grant_on_first_bind": false,
  1337. "auth_source_default_google_balance": 0,
  1338. "auth_source_default_google_concurrency": 5,
  1339. "auth_source_default_google_subscriptions": [],
  1340. "auth_source_default_google_grant_on_signup": false,
  1341. "auth_source_default_google_grant_on_first_bind": false,
  1342. "auth_source_default_linuxdo_balance": 0,
  1343. "auth_source_default_linuxdo_concurrency": 5,
  1344. "auth_source_default_linuxdo_subscriptions": [],
  1345. "auth_source_default_linuxdo_grant_on_signup": false,
  1346. "auth_source_default_linuxdo_grant_on_first_bind": false,
  1347. "auth_source_default_oidc_balance": 0,
  1348. "auth_source_default_oidc_concurrency": 5,
  1349. "auth_source_default_oidc_subscriptions": [],
  1350. "auth_source_default_oidc_grant_on_signup": false,
  1351. "auth_source_default_oidc_grant_on_first_bind": false,
  1352. "auth_source_default_wechat_balance": 0,
  1353. "auth_source_default_wechat_concurrency": 5,
  1354. "auth_source_default_wechat_subscriptions": [],
  1355. "auth_source_default_wechat_grant_on_signup": false,
  1356. "auth_source_default_wechat_grant_on_first_bind": false,
  1357. "auth_source_default_dingtalk_balance": 0,
  1358. "auth_source_default_dingtalk_concurrency": 5,
  1359. "auth_source_default_dingtalk_subscriptions": [],
  1360. "auth_source_default_dingtalk_grant_on_signup": false,
  1361. "auth_source_default_dingtalk_grant_on_first_bind": false,
  1362. "force_email_on_third_party_signup": false,
  1363. "allow_user_view_error_requests": false
  1364. }
  1365. }`,
  1366. },
  1367. {
  1368. name: "POST /api/v1/admin/accounts/bulk-update",
  1369. method: http.MethodPost,
  1370. path: "/api/v1/admin/accounts/bulk-update",
  1371. body: `{"account_ids":[101,102],"schedulable":false}`,
  1372. headers: map[string]string{
  1373. "Content-Type": "application/json",
  1374. },
  1375. wantStatus: http.StatusOK,
  1376. wantJSON: `{
  1377. "code": 0,
  1378. "message": "success",
  1379. "data": {
  1380. "success": 2,
  1381. "failed": 0,
  1382. "success_ids": [101, 102],
  1383. "failed_ids": [],
  1384. "results": [
  1385. {"account_id": 101, "success": true},
  1386. {"account_id": 102, "success": true}
  1387. ]
  1388. }
  1389. }`,
  1390. },
  1391. }
  1392. for _, tt := range tests {
  1393. t.Run(tt.name, func(t *testing.T) {
  1394. deps := newContractDeps(t)
  1395. if tt.setup != nil {
  1396. tt.setup(t, deps)
  1397. }
  1398. status, body := doRequest(t, deps.router, tt.method, tt.path, tt.body, tt.headers)
  1399. require.Equal(t, tt.wantStatus, status)
  1400. require.JSONEq(t, tt.wantJSON, body)
  1401. })
  1402. }
  1403. }
  1404. type contractDeps struct {
  1405. now time.Time
  1406. router http.Handler
  1407. cfg *config.Config
  1408. apiKeyRepo *stubApiKeyRepo
  1409. groupRepo *stubGroupRepo
  1410. userSubRepo *stubUserSubscriptionRepo
  1411. usageRepo *stubUsageLogRepo
  1412. settingRepo *stubSettingRepo
  1413. redeemRepo *stubRedeemCodeRepo
  1414. }
  1415. func newContractDeps(t *testing.T) *contractDeps {
  1416. t.Helper()
  1417. now := time.Date(2025, 1, 2, 3, 4, 5, 0, time.UTC)
  1418. userRepo := &stubUserRepo{
  1419. users: map[int64]*service.User{
  1420. 1: {
  1421. ID: 1,
  1422. Email: "alice@example.com",
  1423. Username: "alice",
  1424. Notes: "hello",
  1425. Role: service.RoleUser,
  1426. Balance: 12.5,
  1427. Concurrency: 5,
  1428. Status: service.StatusActive,
  1429. AllowedGroups: nil,
  1430. CreatedAt: now,
  1431. UpdatedAt: now,
  1432. },
  1433. },
  1434. }
  1435. apiKeyRepo := newStubApiKeyRepo(now)
  1436. apiKeyCache := stubApiKeyCache{}
  1437. groupRepo := &stubGroupRepo{}
  1438. userSubRepo := &stubUserSubscriptionRepo{}
  1439. accountRepo := stubAccountRepo{}
  1440. proxyRepo := stubProxyRepo{}
  1441. redeemRepo := &stubRedeemCodeRepo{}
  1442. cfg := &config.Config{
  1443. Default: config.DefaultConfig{
  1444. APIKeyPrefix: "sk-",
  1445. },
  1446. RunMode: config.RunModeStandard,
  1447. }
  1448. userService := service.NewUserService(userRepo, nil, nil, nil)
  1449. apiKeyService := service.NewAPIKeyService(apiKeyRepo, userRepo, groupRepo, userSubRepo, nil, apiKeyCache, cfg)
  1450. usageRepo := newStubUsageLogRepo()
  1451. usageService := service.NewUsageService(usageRepo, userRepo, nil, nil)
  1452. subscriptionService := service.NewSubscriptionService(groupRepo, userSubRepo, nil, nil, cfg)
  1453. subscriptionHandler := handler.NewSubscriptionHandler(subscriptionService)
  1454. redeemService := service.NewRedeemService(redeemRepo, userRepo, subscriptionService, nil, nil, nil, nil, nil)
  1455. redeemHandler := handler.NewRedeemHandler(redeemService)
  1456. settingRepo := newStubSettingRepo()
  1457. settingService := service.NewSettingService(settingRepo, cfg)
  1458. adminService := service.NewAdminService(nil, userRepo, groupRepo, &accountRepo, proxyRepo, apiKeyRepo, redeemRepo, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
  1459. authHandler := handler.NewAuthHandler(cfg, nil, userService, settingService, nil, redeemService, nil, nil)
  1460. apiKeyHandler := handler.NewAPIKeyHandler(apiKeyService)
  1461. usageHandler := handler.NewUsageHandler(usageService, apiKeyService, nil, nil)
  1462. adminSettingHandler := adminhandler.NewSettingHandler(settingService, nil, nil, nil, nil, nil, nil)
  1463. adminAccountHandler := adminhandler.NewAccountHandler(adminService, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
  1464. jwtAuth := func(c *gin.Context) {
  1465. c.Set(string(middleware.ContextKeyUser), middleware.AuthSubject{
  1466. UserID: 1,
  1467. Concurrency: 5,
  1468. })
  1469. c.Set(string(middleware.ContextKeyUserRole), service.RoleUser)
  1470. c.Next()
  1471. }
  1472. adminAuth := func(c *gin.Context) {
  1473. c.Set(string(middleware.ContextKeyUser), middleware.AuthSubject{
  1474. UserID: 1,
  1475. Concurrency: 5,
  1476. })
  1477. c.Set(string(middleware.ContextKeyUserRole), service.RoleAdmin)
  1478. c.Next()
  1479. }
  1480. r := gin.New()
  1481. v1 := r.Group("/api/v1")
  1482. v1Auth := v1.Group("")
  1483. v1Auth.Use(jwtAuth)
  1484. v1Auth.GET("/auth/me", authHandler.GetCurrentUser)
  1485. v1Keys := v1.Group("")
  1486. v1Keys.Use(jwtAuth)
  1487. v1Keys.GET("/keys", apiKeyHandler.List)
  1488. v1Keys.POST("/keys", apiKeyHandler.Create)
  1489. v1Keys.GET("/groups/available", apiKeyHandler.GetAvailableGroups)
  1490. v1Usage := v1.Group("")
  1491. v1Usage.Use(jwtAuth)
  1492. v1Usage.GET("/usage", usageHandler.List)
  1493. v1Usage.GET("/usage/stats", usageHandler.Stats)
  1494. v1Subs := v1.Group("")
  1495. v1Subs.Use(jwtAuth)
  1496. v1Subs.GET("/subscriptions", subscriptionHandler.List)
  1497. v1Redeem := v1.Group("")
  1498. v1Redeem.Use(jwtAuth)
  1499. v1Redeem.GET("/redeem/history", redeemHandler.GetHistory)
  1500. v1Admin := v1.Group("/admin")
  1501. v1Admin.Use(adminAuth)
  1502. v1Admin.GET("/settings", adminSettingHandler.GetSettings)
  1503. v1Admin.POST("/accounts/bulk-update", adminAccountHandler.BulkUpdate)
  1504. return &contractDeps{
  1505. now: now,
  1506. router: r,
  1507. cfg: cfg,
  1508. apiKeyRepo: apiKeyRepo,
  1509. groupRepo: groupRepo,
  1510. userSubRepo: userSubRepo,
  1511. usageRepo: usageRepo,
  1512. settingRepo: settingRepo,
  1513. redeemRepo: redeemRepo,
  1514. }
  1515. }
  1516. func doRequest(t *testing.T, router http.Handler, method, path, body string, headers map[string]string) (int, string) {
  1517. t.Helper()
  1518. req := httptest.NewRequest(method, path, bytes.NewBufferString(body))
  1519. for k, v := range headers {
  1520. req.Header.Set(k, v)
  1521. }
  1522. w := httptest.NewRecorder()
  1523. router.ServeHTTP(w, req)
  1524. respBody, err := io.ReadAll(w.Result().Body)
  1525. require.NoError(t, err)
  1526. return w.Result().StatusCode, string(respBody)
  1527. }
  1528. func ptr[T any](v T) *T { return &v }
  1529. type stubUserRepo struct {
  1530. users map[int64]*service.User
  1531. }
  1532. func (r *stubUserRepo) Create(ctx context.Context, user *service.User) error {
  1533. return errors.New("not implemented")
  1534. }
  1535. func (r *stubUserRepo) CreateWithEmailAliasGuard(ctx context.Context, user *service.User) error {
  1536. return errors.New("not implemented")
  1537. }
  1538. func (r *stubUserRepo) GetByID(ctx context.Context, id int64) (*service.User, error) {
  1539. user, ok := r.users[id]
  1540. if !ok {
  1541. return nil, service.ErrUserNotFound
  1542. }
  1543. clone := *user
  1544. return &clone, nil
  1545. }
  1546. func (r *stubUserRepo) GetByEmail(ctx context.Context, email string) (*service.User, error) {
  1547. for _, user := range r.users {
  1548. if user.Email == email {
  1549. clone := *user
  1550. return &clone, nil
  1551. }
  1552. }
  1553. return nil, service.ErrUserNotFound
  1554. }
  1555. func (r *stubUserRepo) GetFirstAdmin(ctx context.Context) (*service.User, error) {
  1556. for _, user := range r.users {
  1557. if user.Role == service.RoleAdmin && user.Status == service.StatusActive {
  1558. clone := *user
  1559. return &clone, nil
  1560. }
  1561. }
  1562. return nil, service.ErrUserNotFound
  1563. }
  1564. func (r *stubUserRepo) Update(ctx context.Context, user *service.User, fields service.UserUpdateFields) error {
  1565. return errors.New("not implemented")
  1566. }
  1567. func (r *stubUserRepo) Delete(ctx context.Context, id int64) error {
  1568. return errors.New("not implemented")
  1569. }
  1570. func (r *stubUserRepo) GetUserAvatar(ctx context.Context, userID int64) (*service.UserAvatar, error) {
  1571. return nil, nil
  1572. }
  1573. func (r *stubUserRepo) UpsertUserAvatar(ctx context.Context, userID int64, input service.UpsertUserAvatarInput) (*service.UserAvatar, error) {
  1574. return nil, errors.New("not implemented")
  1575. }
  1576. func (r *stubUserRepo) DeleteUserAvatar(ctx context.Context, userID int64) error {
  1577. return errors.New("not implemented")
  1578. }
  1579. func (r *stubUserRepo) List(ctx context.Context, params pagination.PaginationParams) ([]service.User, *pagination.PaginationResult, error) {
  1580. return nil, nil, errors.New("not implemented")
  1581. }
  1582. func (r *stubUserRepo) ListWithFilters(ctx context.Context, params pagination.PaginationParams, filters service.UserListFilters) ([]service.User, *pagination.PaginationResult, error) {
  1583. return nil, nil, errors.New("not implemented")
  1584. }
  1585. func (r *stubUserRepo) UpdateBalance(ctx context.Context, id int64, amount float64) error {
  1586. return errors.New("not implemented")
  1587. }
  1588. func (r *stubUserRepo) DeductBalance(ctx context.Context, id int64, amount float64) error {
  1589. return errors.New("not implemented")
  1590. }
  1591. func (r *stubUserRepo) AdjustBalance(ctx context.Context, id int64, delta float64) (service.BalanceChange, error) {
  1592. return service.BalanceChange{}, errors.New("not implemented")
  1593. }
  1594. func (r *stubUserRepo) SetBalance(ctx context.Context, id int64, value float64) (service.BalanceChange, error) {
  1595. return service.BalanceChange{}, errors.New("not implemented")
  1596. }
  1597. func (r *stubUserRepo) UpdateConcurrency(ctx context.Context, id int64, amount int) error {
  1598. return errors.New("not implemented")
  1599. }
  1600. func (r *stubUserRepo) BatchSetConcurrency(context.Context, []int64, int) (int, error) { return 0, nil }
  1601. func (r *stubUserRepo) BatchAddConcurrency(context.Context, []int64, int) (int, error) { return 0, nil }
  1602. func (r *stubUserRepo) BatchUpdateLimits(context.Context, []int64, *int, *int) (int, error) {
  1603. return 0, nil
  1604. }
  1605. func (r *stubUserRepo) ExistsByEmail(ctx context.Context, email string) (bool, error) {
  1606. return false, errors.New("not implemented")
  1607. }
  1608. func (r *stubUserRepo) ExistsByEmailAlias(ctx context.Context, email string) (bool, error) {
  1609. return false, errors.New("not implemented")
  1610. }
  1611. func (r *stubUserRepo) RemoveGroupFromAllowedGroups(ctx context.Context, groupID int64) (int64, error) {
  1612. return 0, errors.New("not implemented")
  1613. }
  1614. func (r *stubUserRepo) RemoveGroupFromUserAllowedGroups(ctx context.Context, userID int64, groupID int64) error {
  1615. return errors.New("not implemented")
  1616. }
  1617. func (r *stubUserRepo) AddGroupToAllowedGroups(ctx context.Context, userID int64, groupID int64) error {
  1618. return errors.New("not implemented")
  1619. }
  1620. func (r *stubUserRepo) ListUserAuthIdentities(ctx context.Context, userID int64) ([]service.UserAuthIdentityRecord, error) {
  1621. return nil, nil
  1622. }
  1623. func (r *stubUserRepo) UnbindUserAuthProvider(context.Context, int64, string) error {
  1624. return errors.New("not implemented")
  1625. }
  1626. func (r *stubUserRepo) GetLatestUsedAtByUserIDs(ctx context.Context, userIDs []int64) (map[int64]*time.Time, error) {
  1627. return map[int64]*time.Time{}, nil
  1628. }
  1629. func (r *stubUserRepo) GetLatestUsedAtByUserID(ctx context.Context, userID int64) (*time.Time, error) {
  1630. return nil, nil
  1631. }
  1632. func (r *stubUserRepo) UpdateUserLastActiveAt(ctx context.Context, userID int64, activeAt time.Time) error {
  1633. return nil
  1634. }
  1635. func (r *stubUserRepo) UpdateTotpSecret(ctx context.Context, userID int64, encryptedSecret *string) error {
  1636. return errors.New("not implemented")
  1637. }
  1638. func (r *stubUserRepo) EnableTotp(ctx context.Context, userID int64) error {
  1639. return errors.New("not implemented")
  1640. }
  1641. func (r *stubUserRepo) DisableTotp(ctx context.Context, userID int64) error {
  1642. return errors.New("not implemented")
  1643. }
  1644. func (r *stubUserRepo) GetByIDIncludeDeleted(ctx context.Context, id int64) (*service.User, error) {
  1645. panic("unexpected GetByIDIncludeDeleted call")
  1646. }
  1647. type stubApiKeyCache struct{}
  1648. func (stubApiKeyCache) GetCreateAttemptCount(ctx context.Context, userID int64) (int, error) {
  1649. return 0, nil
  1650. }
  1651. func (stubApiKeyCache) IncrementCreateAttemptCount(ctx context.Context, userID int64) error {
  1652. return nil
  1653. }
  1654. func (stubApiKeyCache) DeleteCreateAttemptCount(ctx context.Context, userID int64) error {
  1655. return nil
  1656. }
  1657. func (stubApiKeyCache) IncrementDailyUsage(ctx context.Context, apiKey string) error {
  1658. return nil
  1659. }
  1660. func (stubApiKeyCache) SetDailyUsageExpiry(ctx context.Context, apiKey string, ttl time.Duration) error {
  1661. return nil
  1662. }
  1663. func (stubApiKeyCache) GetAuthCache(ctx context.Context, key string) (*service.APIKeyAuthCacheEntry, error) {
  1664. return nil, nil
  1665. }
  1666. func (stubApiKeyCache) SetAuthCache(ctx context.Context, key string, entry *service.APIKeyAuthCacheEntry, ttl time.Duration) error {
  1667. return nil
  1668. }
  1669. func (stubApiKeyCache) DeleteAuthCache(ctx context.Context, key string) error {
  1670. return nil
  1671. }
  1672. func (stubApiKeyCache) PublishAuthCacheInvalidation(ctx context.Context, cacheKey string) error {
  1673. return nil
  1674. }
  1675. func (stubApiKeyCache) SubscribeAuthCacheInvalidation(ctx context.Context, handler func(cacheKey string)) error {
  1676. return nil
  1677. }
  1678. type stubGroupRepo struct {
  1679. active []service.Group
  1680. }
  1681. func (r *stubGroupRepo) SetActive(groups []service.Group) {
  1682. r.active = append([]service.Group(nil), groups...)
  1683. }
  1684. func (stubGroupRepo) Create(ctx context.Context, group *service.Group) error {
  1685. return errors.New("not implemented")
  1686. }
  1687. func (stubGroupRepo) GetByID(ctx context.Context, id int64) (*service.Group, error) {
  1688. return nil, service.ErrGroupNotFound
  1689. }
  1690. func (stubGroupRepo) GetByIDLite(ctx context.Context, id int64) (*service.Group, error) {
  1691. return nil, service.ErrGroupNotFound
  1692. }
  1693. func (stubGroupRepo) Update(ctx context.Context, group *service.Group) error {
  1694. return errors.New("not implemented")
  1695. }
  1696. func (stubGroupRepo) Delete(ctx context.Context, id int64) error {
  1697. return errors.New("not implemented")
  1698. }
  1699. func (stubGroupRepo) DeleteCascade(ctx context.Context, id int64) ([]int64, error) {
  1700. return nil, errors.New("not implemented")
  1701. }
  1702. func (stubGroupRepo) DeleteCascadeIfEmpty(ctx context.Context, id int64) ([]int64, error) {
  1703. return nil, errors.New("not implemented")
  1704. }
  1705. func (stubGroupRepo) List(ctx context.Context, params pagination.PaginationParams) ([]service.Group, *pagination.PaginationResult, error) {
  1706. return nil, nil, errors.New("not implemented")
  1707. }
  1708. func (stubGroupRepo) ListWithFilters(ctx context.Context, params pagination.PaginationParams, platform, status, search string, isExclusive *bool) ([]service.Group, *pagination.PaginationResult, error) {
  1709. return nil, nil, errors.New("not implemented")
  1710. }
  1711. func (r *stubGroupRepo) ListActive(ctx context.Context) ([]service.Group, error) {
  1712. return append([]service.Group(nil), r.active...), nil
  1713. }
  1714. func (r *stubGroupRepo) ListActiveByPlatform(ctx context.Context, platform string) ([]service.Group, error) {
  1715. out := make([]service.Group, 0, len(r.active))
  1716. for i := range r.active {
  1717. g := r.active[i]
  1718. if g.Platform == platform {
  1719. out = append(out, g)
  1720. }
  1721. }
  1722. return out, nil
  1723. }
  1724. func (stubGroupRepo) ExistsByName(ctx context.Context, name string) (bool, error) {
  1725. return false, errors.New("not implemented")
  1726. }
  1727. func (stubGroupRepo) GetAccountCount(ctx context.Context, groupID int64) (int64, int64, error) {
  1728. return 0, 0, errors.New("not implemented")
  1729. }
  1730. func (stubGroupRepo) DeleteAccountGroupsByGroupID(ctx context.Context, groupID int64) (int64, error) {
  1731. return 0, errors.New("not implemented")
  1732. }
  1733. func (stubGroupRepo) BindAccountsToGroup(ctx context.Context, groupID int64, accountIDs []int64) error {
  1734. return errors.New("not implemented")
  1735. }
  1736. func (stubGroupRepo) GetAccountIDsByGroupIDs(ctx context.Context, groupIDs []int64) ([]int64, error) {
  1737. return nil, errors.New("not implemented")
  1738. }
  1739. func (stubGroupRepo) UpdateSortOrders(ctx context.Context, updates []service.GroupSortOrderUpdate) error {
  1740. return nil
  1741. }
  1742. func (stubGroupRepo) FindByDuplicateOperationID(ctx context.Context, operationID string) (*service.Group, error) {
  1743. return nil, nil
  1744. }
  1745. func (stubGroupRepo) CreateFromSource(ctx context.Context, group *service.Group, sourceGroupID int64) error {
  1746. return errors.New("not implemented")
  1747. }
  1748. type stubAccountRepo struct {
  1749. bulkUpdateIDs []int64
  1750. }
  1751. func (s *stubAccountRepo) Create(ctx context.Context, account *service.Account) error {
  1752. return errors.New("not implemented")
  1753. }
  1754. func (s *stubAccountRepo) CreateWithAccountGroups(ctx context.Context, account *service.Account, groups []service.AccountGroup) error {
  1755. return errors.New("not implemented")
  1756. }
  1757. func (s *stubAccountRepo) GetByID(ctx context.Context, id int64) (*service.Account, error) {
  1758. return nil, service.ErrAccountNotFound
  1759. }
  1760. func (s *stubAccountRepo) GetByIDs(ctx context.Context, ids []int64) ([]*service.Account, error) {
  1761. return nil, errors.New("not implemented")
  1762. }
  1763. func (s *stubAccountRepo) ExistsByID(ctx context.Context, id int64) (bool, error) {
  1764. return false, errors.New("not implemented")
  1765. }
  1766. func (s *stubAccountRepo) GetByCRSAccountID(ctx context.Context, crsAccountID string) (*service.Account, error) {
  1767. return nil, errors.New("not implemented")
  1768. }
  1769. func (s *stubAccountRepo) FindByExtraField(ctx context.Context, key string, value any) ([]service.Account, error) {
  1770. return nil, errors.New("not implemented")
  1771. }
  1772. func (s *stubAccountRepo) Update(ctx context.Context, account *service.Account) error {
  1773. return errors.New("not implemented")
  1774. }
  1775. func (s *stubAccountRepo) UpdateWithAccountBillingSettings(
  1776. ctx context.Context,
  1777. account *service.Account,
  1778. probeEnabled *bool,
  1779. rateSyncEnabled *bool,
  1780. rateMultiplier *float64,
  1781. ) error {
  1782. return errors.New("not implemented")
  1783. }
  1784. func (s *stubAccountRepo) Delete(ctx context.Context, id int64) error {
  1785. return errors.New("not implemented")
  1786. }
  1787. func (s *stubAccountRepo) List(ctx context.Context, params pagination.PaginationParams) ([]service.Account, *pagination.PaginationResult, error) {
  1788. return nil, nil, errors.New("not implemented")
  1789. }
  1790. func (s *stubAccountRepo) ListAllWithFilters(context.Context, string, string, string, string, int64, string) ([]service.Account, error) {
  1791. return nil, nil
  1792. }
  1793. func (s *stubAccountRepo) ListWithFilters(ctx context.Context, params pagination.PaginationParams, platform, accountType, status, search string, groupID int64, privacyMode string) ([]service.Account, *pagination.PaginationResult, error) {
  1794. return nil, nil, errors.New("not implemented")
  1795. }
  1796. func (s *stubAccountRepo) ListByGroup(ctx context.Context, groupID int64) ([]service.Account, error) {
  1797. return nil, errors.New("not implemented")
  1798. }
  1799. func (s *stubAccountRepo) ListActive(ctx context.Context) ([]service.Account, error) {
  1800. return nil, errors.New("not implemented")
  1801. }
  1802. func (s *stubAccountRepo) ListOAuthRefreshCandidates(ctx context.Context) ([]service.Account, error) {
  1803. return nil, errors.New("not implemented")
  1804. }
  1805. func (s *stubAccountRepo) ListByPlatform(ctx context.Context, platform string) ([]service.Account, error) {
  1806. return nil, errors.New("not implemented")
  1807. }
  1808. func (s *stubAccountRepo) UpdateLastUsed(ctx context.Context, id int64) error {
  1809. return errors.New("not implemented")
  1810. }
  1811. func (s *stubAccountRepo) BatchUpdateLastUsed(ctx context.Context, updates map[int64]time.Time) error {
  1812. return errors.New("not implemented")
  1813. }
  1814. func (s *stubAccountRepo) SetError(ctx context.Context, id int64, errorMsg string) error {
  1815. return errors.New("not implemented")
  1816. }
  1817. func (s *stubAccountRepo) ClearError(ctx context.Context, id int64) error {
  1818. return errors.New("not implemented")
  1819. }
  1820. func (s *stubAccountRepo) SetSchedulable(ctx context.Context, id int64, schedulable bool) error {
  1821. return errors.New("not implemented")
  1822. }
  1823. func (s *stubAccountRepo) AutoPauseExpiredAccounts(ctx context.Context, now time.Time) (int64, error) {
  1824. return 0, errors.New("not implemented")
  1825. }
  1826. func (s *stubAccountRepo) BindGroups(ctx context.Context, accountID int64, groupIDs []int64) error {
  1827. return errors.New("not implemented")
  1828. }
  1829. func (s *stubAccountRepo) ListShadowsByParent(ctx context.Context, parentID int64) ([]*service.Account, error) {
  1830. return nil, errors.New("not implemented")
  1831. }
  1832. func (s *stubAccountRepo) ListSchedulable(ctx context.Context) ([]service.Account, error) {
  1833. return nil, errors.New("not implemented")
  1834. }
  1835. func (s *stubAccountRepo) ListSchedulableByGroupID(ctx context.Context, groupID int64) ([]service.Account, error) {
  1836. return nil, errors.New("not implemented")
  1837. }
  1838. func (s *stubAccountRepo) ListSchedulableByPlatform(ctx context.Context, platform string) ([]service.Account, error) {
  1839. return nil, errors.New("not implemented")
  1840. }
  1841. func (s *stubAccountRepo) ListSchedulableByGroupIDAndPlatform(ctx context.Context, groupID int64, platform string) ([]service.Account, error) {
  1842. return nil, errors.New("not implemented")
  1843. }
  1844. func (s *stubAccountRepo) ListSchedulableByPlatforms(ctx context.Context, platforms []string) ([]service.Account, error) {
  1845. return nil, errors.New("not implemented")
  1846. }
  1847. func (s *stubAccountRepo) ListSchedulableByGroupIDAndPlatforms(ctx context.Context, groupID int64, platforms []string) ([]service.Account, error) {
  1848. return nil, errors.New("not implemented")
  1849. }
  1850. func (s *stubAccountRepo) ListSchedulableUngroupedByPlatform(ctx context.Context, platform string) ([]service.Account, error) {
  1851. return nil, errors.New("not implemented")
  1852. }
  1853. func (s *stubAccountRepo) ListSchedulableUngroupedByPlatforms(ctx context.Context, platforms []string) ([]service.Account, error) {
  1854. return nil, errors.New("not implemented")
  1855. }
  1856. func (s *stubAccountRepo) ListModelAvailabilityCandidates(ctx context.Context, groupID *int64, platforms []string, includeGrouped bool) ([]service.Account, error) {
  1857. return nil, errors.New("not implemented")
  1858. }
  1859. func (s *stubAccountRepo) SetRateLimited(ctx context.Context, id int64, resetAt time.Time) error {
  1860. return errors.New("not implemented")
  1861. }
  1862. func (s *stubAccountRepo) SetModelRateLimit(ctx context.Context, id int64, scope string, resetAt time.Time, reason ...string) error {
  1863. return errors.New("not implemented")
  1864. }
  1865. func (s *stubAccountRepo) SetOverloaded(ctx context.Context, id int64, until time.Time) error {
  1866. return errors.New("not implemented")
  1867. }
  1868. func (s *stubAccountRepo) SetTempUnschedulable(ctx context.Context, id int64, until time.Time, reason string) error {
  1869. return errors.New("not implemented")
  1870. }
  1871. func (s *stubAccountRepo) ClearTempUnschedulable(ctx context.Context, id int64) error {
  1872. return errors.New("not implemented")
  1873. }
  1874. func (s *stubAccountRepo) ClearRateLimit(ctx context.Context, id int64) error {
  1875. return errors.New("not implemented")
  1876. }
  1877. func (s *stubAccountRepo) ClearAntigravityQuotaScopes(ctx context.Context, id int64) error {
  1878. return errors.New("not implemented")
  1879. }
  1880. func (s *stubAccountRepo) ClearModelRateLimits(ctx context.Context, id int64) error {
  1881. return errors.New("not implemented")
  1882. }
  1883. func (s *stubAccountRepo) UpdateSessionWindow(ctx context.Context, id int64, start, end *time.Time, status string) error {
  1884. return errors.New("not implemented")
  1885. }
  1886. func (s *stubAccountRepo) UpdateSessionWindowEnd(ctx context.Context, id int64, end time.Time) error {
  1887. return errors.New("not implemented")
  1888. }
  1889. func (s *stubAccountRepo) UpdateExtra(ctx context.Context, id int64, updates map[string]any) error {
  1890. return errors.New("not implemented")
  1891. }
  1892. func (s *stubAccountRepo) IncrementQuotaUsed(ctx context.Context, id int64, amount float64) error {
  1893. return errors.New("not implemented")
  1894. }
  1895. func (s *stubAccountRepo) ResetQuotaUsedAndClearRateLimitCooldown(ctx context.Context, id int64) error {
  1896. return errors.New("not implemented")
  1897. }
  1898. func (s *stubAccountRepo) BulkUpdate(ctx context.Context, ids []int64, updates service.AccountBulkUpdate) (int64, error) {
  1899. s.bulkUpdateIDs = append([]int64{}, ids...)
  1900. return int64(len(ids)), nil
  1901. }
  1902. func (s *stubAccountRepo) ListCRSAccountIDs(ctx context.Context) (map[string]int64, error) {
  1903. return nil, errors.New("not implemented")
  1904. }
  1905. func (s *stubAccountRepo) RevertProxyFallback(ctx context.Context, accountID int64) error {
  1906. return nil
  1907. }
  1908. type stubProxyRepo struct{}
  1909. func (stubProxyRepo) Create(ctx context.Context, proxy *service.Proxy) error {
  1910. return errors.New("not implemented")
  1911. }
  1912. func (stubProxyRepo) GetByID(ctx context.Context, id int64) (*service.Proxy, error) {
  1913. return nil, service.ErrProxyNotFound
  1914. }
  1915. func (stubProxyRepo) ListByIDs(ctx context.Context, ids []int64) ([]service.Proxy, error) {
  1916. return nil, errors.New("not implemented")
  1917. }
  1918. func (stubProxyRepo) Update(ctx context.Context, proxy *service.Proxy) error {
  1919. return errors.New("not implemented")
  1920. }
  1921. func (stubProxyRepo) Delete(ctx context.Context, id int64) error {
  1922. return errors.New("not implemented")
  1923. }
  1924. func (stubProxyRepo) List(ctx context.Context, params pagination.PaginationParams) ([]service.Proxy, *pagination.PaginationResult, error) {
  1925. return nil, nil, errors.New("not implemented")
  1926. }
  1927. func (stubProxyRepo) ListWithFilters(ctx context.Context, params pagination.PaginationParams, protocol, status, search string) ([]service.Proxy, *pagination.PaginationResult, error) {
  1928. return nil, nil, errors.New("not implemented")
  1929. }
  1930. func (stubProxyRepo) ListWithFiltersAndAccountCount(ctx context.Context, params pagination.PaginationParams, protocol, status, search string) ([]service.ProxyWithAccountCount, *pagination.PaginationResult, error) {
  1931. return nil, nil, errors.New("not implemented")
  1932. }
  1933. func (stubProxyRepo) ListActive(ctx context.Context) ([]service.Proxy, error) {
  1934. return nil, errors.New("not implemented")
  1935. }
  1936. func (stubProxyRepo) ListActiveWithAccountCount(ctx context.Context) ([]service.ProxyWithAccountCount, error) {
  1937. return nil, errors.New("not implemented")
  1938. }
  1939. func (stubProxyRepo) ExistsByHostPortAuth(ctx context.Context, host string, port int, username, password string) (bool, error) {
  1940. return false, errors.New("not implemented")
  1941. }
  1942. func (stubProxyRepo) CountAccountsByProxyID(ctx context.Context, proxyID int64) (int64, error) {
  1943. return 0, errors.New("not implemented")
  1944. }
  1945. func (stubProxyRepo) ListAccountSummariesByProxyID(ctx context.Context, proxyID int64) ([]service.ProxyAccountSummary, error) {
  1946. return nil, errors.New("not implemented")
  1947. }
  1948. func (stubProxyRepo) SweepExpiredProxies(ctx context.Context, now time.Time) (int64, error) {
  1949. return 0, nil
  1950. }
  1951. func (stubProxyRepo) ListAllForFallback(ctx context.Context) ([]service.Proxy, error) {
  1952. return nil, nil
  1953. }
  1954. func (stubProxyRepo) CountExpired(ctx context.Context) (int64, error) {
  1955. return 0, nil
  1956. }
  1957. func (stubProxyRepo) CountExpiringSoon(ctx context.Context, now time.Time) (int64, error) {
  1958. return 0, nil
  1959. }
  1960. type stubRedeemCodeRepo struct {
  1961. byUser map[int64][]service.RedeemCode
  1962. }
  1963. func (r *stubRedeemCodeRepo) SetByUser(userID int64, codes []service.RedeemCode) {
  1964. if r.byUser == nil {
  1965. r.byUser = make(map[int64][]service.RedeemCode)
  1966. }
  1967. r.byUser[userID] = append([]service.RedeemCode(nil), codes...)
  1968. }
  1969. func (stubRedeemCodeRepo) Create(ctx context.Context, code *service.RedeemCode) error {
  1970. return errors.New("not implemented")
  1971. }
  1972. func (stubRedeemCodeRepo) CreateBatch(ctx context.Context, codes []service.RedeemCode) error {
  1973. return errors.New("not implemented")
  1974. }
  1975. func (stubRedeemCodeRepo) GetByID(ctx context.Context, id int64) (*service.RedeemCode, error) {
  1976. return nil, service.ErrRedeemCodeNotFound
  1977. }
  1978. func (stubRedeemCodeRepo) GetByCode(ctx context.Context, code string) (*service.RedeemCode, error) {
  1979. return nil, service.ErrRedeemCodeNotFound
  1980. }
  1981. func (stubRedeemCodeRepo) Update(ctx context.Context, code *service.RedeemCode) error {
  1982. return errors.New("not implemented")
  1983. }
  1984. func (stubRedeemCodeRepo) BatchUpdate(ctx context.Context, ids []int64, fields service.RedeemCodeBatchUpdateFields) (int64, error) {
  1985. return int64(len(ids)), nil
  1986. }
  1987. func (stubRedeemCodeRepo) Delete(ctx context.Context, id int64) error {
  1988. return errors.New("not implemented")
  1989. }
  1990. func (stubRedeemCodeRepo) Use(ctx context.Context, id, userID int64) error {
  1991. return errors.New("not implemented")
  1992. }
  1993. func (stubRedeemCodeRepo) List(ctx context.Context, params pagination.PaginationParams) ([]service.RedeemCode, *pagination.PaginationResult, error) {
  1994. return nil, nil, errors.New("not implemented")
  1995. }
  1996. func (stubRedeemCodeRepo) ListWithFilters(ctx context.Context, params pagination.PaginationParams, codeType, status, search string) ([]service.RedeemCode, *pagination.PaginationResult, error) {
  1997. return nil, nil, errors.New("not implemented")
  1998. }
  1999. func (r *stubRedeemCodeRepo) ListByUser(ctx context.Context, userID int64, limit int) ([]service.RedeemCode, error) {
  2000. if r.byUser == nil {
  2001. return nil, nil
  2002. }
  2003. codes := r.byUser[userID]
  2004. if limit > 0 && len(codes) > limit {
  2005. codes = codes[:limit]
  2006. }
  2007. return append([]service.RedeemCode(nil), codes...), nil
  2008. }
  2009. func (stubRedeemCodeRepo) ListByUserPaginated(ctx context.Context, userID int64, params pagination.PaginationParams, codeType string) ([]service.RedeemCode, *pagination.PaginationResult, error) {
  2010. return nil, nil, errors.New("not implemented")
  2011. }
  2012. func (stubRedeemCodeRepo) SumPositiveBalanceByUser(ctx context.Context, userID int64) (float64, error) {
  2013. return 0, errors.New("not implemented")
  2014. }
  2015. type stubUserSubscriptionRepo struct {
  2016. byUser map[int64][]service.UserSubscription
  2017. activeByUser map[int64][]service.UserSubscription
  2018. }
  2019. func (r *stubUserSubscriptionRepo) SetByUserID(userID int64, subs []service.UserSubscription) {
  2020. if r.byUser == nil {
  2021. r.byUser = make(map[int64][]service.UserSubscription)
  2022. }
  2023. r.byUser[userID] = append([]service.UserSubscription(nil), subs...)
  2024. }
  2025. func (r *stubUserSubscriptionRepo) SetActiveByUserID(userID int64, subs []service.UserSubscription) {
  2026. if r.activeByUser == nil {
  2027. r.activeByUser = make(map[int64][]service.UserSubscription)
  2028. }
  2029. r.activeByUser[userID] = append([]service.UserSubscription(nil), subs...)
  2030. }
  2031. func (stubUserSubscriptionRepo) Create(ctx context.Context, sub *service.UserSubscription) error {
  2032. return errors.New("not implemented")
  2033. }
  2034. func (stubUserSubscriptionRepo) GetByID(ctx context.Context, id int64) (*service.UserSubscription, error) {
  2035. return nil, errors.New("not implemented")
  2036. }
  2037. func (stubUserSubscriptionRepo) GetByIDForUpdate(ctx context.Context, id int64) (*service.UserSubscription, error) {
  2038. return nil, errors.New("not implemented")
  2039. }
  2040. func (stubUserSubscriptionRepo) GetByIDIncludeDeleted(ctx context.Context, id int64) (*service.UserSubscription, error) {
  2041. return nil, errors.New("not implemented")
  2042. }
  2043. func (stubUserSubscriptionRepo) GetByUserIDAndGroupID(ctx context.Context, userID, groupID int64) (*service.UserSubscription, error) {
  2044. return nil, errors.New("not implemented")
  2045. }
  2046. func (stubUserSubscriptionRepo) GetActiveByUserIDAndGroupID(ctx context.Context, userID, groupID int64) (*service.UserSubscription, error) {
  2047. return nil, errors.New("not implemented")
  2048. }
  2049. func (stubUserSubscriptionRepo) Update(ctx context.Context, sub *service.UserSubscription) error {
  2050. return errors.New("not implemented")
  2051. }
  2052. func (stubUserSubscriptionRepo) Delete(ctx context.Context, id int64) error {
  2053. return errors.New("not implemented")
  2054. }
  2055. func (stubUserSubscriptionRepo) Restore(ctx context.Context, subscriptionID int64, restoredStatus string) (*service.UserSubscription, error) {
  2056. return nil, errors.New("not implemented")
  2057. }
  2058. func (r *stubUserSubscriptionRepo) ListByUserID(ctx context.Context, userID int64) ([]service.UserSubscription, error) {
  2059. if r.byUser == nil {
  2060. return nil, nil
  2061. }
  2062. return append([]service.UserSubscription(nil), r.byUser[userID]...), nil
  2063. }
  2064. func (r *stubUserSubscriptionRepo) ListActiveByUserID(ctx context.Context, userID int64) ([]service.UserSubscription, error) {
  2065. if r.activeByUser == nil {
  2066. return nil, nil
  2067. }
  2068. return append([]service.UserSubscription(nil), r.activeByUser[userID]...), nil
  2069. }
  2070. func (stubUserSubscriptionRepo) ListByGroupID(ctx context.Context, groupID int64, params pagination.PaginationParams) ([]service.UserSubscription, *pagination.PaginationResult, error) {
  2071. return nil, nil, errors.New("not implemented")
  2072. }
  2073. func (stubUserSubscriptionRepo) List(ctx context.Context, params pagination.PaginationParams, userID, groupID *int64, status, platform, sortBy, sortOrder string) ([]service.UserSubscription, *pagination.PaginationResult, error) {
  2074. return nil, nil, errors.New("not implemented")
  2075. }
  2076. func (stubUserSubscriptionRepo) ExistsByUserIDAndGroupID(ctx context.Context, userID, groupID int64) (bool, error) {
  2077. return false, errors.New("not implemented")
  2078. }
  2079. func (stubUserSubscriptionRepo) ExistsActiveByUserIDAndGroupID(ctx context.Context, userID, groupID int64) (bool, error) {
  2080. return false, errors.New("not implemented")
  2081. }
  2082. func (stubUserSubscriptionRepo) ExtendExpiry(ctx context.Context, subscriptionID int64, newExpiresAt time.Time) error {
  2083. return errors.New("not implemented")
  2084. }
  2085. func (stubUserSubscriptionRepo) UpdateStatus(ctx context.Context, subscriptionID int64, status string) error {
  2086. return errors.New("not implemented")
  2087. }
  2088. func (stubUserSubscriptionRepo) UpdateNotes(ctx context.Context, subscriptionID int64, notes string) error {
  2089. return errors.New("not implemented")
  2090. }
  2091. func (stubUserSubscriptionRepo) ActivateWindows(ctx context.Context, id int64, dailyStart, periodicStart time.Time) error {
  2092. return errors.New("not implemented")
  2093. }
  2094. func (stubUserSubscriptionRepo) ResetUsageWindows(ctx context.Context, id int64, resetDaily, resetWeekly, resetMonthly bool, dailyStart, periodicStart time.Time) error {
  2095. return errors.New("not implemented")
  2096. }
  2097. func (stubUserSubscriptionRepo) ResetDailyUsage(ctx context.Context, id int64, expectedWindowStart *time.Time, newWindowStart time.Time) error {
  2098. return errors.New("not implemented")
  2099. }
  2100. func (stubUserSubscriptionRepo) ResetWeeklyUsage(ctx context.Context, id int64, expectedWindowStart *time.Time, newWindowStart time.Time) error {
  2101. return errors.New("not implemented")
  2102. }
  2103. func (stubUserSubscriptionRepo) ResetMonthlyUsage(ctx context.Context, id int64, expectedWindowStart *time.Time, newWindowStart time.Time) error {
  2104. return errors.New("not implemented")
  2105. }
  2106. func (stubUserSubscriptionRepo) IncrementUsage(ctx context.Context, id int64, costUSD float64) error {
  2107. return errors.New("not implemented")
  2108. }
  2109. func (stubUserSubscriptionRepo) BatchUpdateExpiredStatus(ctx context.Context) (int64, error) {
  2110. return 0, errors.New("not implemented")
  2111. }
  2112. type stubApiKeyRepo struct {
  2113. now time.Time
  2114. nextID int64
  2115. byID map[int64]*service.APIKey
  2116. byKey map[string]*service.APIKey
  2117. }
  2118. func newStubApiKeyRepo(now time.Time) *stubApiKeyRepo {
  2119. return &stubApiKeyRepo{
  2120. now: now,
  2121. nextID: 100,
  2122. byID: make(map[int64]*service.APIKey),
  2123. byKey: make(map[string]*service.APIKey),
  2124. }
  2125. }
  2126. func (r *stubApiKeyRepo) MustSeed(key *service.APIKey) {
  2127. if key == nil {
  2128. return
  2129. }
  2130. clone := *key
  2131. r.byID[clone.ID] = &clone
  2132. r.byKey[clone.Key] = &clone
  2133. }
  2134. func (r *stubApiKeyRepo) Create(ctx context.Context, key *service.APIKey) error {
  2135. if key == nil {
  2136. return errors.New("nil key")
  2137. }
  2138. if key.ID == 0 {
  2139. key.ID = r.nextID
  2140. r.nextID++
  2141. }
  2142. if key.CreatedAt.IsZero() {
  2143. key.CreatedAt = r.now
  2144. }
  2145. if key.UpdatedAt.IsZero() {
  2146. key.UpdatedAt = r.now
  2147. }
  2148. clone := *key
  2149. r.byID[clone.ID] = &clone
  2150. r.byKey[clone.Key] = &clone
  2151. return nil
  2152. }
  2153. func (r *stubApiKeyRepo) GetByID(ctx context.Context, id int64) (*service.APIKey, error) {
  2154. key, ok := r.byID[id]
  2155. if !ok {
  2156. return nil, service.ErrAPIKeyNotFound
  2157. }
  2158. clone := *key
  2159. return &clone, nil
  2160. }
  2161. func (r *stubApiKeyRepo) GetKeyAndOwnerID(ctx context.Context, id int64) (string, int64, error) {
  2162. key, ok := r.byID[id]
  2163. if !ok {
  2164. return "", 0, service.ErrAPIKeyNotFound
  2165. }
  2166. return key.Key, key.UserID, nil
  2167. }
  2168. func (r *stubApiKeyRepo) GetByKey(ctx context.Context, key string) (*service.APIKey, error) {
  2169. found, ok := r.byKey[key]
  2170. if !ok {
  2171. return nil, service.ErrAPIKeyNotFound
  2172. }
  2173. clone := *found
  2174. return &clone, nil
  2175. }
  2176. func (r *stubApiKeyRepo) GetByKeyForAuth(ctx context.Context, key string) (*service.APIKey, error) {
  2177. return r.GetByKey(ctx, key)
  2178. }
  2179. func (r *stubApiKeyRepo) Update(ctx context.Context, key *service.APIKey, _ service.APIKeyUpdateFields) error {
  2180. if key == nil {
  2181. return errors.New("nil key")
  2182. }
  2183. if _, ok := r.byID[key.ID]; !ok {
  2184. return service.ErrAPIKeyNotFound
  2185. }
  2186. if key.UpdatedAt.IsZero() {
  2187. key.UpdatedAt = r.now
  2188. }
  2189. clone := *key
  2190. r.byID[clone.ID] = &clone
  2191. r.byKey[clone.Key] = &clone
  2192. return nil
  2193. }
  2194. func (r *stubApiKeyRepo) Delete(ctx context.Context, id int64) error {
  2195. key, ok := r.byID[id]
  2196. if !ok {
  2197. return service.ErrAPIKeyNotFound
  2198. }
  2199. delete(r.byID, id)
  2200. delete(r.byKey, key.Key)
  2201. return nil
  2202. }
  2203. func (r *stubApiKeyRepo) DeleteWithAudit(ctx context.Context, id int64) error {
  2204. return r.Delete(ctx, id)
  2205. }
  2206. func (r *stubApiKeyRepo) ListByUserID(ctx context.Context, userID int64, params pagination.PaginationParams, _ service.APIKeyListFilters) ([]service.APIKey, *pagination.PaginationResult, error) {
  2207. ids := make([]int64, 0, len(r.byID))
  2208. for id := range r.byID {
  2209. if r.byID[id].UserID == userID {
  2210. ids = append(ids, id)
  2211. }
  2212. }
  2213. sort.Slice(ids, func(i, j int) bool { return ids[i] > ids[j] })
  2214. start := params.Offset()
  2215. if start > len(ids) {
  2216. start = len(ids)
  2217. }
  2218. end := start + params.Limit()
  2219. if end > len(ids) {
  2220. end = len(ids)
  2221. }
  2222. out := make([]service.APIKey, 0, end-start)
  2223. for _, id := range ids[start:end] {
  2224. clone := *r.byID[id]
  2225. out = append(out, clone)
  2226. }
  2227. total := int64(len(ids))
  2228. pageSize := params.Limit()
  2229. pages := int(math.Ceil(float64(total) / float64(pageSize)))
  2230. if pages < 1 {
  2231. pages = 1
  2232. }
  2233. return out, &pagination.PaginationResult{
  2234. Total: total,
  2235. Page: params.Page,
  2236. PageSize: pageSize,
  2237. Pages: pages,
  2238. }, nil
  2239. }
  2240. func (r *stubApiKeyRepo) VerifyOwnership(ctx context.Context, userID int64, apiKeyIDs []int64) ([]int64, error) {
  2241. if len(apiKeyIDs) == 0 {
  2242. return []int64{}, nil
  2243. }
  2244. seen := make(map[int64]struct{}, len(apiKeyIDs))
  2245. out := make([]int64, 0, len(apiKeyIDs))
  2246. for _, id := range apiKeyIDs {
  2247. if _, ok := seen[id]; ok {
  2248. continue
  2249. }
  2250. seen[id] = struct{}{}
  2251. key, ok := r.byID[id]
  2252. if ok && key.UserID == userID {
  2253. out = append(out, id)
  2254. }
  2255. }
  2256. return out, nil
  2257. }
  2258. func (r *stubApiKeyRepo) CountByUserID(ctx context.Context, userID int64) (int64, error) {
  2259. var count int64
  2260. for _, key := range r.byID {
  2261. if key.UserID == userID {
  2262. count++
  2263. }
  2264. }
  2265. return count, nil
  2266. }
  2267. func (r *stubApiKeyRepo) ExistsByKey(ctx context.Context, key string) (bool, error) {
  2268. _, ok := r.byKey[key]
  2269. return ok, nil
  2270. }
  2271. func (r *stubApiKeyRepo) ListByGroupID(ctx context.Context, groupID int64, params pagination.PaginationParams) ([]service.APIKey, *pagination.PaginationResult, error) {
  2272. return nil, nil, errors.New("not implemented")
  2273. }
  2274. func (r *stubApiKeyRepo) SearchAPIKeys(ctx context.Context, userID int64, keyword string, limit int) ([]service.APIKey, error) {
  2275. return nil, errors.New("not implemented")
  2276. }
  2277. func (r *stubApiKeyRepo) ClearGroupIDByGroupID(ctx context.Context, groupID int64) (int64, error) {
  2278. return 0, errors.New("not implemented")
  2279. }
  2280. func (r *stubApiKeyRepo) UpdateGroupIDByUserAndGroup(ctx context.Context, userID, oldGroupID, newGroupID int64) (int64, error) {
  2281. var updated int64
  2282. for id, key := range r.byID {
  2283. if key.UserID != userID || key.GroupID == nil || *key.GroupID != oldGroupID {
  2284. continue
  2285. }
  2286. clone := *key
  2287. gid := newGroupID
  2288. clone.GroupID = &gid
  2289. r.byID[id] = &clone
  2290. r.byKey[clone.Key] = &clone
  2291. updated++
  2292. }
  2293. return updated, nil
  2294. }
  2295. func (r *stubApiKeyRepo) CountByGroupID(ctx context.Context, groupID int64) (int64, error) {
  2296. return 0, errors.New("not implemented")
  2297. }
  2298. func (r *stubApiKeyRepo) ListKeysByUserID(ctx context.Context, userID int64) ([]string, error) {
  2299. return nil, errors.New("not implemented")
  2300. }
  2301. func (r *stubApiKeyRepo) ListKeysByGroupID(ctx context.Context, groupID int64) ([]string, error) {
  2302. return nil, errors.New("not implemented")
  2303. }
  2304. func (r *stubApiKeyRepo) IncrementQuotaUsed(ctx context.Context, id int64, amount float64) (float64, error) {
  2305. return 0, errors.New("not implemented")
  2306. }
  2307. func (r *stubApiKeyRepo) UpdateLastUsed(ctx context.Context, id int64, usedAt time.Time) error {
  2308. key, ok := r.byID[id]
  2309. if !ok {
  2310. return service.ErrAPIKeyNotFound
  2311. }
  2312. ts := usedAt
  2313. key.LastUsedAt = &ts
  2314. key.UpdatedAt = usedAt
  2315. clone := *key
  2316. r.byID[id] = &clone
  2317. r.byKey[clone.Key] = &clone
  2318. return nil
  2319. }
  2320. func (r *stubApiKeyRepo) IncrementRateLimitUsage(ctx context.Context, id int64, cost float64) error {
  2321. return nil
  2322. }
  2323. func (r *stubApiKeyRepo) ResetRateLimitWindows(ctx context.Context, id int64) error {
  2324. return nil
  2325. }
  2326. func (r *stubApiKeyRepo) GetRateLimitData(ctx context.Context, id int64) (*service.APIKeyRateLimitData, error) {
  2327. return nil, nil
  2328. }
  2329. type stubUsageLogRepo struct {
  2330. userLogs map[int64][]service.UsageLog
  2331. }
  2332. func newStubUsageLogRepo() *stubUsageLogRepo {
  2333. return &stubUsageLogRepo{userLogs: make(map[int64][]service.UsageLog)}
  2334. }
  2335. func (r *stubUsageLogRepo) SetUserLogs(userID int64, logs []service.UsageLog) {
  2336. r.userLogs[userID] = logs
  2337. }
  2338. func (r *stubUsageLogRepo) Create(ctx context.Context, log *service.UsageLog) (bool, error) {
  2339. return false, errors.New("not implemented")
  2340. }
  2341. func (r *stubUsageLogRepo) GetByID(ctx context.Context, id int64) (*service.UsageLog, error) {
  2342. return nil, errors.New("not implemented")
  2343. }
  2344. func (r *stubUsageLogRepo) Delete(ctx context.Context, id int64) error {
  2345. return errors.New("not implemented")
  2346. }
  2347. func (r *stubUsageLogRepo) ListByUser(ctx context.Context, userID int64, params pagination.PaginationParams) ([]service.UsageLog, *pagination.PaginationResult, error) {
  2348. logs := r.userLogs[userID]
  2349. total := int64(len(logs))
  2350. out := paginateLogs(logs, params)
  2351. return out, paginationResult(total, params), nil
  2352. }
  2353. func (r *stubUsageLogRepo) ListByAPIKey(ctx context.Context, apiKeyID int64, params pagination.PaginationParams) ([]service.UsageLog, *pagination.PaginationResult, error) {
  2354. return nil, nil, errors.New("not implemented")
  2355. }
  2356. func (r *stubUsageLogRepo) ListByAccount(ctx context.Context, accountID int64, params pagination.PaginationParams) ([]service.UsageLog, *pagination.PaginationResult, error) {
  2357. return nil, nil, errors.New("not implemented")
  2358. }
  2359. func (r *stubUsageLogRepo) ListByUserAndTimeRange(ctx context.Context, userID int64, startTime, endTime time.Time) ([]service.UsageLog, *pagination.PaginationResult, error) {
  2360. logs := r.userLogs[userID]
  2361. return logs, paginationResult(int64(len(logs)), pagination.PaginationParams{Page: 1, PageSize: 100}), nil
  2362. }
  2363. func (r *stubUsageLogRepo) ListByAPIKeyAndTimeRange(ctx context.Context, apiKeyID int64, startTime, endTime time.Time) ([]service.UsageLog, *pagination.PaginationResult, error) {
  2364. return nil, nil, errors.New("not implemented")
  2365. }
  2366. func (r *stubUsageLogRepo) ListByAccountAndTimeRange(ctx context.Context, accountID int64, startTime, endTime time.Time) ([]service.UsageLog, *pagination.PaginationResult, error) {
  2367. return nil, nil, errors.New("not implemented")
  2368. }
  2369. func (r *stubUsageLogRepo) ListByModelAndTimeRange(ctx context.Context, modelName string, startTime, endTime time.Time) ([]service.UsageLog, *pagination.PaginationResult, error) {
  2370. return nil, nil, errors.New("not implemented")
  2371. }
  2372. func (r *stubUsageLogRepo) GetAccountWindowStats(ctx context.Context, accountID int64, startTime time.Time) (*usagestats.AccountStats, error) {
  2373. return nil, errors.New("not implemented")
  2374. }
  2375. func (r *stubUsageLogRepo) GetAccountTodayStats(ctx context.Context, accountID int64) (*usagestats.AccountStats, error) {
  2376. return nil, errors.New("not implemented")
  2377. }
  2378. func (r *stubUsageLogRepo) GetDashboardStats(ctx context.Context) (*usagestats.DashboardStats, error) {
  2379. return nil, errors.New("not implemented")
  2380. }
  2381. func (r *stubUsageLogRepo) GetUsageTrendWithFilters(ctx context.Context, startTime, endTime time.Time, granularity string, userID, apiKeyID, accountID, groupID int64, model string, requestType *int16, stream *bool, billingType *int8) ([]usagestats.TrendDataPoint, error) {
  2382. return nil, errors.New("not implemented")
  2383. }
  2384. func (r *stubUsageLogRepo) GetModelStatsWithFilters(ctx context.Context, startTime, endTime time.Time, userID, apiKeyID, accountID, groupID int64, requestType *int16, stream *bool, billingType *int8) ([]usagestats.ModelStat, error) {
  2385. return nil, errors.New("not implemented")
  2386. }
  2387. func (r *stubUsageLogRepo) GetEndpointStatsWithFilters(ctx context.Context, startTime, endTime time.Time, userID, apiKeyID, accountID, groupID int64, model string, requestType *int16, stream *bool, billingType *int8) ([]usagestats.EndpointStat, error) {
  2388. return nil, errors.New("not implemented")
  2389. }
  2390. func (r *stubUsageLogRepo) GetUpstreamEndpointStatsWithFilters(ctx context.Context, startTime, endTime time.Time, userID, apiKeyID, accountID, groupID int64, model string, requestType *int16, stream *bool, billingType *int8) ([]usagestats.EndpointStat, error) {
  2391. return nil, errors.New("not implemented")
  2392. }
  2393. func (r *stubUsageLogRepo) GetGroupStatsWithFilters(ctx context.Context, startTime, endTime time.Time, userID, apiKeyID, accountID, groupID int64, requestType *int16, stream *bool, billingType *int8) ([]usagestats.GroupStat, error) {
  2394. return nil, errors.New("not implemented")
  2395. }
  2396. func (r *stubUsageLogRepo) GetUserBreakdownStats(ctx context.Context, startTime, endTime time.Time, dim usagestats.UserBreakdownDimension, limit int) ([]usagestats.UserBreakdownItem, error) {
  2397. return nil, errors.New("not implemented")
  2398. }
  2399. func (r *stubUsageLogRepo) GetAPIKeyUsageTrend(ctx context.Context, startTime, endTime time.Time, granularity string, limit int) ([]usagestats.APIKeyUsageTrendPoint, error) {
  2400. return nil, errors.New("not implemented")
  2401. }
  2402. func (r *stubUsageLogRepo) GetUserUsageTrend(ctx context.Context, startTime, endTime time.Time, granularity string, limit int) ([]usagestats.UserUsageTrendPoint, error) {
  2403. return nil, errors.New("not implemented")
  2404. }
  2405. func (r *stubUsageLogRepo) GetUserSpendingRanking(ctx context.Context, startTime, endTime time.Time, limit int) (*usagestats.UserSpendingRankingResponse, error) {
  2406. return nil, errors.New("not implemented")
  2407. }
  2408. func (r *stubUsageLogRepo) GetUserStatsAggregated(ctx context.Context, userID int64, startTime, endTime time.Time) (*usagestats.UsageStats, error) {
  2409. logs := r.userLogs[userID]
  2410. if len(logs) == 0 {
  2411. return &usagestats.UsageStats{}, nil
  2412. }
  2413. var totalRequests int64
  2414. var totalInputTokens int64
  2415. var totalOutputTokens int64
  2416. var totalCacheTokens int64
  2417. var totalCacheCreationTokens int64
  2418. var totalCacheReadTokens int64
  2419. var totalCost float64
  2420. var totalActualCost float64
  2421. var totalDuration int64
  2422. var durationCount int64
  2423. for _, log := range logs {
  2424. totalRequests++
  2425. totalInputTokens += int64(log.InputTokens)
  2426. totalOutputTokens += int64(log.OutputTokens)
  2427. totalCacheTokens += int64(log.CacheCreationTokens + log.CacheReadTokens)
  2428. totalCacheCreationTokens += int64(log.CacheCreationTokens)
  2429. totalCacheReadTokens += int64(log.CacheReadTokens)
  2430. totalCost += log.TotalCost
  2431. totalActualCost += log.ActualCost
  2432. if log.DurationMs != nil {
  2433. totalDuration += int64(*log.DurationMs)
  2434. durationCount++
  2435. }
  2436. }
  2437. var avgDuration float64
  2438. if durationCount > 0 {
  2439. avgDuration = float64(totalDuration) / float64(durationCount)
  2440. }
  2441. return &usagestats.UsageStats{
  2442. TotalRequests: totalRequests,
  2443. TotalInputTokens: totalInputTokens,
  2444. TotalOutputTokens: totalOutputTokens,
  2445. TotalCacheTokens: totalCacheTokens,
  2446. TotalCacheCreationTokens: totalCacheCreationTokens,
  2447. TotalCacheReadTokens: totalCacheReadTokens,
  2448. TotalTokens: totalInputTokens + totalOutputTokens + totalCacheTokens,
  2449. TotalCost: totalCost,
  2450. TotalActualCost: totalActualCost,
  2451. AverageDurationMs: avgDuration,
  2452. }, nil
  2453. }
  2454. func (r *stubUsageLogRepo) GetAPIKeyStatsAggregated(ctx context.Context, apiKeyID int64, startTime, endTime time.Time) (*usagestats.UsageStats, error) {
  2455. return nil, errors.New("not implemented")
  2456. }
  2457. func (r *stubUsageLogRepo) GetAccountStatsAggregated(ctx context.Context, accountID int64, startTime, endTime time.Time) (*usagestats.UsageStats, error) {
  2458. return nil, errors.New("not implemented")
  2459. }
  2460. func (r *stubUsageLogRepo) GetModelStatsAggregated(ctx context.Context, modelName string, startTime, endTime time.Time) (*usagestats.UsageStats, error) {
  2461. return nil, errors.New("not implemented")
  2462. }
  2463. func (r *stubUsageLogRepo) GetDailyStatsAggregated(ctx context.Context, userID int64, startTime, endTime time.Time) ([]map[string]any, error) {
  2464. return nil, errors.New("not implemented")
  2465. }
  2466. func (r *stubUsageLogRepo) GetBatchUserUsageStats(ctx context.Context, userIDs []int64, startTime, endTime time.Time) (map[int64]*usagestats.BatchUserUsageStats, error) {
  2467. return nil, errors.New("not implemented")
  2468. }
  2469. func (r *stubUsageLogRepo) GetBatchAPIKeyUsageStats(ctx context.Context, apiKeyIDs []int64, startTime, endTime time.Time) (map[int64]*usagestats.BatchAPIKeyUsageStats, error) {
  2470. return nil, errors.New("not implemented")
  2471. }
  2472. func (r *stubUsageLogRepo) GetUserDashboardStats(ctx context.Context, userID int64) (*usagestats.UserDashboardStats, error) {
  2473. return nil, errors.New("not implemented")
  2474. }
  2475. func (r *stubUsageLogRepo) GetAPIKeyDashboardStats(ctx context.Context, apiKeyID int64) (*usagestats.UserDashboardStats, error) {
  2476. return nil, errors.New("not implemented")
  2477. }
  2478. func (r *stubUsageLogRepo) GetUserUsageTrendByUserID(ctx context.Context, userID int64, startTime, endTime time.Time, granularity string) ([]usagestats.TrendDataPoint, error) {
  2479. return nil, errors.New("not implemented")
  2480. }
  2481. func (r *stubUsageLogRepo) GetUserModelStats(ctx context.Context, userID int64, startTime, endTime time.Time) ([]usagestats.ModelStat, error) {
  2482. return nil, errors.New("not implemented")
  2483. }
  2484. func (r *stubUsageLogRepo) ListWithFilters(ctx context.Context, params pagination.PaginationParams, filters usagestats.UsageLogFilters) ([]service.UsageLog, *pagination.PaginationResult, error) {
  2485. logs := r.userLogs[filters.UserID]
  2486. // Apply filters
  2487. var filtered []service.UsageLog
  2488. for _, log := range logs {
  2489. // Apply APIKeyID filter
  2490. if filters.APIKeyID > 0 && log.APIKeyID != filters.APIKeyID {
  2491. continue
  2492. }
  2493. // Apply Model filter
  2494. if filters.Model != "" && stubUsageLogFilterModel(log, filters.ModelFilterSource) != filters.Model {
  2495. continue
  2496. }
  2497. // Apply Stream filter
  2498. if filters.Stream != nil && log.Stream != *filters.Stream {
  2499. continue
  2500. }
  2501. // Apply BillingType filter
  2502. if filters.BillingType != nil && log.BillingType != *filters.BillingType {
  2503. continue
  2504. }
  2505. // Apply time range filters
  2506. if filters.StartTime != nil && log.CreatedAt.Before(*filters.StartTime) {
  2507. continue
  2508. }
  2509. if filters.EndTime != nil && log.CreatedAt.After(*filters.EndTime) {
  2510. continue
  2511. }
  2512. filtered = append(filtered, log)
  2513. }
  2514. total := int64(len(filtered))
  2515. out := paginateLogs(filtered, params)
  2516. return out, paginationResult(total, params), nil
  2517. }
  2518. func stubUsageLogFilterModel(log service.UsageLog, source string) string {
  2519. if source == usagestats.ModelSourceRequested && log.RequestedModel != "" {
  2520. return log.RequestedModel
  2521. }
  2522. return log.Model
  2523. }
  2524. func (r *stubUsageLogRepo) GetGlobalStats(ctx context.Context, startTime, endTime time.Time) (*usagestats.UsageStats, error) {
  2525. return nil, errors.New("not implemented")
  2526. }
  2527. func (r *stubUsageLogRepo) GetAccountUsageStats(ctx context.Context, accountID int64, startTime, endTime time.Time) (*usagestats.AccountUsageStatsResponse, error) {
  2528. return nil, errors.New("not implemented")
  2529. }
  2530. func (r *stubUsageLogRepo) GetStatsWithFilters(ctx context.Context, filters usagestats.UsageLogFilters) (*usagestats.UsageStats, error) {
  2531. logs, _, err := r.ListWithFilters(ctx, pagination.PaginationParams{Page: 1, PageSize: 100000}, filters)
  2532. if err != nil {
  2533. return nil, err
  2534. }
  2535. var totalRequests int64
  2536. var totalInputTokens int64
  2537. var totalOutputTokens int64
  2538. var totalCacheTokens int64
  2539. var totalCacheCreationTokens int64
  2540. var totalCacheReadTokens int64
  2541. var totalCost float64
  2542. var totalActualCost float64
  2543. var totalDuration int64
  2544. var durationCount int64
  2545. for _, log := range logs {
  2546. totalRequests++
  2547. totalInputTokens += int64(log.InputTokens)
  2548. totalOutputTokens += int64(log.OutputTokens)
  2549. totalCacheTokens += int64(log.CacheCreationTokens + log.CacheReadTokens)
  2550. totalCacheCreationTokens += int64(log.CacheCreationTokens)
  2551. totalCacheReadTokens += int64(log.CacheReadTokens)
  2552. totalCost += log.TotalCost
  2553. totalActualCost += log.ActualCost
  2554. if log.DurationMs != nil {
  2555. totalDuration += int64(*log.DurationMs)
  2556. durationCount++
  2557. }
  2558. }
  2559. var avgDuration float64
  2560. if durationCount > 0 {
  2561. avgDuration = float64(totalDuration) / float64(durationCount)
  2562. }
  2563. return &usagestats.UsageStats{
  2564. TotalRequests: totalRequests,
  2565. TotalInputTokens: totalInputTokens,
  2566. TotalOutputTokens: totalOutputTokens,
  2567. TotalCacheTokens: totalCacheTokens,
  2568. TotalCacheCreationTokens: totalCacheCreationTokens,
  2569. TotalCacheReadTokens: totalCacheReadTokens,
  2570. TotalTokens: totalInputTokens + totalOutputTokens + totalCacheTokens,
  2571. TotalCost: totalCost,
  2572. TotalActualCost: totalActualCost,
  2573. AverageDurationMs: avgDuration,
  2574. Endpoints: []usagestats.EndpointStat{},
  2575. }, nil
  2576. }
  2577. func (r *stubUsageLogRepo) GetAllGroupUsageSummary(ctx context.Context, todayStart time.Time) ([]usagestats.GroupUsageSummary, error) {
  2578. return nil, errors.New("not implemented")
  2579. }
  2580. type stubSettingRepo struct {
  2581. all map[string]string
  2582. }
  2583. func newStubSettingRepo() *stubSettingRepo {
  2584. return &stubSettingRepo{all: make(map[string]string)}
  2585. }
  2586. func (r *stubSettingRepo) SetAll(values map[string]string) {
  2587. r.all = make(map[string]string, len(values))
  2588. for k, v := range values {
  2589. r.all[k] = v
  2590. }
  2591. }
  2592. func (r *stubSettingRepo) Get(ctx context.Context, key string) (*service.Setting, error) {
  2593. value, ok := r.all[key]
  2594. if !ok {
  2595. return nil, service.ErrSettingNotFound
  2596. }
  2597. return &service.Setting{Key: key, Value: value}, nil
  2598. }
  2599. func (r *stubSettingRepo) GetValue(ctx context.Context, key string) (string, error) {
  2600. value, ok := r.all[key]
  2601. if !ok {
  2602. return "", service.ErrSettingNotFound
  2603. }
  2604. return value, nil
  2605. }
  2606. func (r *stubSettingRepo) Set(ctx context.Context, key, value string) error {
  2607. r.all[key] = value
  2608. return nil
  2609. }
  2610. func (r *stubSettingRepo) GetMultiple(ctx context.Context, keys []string) (map[string]string, error) {
  2611. out := make(map[string]string, len(keys))
  2612. for _, key := range keys {
  2613. out[key] = r.all[key]
  2614. }
  2615. return out, nil
  2616. }
  2617. func (r *stubSettingRepo) SetMultiple(ctx context.Context, settings map[string]string) error {
  2618. for k, v := range settings {
  2619. r.all[k] = v
  2620. }
  2621. return nil
  2622. }
  2623. func (r *stubSettingRepo) GetAll(ctx context.Context) (map[string]string, error) {
  2624. out := make(map[string]string, len(r.all))
  2625. for k, v := range r.all {
  2626. out[k] = v
  2627. }
  2628. return out, nil
  2629. }
  2630. func (r *stubSettingRepo) Delete(ctx context.Context, key string) error {
  2631. delete(r.all, key)
  2632. return nil
  2633. }
  2634. func paginateLogs(logs []service.UsageLog, params pagination.PaginationParams) []service.UsageLog {
  2635. start := params.Offset()
  2636. if start > len(logs) {
  2637. start = len(logs)
  2638. }
  2639. end := start + params.Limit()
  2640. if end > len(logs) {
  2641. end = len(logs)
  2642. }
  2643. out := make([]service.UsageLog, 0, end-start)
  2644. out = append(out, logs[start:end]...)
  2645. return out
  2646. }
  2647. func paginationResult(total int64, params pagination.PaginationParams) *pagination.PaginationResult {
  2648. pageSize := params.Limit()
  2649. pages := int(math.Ceil(float64(total) / float64(pageSize)))
  2650. if pages < 1 {
  2651. pages = 1
  2652. }
  2653. return &pagination.PaginationResult{
  2654. Total: total,
  2655. Page: params.Page,
  2656. PageSize: pageSize,
  2657. Pages: pages,
  2658. }
  2659. }
  2660. // Ensure compile-time interface compliance.
  2661. var (
  2662. _ service.UserRepository = (*stubUserRepo)(nil)
  2663. _ service.APIKeyRepository = (*stubApiKeyRepo)(nil)
  2664. _ service.APIKeyCache = (*stubApiKeyCache)(nil)
  2665. _ service.AdminGroupRepository = (*stubGroupRepo)(nil)
  2666. _ service.UserSubscriptionRepository = (*stubUserSubscriptionRepo)(nil)
  2667. _ service.UsageLogRepository = (*stubUsageLogRepo)(nil)
  2668. _ service.SettingRepository = (*stubSettingRepo)(nil)
  2669. )