| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957 |
- //go:build unit
- package server_test
- import (
- "bytes"
- "context"
- "errors"
- "io"
- "math"
- "net/http"
- "net/http/httptest"
- "sort"
- "testing"
- "time"
- "github.com/Wei-Shaw/sub2api/internal/config"
- "github.com/Wei-Shaw/sub2api/internal/handler"
- adminhandler "github.com/Wei-Shaw/sub2api/internal/handler/admin"
- "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
- "github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
- "github.com/Wei-Shaw/sub2api/internal/server/middleware"
- "github.com/Wei-Shaw/sub2api/internal/service"
- "github.com/gin-gonic/gin"
- "github.com/stretchr/testify/require"
- )
- func TestAPIContracts(t *testing.T) {
- gin.SetMode(gin.TestMode)
- tests := []struct {
- name string
- setup func(t *testing.T, deps *contractDeps)
- method string
- path string
- body string
- headers map[string]string
- wantStatus int
- wantJSON string
- }{
- {
- name: "GET /api/v1/auth/me",
- method: http.MethodGet,
- path: "/api/v1/auth/me",
- wantStatus: http.StatusOK,
- wantJSON: `{
- "code": 0,
- "message": "success",
- "data": {
- "id": 1,
- "email": "alice@example.com",
- "email_bound": true,
- "username": "alice",
- "role": "user",
- "balance": 12.5,
- "frozen_balance": 0,
- "concurrency": 5,
- "rpm_limit": 0,
- "status": "active",
- "allowed_groups": null,
- "created_at": "2025-01-02T03:04:05Z",
- "updated_at": "2025-01-02T03:04:05Z",
- "balance_notify_enabled": false,
- "balance_notify_threshold_type": "",
- "balance_notify_threshold": null,
- "balance_notify_extra_emails": null,
- "total_recharged": 0,
- "linuxdo_bound": false,
- "oidc_bound": false,
- "wechat_bound": false,
- "dingtalk_bound": false,
- "identities": {
- "email": {
- "provider": "email",
- "provider_key": "email",
- "bound": true,
- "bound_count": 1,
- "can_bind": false,
- "can_unbind": false,
- "display_name": "alice@example.com",
- "subject_hint": "a***e@example.com",
- "note_key": "profile.authBindings.notes.emailManagedFromProfile",
- "note": "Primary account email is managed from the profile form."
- },
- "linuxdo": {
- "provider": "linuxdo",
- "bound": false,
- "bound_count": 0,
- "can_bind": true,
- "can_unbind": false,
- "bind_start_path": "/api/v1/auth/oauth/linuxdo/bind/start?intent=bind_current_user&redirect=%2Fsettings%2Fprofile"
- },
- "oidc": {
- "provider": "oidc",
- "bound": false,
- "bound_count": 0,
- "can_bind": true,
- "can_unbind": false,
- "bind_start_path": "/api/v1/auth/oauth/oidc/bind/start?intent=bind_current_user&redirect=%2Fsettings%2Fprofile"
- },
- "wechat": {
- "provider": "wechat",
- "bound": false,
- "bound_count": 0,
- "can_bind": true,
- "can_unbind": false,
- "bind_start_path": "/api/v1/auth/oauth/wechat/bind/start?intent=bind_current_user&redirect=%2Fsettings%2Fprofile"
- },
- "dingtalk": {
- "provider": "dingtalk",
- "bound": false,
- "bound_count": 0,
- "can_bind": true,
- "can_unbind": false,
- "bind_start_path": "/api/v1/auth/oauth/dingtalk/bind/start?intent=bind_current_user&redirect=%2Fsettings%2Fprofile"
- }
- },
- "identity_bindings": {
- "email": {
- "provider": "email",
- "provider_key": "email",
- "bound": true,
- "bound_count": 1,
- "can_bind": false,
- "can_unbind": false,
- "display_name": "alice@example.com",
- "subject_hint": "a***e@example.com",
- "note_key": "profile.authBindings.notes.emailManagedFromProfile",
- "note": "Primary account email is managed from the profile form."
- },
- "linuxdo": {
- "provider": "linuxdo",
- "bound": false,
- "bound_count": 0,
- "can_bind": true,
- "can_unbind": false,
- "bind_start_path": "/api/v1/auth/oauth/linuxdo/bind/start?intent=bind_current_user&redirect=%2Fsettings%2Fprofile"
- },
- "oidc": {
- "provider": "oidc",
- "bound": false,
- "bound_count": 0,
- "can_bind": true,
- "can_unbind": false,
- "bind_start_path": "/api/v1/auth/oauth/oidc/bind/start?intent=bind_current_user&redirect=%2Fsettings%2Fprofile"
- },
- "wechat": {
- "provider": "wechat",
- "bound": false,
- "bound_count": 0,
- "can_bind": true,
- "can_unbind": false,
- "bind_start_path": "/api/v1/auth/oauth/wechat/bind/start?intent=bind_current_user&redirect=%2Fsettings%2Fprofile"
- },
- "dingtalk": {
- "provider": "dingtalk",
- "bound": false,
- "bound_count": 0,
- "can_bind": true,
- "can_unbind": false,
- "bind_start_path": "/api/v1/auth/oauth/dingtalk/bind/start?intent=bind_current_user&redirect=%2Fsettings%2Fprofile"
- }
- },
- "auth_bindings": {
- "email": {
- "provider": "email",
- "provider_key": "email",
- "bound": true,
- "bound_count": 1,
- "can_bind": false,
- "can_unbind": false,
- "display_name": "alice@example.com",
- "subject_hint": "a***e@example.com",
- "note_key": "profile.authBindings.notes.emailManagedFromProfile",
- "note": "Primary account email is managed from the profile form."
- },
- "linuxdo": {
- "provider": "linuxdo",
- "bound": false,
- "bound_count": 0,
- "can_bind": true,
- "can_unbind": false,
- "bind_start_path": "/api/v1/auth/oauth/linuxdo/bind/start?intent=bind_current_user&redirect=%2Fsettings%2Fprofile"
- },
- "oidc": {
- "provider": "oidc",
- "bound": false,
- "bound_count": 0,
- "can_bind": true,
- "can_unbind": false,
- "bind_start_path": "/api/v1/auth/oauth/oidc/bind/start?intent=bind_current_user&redirect=%2Fsettings%2Fprofile"
- },
- "wechat": {
- "provider": "wechat",
- "bound": false,
- "bound_count": 0,
- "can_bind": true,
- "can_unbind": false,
- "bind_start_path": "/api/v1/auth/oauth/wechat/bind/start?intent=bind_current_user&redirect=%2Fsettings%2Fprofile"
- },
- "dingtalk": {
- "provider": "dingtalk",
- "bound": false,
- "bound_count": 0,
- "can_bind": true,
- "can_unbind": false,
- "bind_start_path": "/api/v1/auth/oauth/dingtalk/bind/start?intent=bind_current_user&redirect=%2Fsettings%2Fprofile"
- }
- },
- "run_mode": "standard"
- }
- }`,
- },
- {
- name: "POST /api/v1/keys",
- method: http.MethodPost,
- path: "/api/v1/keys",
- body: `{"name":"Key One","custom_key":"sk_custom_1234567890"}`,
- headers: map[string]string{
- "Content-Type": "application/json",
- },
- wantStatus: http.StatusOK,
- wantJSON: `{
- "code": 0,
- "message": "success",
- "data": {
- "id": 100,
- "user_id": 1,
- "key": "sk_custom_1234567890",
- "name": "Key One",
- "group_id": null,
- "status": "active",
- "ip_whitelist": null,
- "ip_blacklist": null,
- "last_used_at": null,
- "last_used_ip": null,
- "current_concurrency": 0,
- "quota": 0,
- "quota_used": 0,
- "rate_limit_5h": 0,
- "rate_limit_1d": 0,
- "rate_limit_7d": 0,
- "usage_5h": 0,
- "usage_1d": 0,
- "usage_7d": 0,
- "window_5h_start": null,
- "window_1d_start": null,
- "window_7d_start": null,
- "expires_at": null,
- "created_at": "2025-01-02T03:04:05Z",
- "updated_at": "2025-01-02T03:04:05Z"
- }
- }`,
- },
- {
- name: "GET /api/v1/keys (paginated)",
- setup: func(t *testing.T, deps *contractDeps) {
- t.Helper()
- deps.apiKeyRepo.MustSeed(&service.APIKey{
- ID: 100,
- UserID: 1,
- Key: "sk_custom_1234567890",
- Name: "Key One",
- Status: service.StatusActive,
- CreatedAt: deps.now,
- UpdatedAt: deps.now,
- })
- },
- method: http.MethodGet,
- path: "/api/v1/keys?page=1&page_size=10",
- wantStatus: http.StatusOK,
- wantJSON: `{
- "code": 0,
- "message": "success",
- "data": {
- "items": [
- {
- "id": 100,
- "user_id": 1,
- "key": "sk_custom_1234567890",
- "name": "Key One",
- "group_id": null,
- "status": "active",
- "ip_whitelist": null,
- "ip_blacklist": null,
- "last_used_at": null,
- "last_used_ip": null,
- "current_concurrency": 0,
- "quota": 0,
- "quota_used": 0,
- "rate_limit_5h": 0,
- "rate_limit_1d": 0,
- "rate_limit_7d": 0,
- "usage_5h": 0,
- "usage_1d": 0,
- "usage_7d": 0,
- "window_5h_start": null,
- "window_1d_start": null,
- "window_7d_start": null,
- "expires_at": null,
- "created_at": "2025-01-02T03:04:05Z",
- "updated_at": "2025-01-02T03:04:05Z"
- }
- ],
- "total": 1,
- "page": 1,
- "page_size": 10,
- "pages": 1
- }
- }`,
- },
- {
- name: "GET /api/v1/groups/available",
- setup: func(t *testing.T, deps *contractDeps) {
- t.Helper()
- // 普通用户可见的分组列表不应包含内部字段(如 model_routing/account_count),
- // 也不得包含利润控制配置——它与同响应的 rate_multiplier 相乘即可反推上游成本上限。
- deps.groupRepo.SetActive([]service.Group{
- {
- ID: 10,
- Name: "Group One",
- Description: "desc",
- Platform: service.PlatformAnthropic,
- RateMultiplier: 1.5,
- PeakRateMultiplier: 1.0,
- IsExclusive: false,
- Status: service.StatusActive,
- SubscriptionType: service.SubscriptionTypeStandard,
- ProfitControlEnabled: true,
- ProfitMinMargin: 0.3,
- ProfitSafetyBuffer: 0.05,
- ModelRoutingEnabled: true,
- ModelRouting: map[string][]int64{
- "claude-3-*": []int64{101, 102},
- },
- AccountCount: 2,
- CreatedAt: deps.now,
- UpdatedAt: deps.now,
- },
- })
- deps.userSubRepo.SetActiveByUserID(1, nil)
- },
- method: http.MethodGet,
- path: "/api/v1/groups/available",
- wantStatus: http.StatusOK,
- wantJSON: `{
- "code": 0,
- "message": "success",
- "data": [
- {
- "id": 10,
- "name": "Group One",
- "description": "desc",
- "platform": "anthropic",
- "rate_multiplier": 1.5,
- "peak_rate_enabled": false,
- "peak_start": "",
- "peak_end": "",
- "peak_rate_multiplier": 1,
- "is_exclusive": false,
- "status": "active",
- "subscription_type": "standard",
- "daily_limit_usd": null,
- "weekly_limit_usd": null,
- "monthly_limit_usd": null,
- "long_context_pricing_enabled": false,
- "image_price_1k": null,
- "image_price_2k": null,
- "image_price_4k": null,
- "video_price_480p": null,
- "video_price_720p": null,
- "video_price_1080p": null,
- "web_search_price_per_call": null,
- "search_price_per_1k": null,
- "audio_tts_price_per_million_chars": null,
- "audio_stt_price_per_hour": null,
- "audio_realtime_price_per_min": null,
- "allow_image_generation": false,
- "allow_batch_image_generation": false,
- "batch_image_discount_multiplier": 0,
- "batch_image_hold_multiplier": 0,
- "image_rate_independent": false,
- "image_rate_multiplier": 0,
- "video_rate_independent": false,
- "video_rate_multiplier": 0,
- "claude_code_only": false,
- "allow_messages_dispatch": false,
- "allow_live": false,
- "security_policy_enabled": false,
- "security_policy_mode": "",
- "security_policy_email_enabled": false,
- "fallback_group_id": null,
- "fallback_group_id_on_invalid_request": null,
- "require_oauth_only": false,
- "require_privacy_set": false,
- "max_reasoning_effort": "",
- "max_reasoning_effort_over_limit": "",
- "reasoning_effort_mappings": null,
- "rpm_limit": 0,
- "created_at": "2025-01-02T03:04:05Z",
- "updated_at": "2025-01-02T03:04:05Z"
- }
- ]
- }`,
- },
- {
- name: "GET /api/v1/subscriptions",
- setup: func(t *testing.T, deps *contractDeps) {
- t.Helper()
- // 普通用户订阅接口不应包含 assigned_* / notes 等管理员字段。
- deps.userSubRepo.SetByUserID(1, []service.UserSubscription{
- {
- ID: 501,
- UserID: 1,
- GroupID: 10,
- StartsAt: deps.now,
- ExpiresAt: time.Date(2099, 1, 2, 3, 4, 5, 0, time.UTC), // 使用未来日期避免 normalizeSubscriptionStatus 标记为过期
- Status: service.SubscriptionStatusActive,
- DailyUsageUSD: 1.23,
- WeeklyUsageUSD: 2.34,
- MonthlyUsageUSD: 3.45,
- AssignedBy: ptr(int64(999)),
- AssignedAt: deps.now,
- Notes: "admin-note",
- CreatedAt: deps.now,
- UpdatedAt: deps.now,
- },
- })
- },
- method: http.MethodGet,
- path: "/api/v1/subscriptions",
- wantStatus: http.StatusOK,
- wantJSON: `{
- "code": 0,
- "message": "success",
- "data": [
- {
- "id": 501,
- "user_id": 1,
- "group_id": 10,
- "starts_at": "2025-01-02T03:04:05Z",
- "expires_at": "2099-01-02T03:04:05Z",
- "status": "active",
- "daily_window_start": null,
- "weekly_window_start": null,
- "monthly_window_start": null,
- "daily_usage_usd": 1.23,
- "weekly_usage_usd": 2.34,
- "monthly_usage_usd": 3.45,
- "created_at": "2025-01-02T03:04:05Z",
- "updated_at": "2025-01-02T03:04:05Z"
- }
- ]
- }`,
- },
- {
- name: "GET /api/v1/redeem/history",
- setup: func(t *testing.T, deps *contractDeps) {
- t.Helper()
- // 普通用户兑换历史不应包含 notes 等内部字段。
- deps.redeemRepo.SetByUser(1, []service.RedeemCode{
- {
- ID: 900,
- Code: "CODE-123",
- Type: service.RedeemTypeBalance,
- Value: 1.25,
- Status: service.StatusUsed,
- UsedBy: ptr(int64(1)),
- UsedAt: ptr(deps.now),
- Notes: "internal-note",
- CreatedAt: deps.now,
- },
- })
- },
- method: http.MethodGet,
- path: "/api/v1/redeem/history",
- wantStatus: http.StatusOK,
- wantJSON: `{
- "code": 0,
- "message": "success",
- "data": [
- {
- "id": 900,
- "code": "CODE-123",
- "type": "balance",
- "value": 1.25,
- "status": "used",
- "used_by": 1,
- "used_at": "2025-01-02T03:04:05Z",
- "created_at": "2025-01-02T03:04:05Z",
- "group_id": null,
- "validity_days": 0
- }
- ]
- }`,
- },
- {
- name: "GET /api/v1/usage/stats",
- setup: func(t *testing.T, deps *contractDeps) {
- t.Helper()
- deps.usageRepo.SetUserLogs(1, []service.UsageLog{
- {
- ID: 1,
- UserID: 1,
- APIKeyID: 100,
- AccountID: 200,
- Model: "claude-3",
- InputTokens: 10,
- OutputTokens: 20,
- CacheCreationTokens: 1,
- CacheReadTokens: 2,
- TotalCost: 0.5,
- ActualCost: 0.5,
- DurationMs: ptr(100),
- CreatedAt: deps.now,
- },
- {
- ID: 2,
- UserID: 1,
- APIKeyID: 100,
- AccountID: 200,
- Model: "claude-3",
- InputTokens: 5,
- OutputTokens: 15,
- TotalCost: 0.25,
- ActualCost: 0.25,
- DurationMs: ptr(300),
- CreatedAt: deps.now,
- },
- })
- },
- method: http.MethodGet,
- path: "/api/v1/usage/stats?start_date=2025-01-01&end_date=2025-01-02",
- wantStatus: http.StatusOK,
- wantJSON: `{
- "code": 0,
- "message": "success",
- "data": {
- "total_requests": 2,
- "total_input_tokens": 15,
- "total_output_tokens": 35,
- "total_cache_tokens": 3,
- "total_cache_creation_tokens": 1,
- "total_cache_read_tokens": 2,
- "total_tokens": 53,
- "total_cost": 0.75,
- "total_actual_cost": 0.75,
- "average_duration_ms": 200
- }
- }`,
- },
- {
- name: "GET /api/v1/usage (paginated)",
- setup: func(t *testing.T, deps *contractDeps) {
- t.Helper()
- deps.usageRepo.SetUserLogs(1, []service.UsageLog{
- {
- ID: 1,
- UserID: 1,
- APIKeyID: 100,
- AccountID: 200,
- AccountRateMultiplier: ptr(0.5),
- RequestID: "req_123",
- Model: "claude-3",
- InputTokens: 10,
- OutputTokens: 20,
- CacheCreationTokens: 1,
- CacheReadTokens: 2,
- TotalCost: 0.5,
- ActualCost: 0.5,
- RateMultiplier: 1,
- BillingType: service.BillingTypeBalance,
- Stream: true,
- DurationMs: ptr(100),
- FirstTokenMs: ptr(50),
- CreatedAt: deps.now,
- },
- })
- },
- method: http.MethodGet,
- path: "/api/v1/usage?page=1&page_size=10",
- wantStatus: http.StatusOK,
- wantJSON: `{
- "code": 0,
- "message": "success",
- "data": {
- "items": [
- {
- "id": 1,
- "user_id": 1,
- "api_key_id": 100,
- "account_id": 200,
- "request_id": "req_123",
- "model": "claude-3",
- "request_type": "stream",
- "native_compaction_v2": false,
- "openai_ws_mode": false,
- "group_id": null,
- "subscription_id": null,
- "input_tokens": 10,
- "output_tokens": 20,
- "cache_creation_tokens": 1,
- "cache_read_tokens": 2,
- "cache_creation_5m_tokens": 0,
- "cache_creation_1h_tokens": 0,
- "input_cost": 0,
- "output_cost": 0,
- "cache_creation_cost": 0,
- "cache_read_cost": 0,
- "total_cost": 0.5,
- "actual_cost": 0.5,
- "rate_multiplier": 1,
- "long_context_billing_applied": false,
- "billing_type": 0,
- "stream": true,
- "duration_ms": 100,
- "first_token_ms": 50,
- "image_count": 0,
- "image_size": null,
- "image_input_size": null,
- "image_output_size": null,
- "image_input_tokens": 0,
- "image_input_cost": 0,
- "image_output_tokens": 0,
- "image_output_cost": 0,
- "image_size_source": null,
- "image_size_breakdown": null,
- "media_type": null,
- "cache_ttl_overridden": false,
- "created_at": "2025-01-02T03:04:05Z",
- "user_agent": null
- }
- ],
- "total": 1,
- "page": 1,
- "page_size": 10,
- "pages": 1
- }
- }`,
- },
- {
- name: "GET /api/v1/admin/settings",
- setup: func(t *testing.T, deps *contractDeps) {
- t.Helper()
- deps.settingRepo.SetAll(map[string]string{
- service.SettingKeyRegistrationEnabled: "true",
- service.SettingKeyEmailVerifyEnabled: "false",
- service.SettingKeyRegistrationEmailSuffixWhitelist: "[]",
- service.SettingKeyPromoCodeEnabled: "true",
- service.SettingKeySMTPHost: "smtp.example.com",
- service.SettingKeySMTPPort: "587",
- service.SettingKeySMTPUsername: "user",
- service.SettingKeySMTPPassword: "secret",
- service.SettingKeySMTPFrom: "no-reply@example.com",
- service.SettingKeySMTPFromName: "Sub2API",
- service.SettingKeySMTPUseTLS: "true",
- service.SettingKeyTurnstileEnabled: "true",
- service.SettingKeyTurnstileSiteKey: "site-key",
- service.SettingKeyTurnstileSecretKey: "secret-key",
- service.SettingKeyOIDCConnectEnabled: "false",
- service.SettingKeyOIDCConnectProviderName: "OIDC",
- service.SettingKeyOIDCConnectClientID: "",
- service.SettingKeyOIDCConnectIssuerURL: "",
- service.SettingKeyOIDCConnectDiscoveryURL: "",
- service.SettingKeyOIDCConnectAuthorizeURL: "",
- service.SettingKeyOIDCConnectTokenURL: "",
- service.SettingKeyOIDCConnectUserInfoURL: "",
- service.SettingKeyOIDCConnectJWKSURL: "",
- service.SettingKeyOIDCConnectScopes: "openid email profile",
- service.SettingKeyOIDCConnectRedirectURL: "",
- service.SettingKeyOIDCConnectFrontendRedirectURL: "/auth/oidc/callback",
- service.SettingKeyOIDCConnectTokenAuthMethod: "client_secret_post",
- service.SettingKeyOIDCConnectUsePKCE: "true",
- service.SettingKeyOIDCConnectValidateIDToken: "true",
- service.SettingKeyOIDCConnectAllowedSigningAlgs: "RS256,ES256,PS256",
- service.SettingKeyOIDCConnectClockSkewSeconds: "120",
- service.SettingKeyOIDCConnectRequireEmailVerified: "false",
- service.SettingKeyOIDCConnectUserInfoEmailPath: "",
- service.SettingKeyOIDCConnectUserInfoIDPath: "",
- service.SettingKeyOIDCConnectUserInfoUsernamePath: "",
- service.SettingKeySiteName: "Sub2API",
- service.SettingKeySiteLogo: "",
- service.SettingKeySiteSubtitle: "Subtitle",
- service.SettingKeyAPIBaseURL: "https://api.example.com",
- service.SettingKeyContactInfo: "support",
- service.SettingKeyDocURL: "https://docs.example.com",
- service.SettingKeyDefaultConcurrency: "5",
- service.SettingKeyDefaultBalance: "1.25",
- service.SettingKeyTableDefaultPageSize: "20",
- service.SettingKeyTablePageSizeOptions: "[10,20,50,100]",
- service.SettingKeyOpsMonitoringEnabled: "false",
- service.SettingKeyOpsRealtimeMonitoringEnabled: "true",
- service.SettingKeyOpsQueryModeDefault: "auto",
- service.SettingKeyOpsMetricsIntervalSeconds: "60",
- service.SettingPaymentVisibleMethodAlipaySource: service.VisibleMethodSourceEasyPayAlipay,
- service.SettingPaymentVisibleMethodWxpaySource: service.VisibleMethodSourceOfficialWechat,
- service.SettingPaymentVisibleMethodAlipayEnabled: "true",
- service.SettingPaymentVisibleMethodWxpayEnabled: "false",
- service.SettingKeyOpenAILowUpstreamRatePriorityEnabled: "true",
- service.SettingKeyOpenAIOAuthSchedulingRateMultiplier: "0.05",
- "openai_advanced_scheduler_enabled": "true",
- service.SettingKeyOpenAIAdvancedSchedulerStickyWeightedEnabled: "false",
- service.SettingKeyOpenAIAdvancedSchedulerSubscriptionPriorityEnabled: "false",
- })
- },
- method: http.MethodGet,
- path: "/api/v1/admin/settings",
- wantStatus: http.StatusOK,
- wantJSON: `{
- "code": 0,
- "message": "success",
- "data": {
- "registration_enabled": true,
- "email_verify_enabled": false,
- "registration_email_suffix_whitelist": [],
- "registration_email_domain_quota_enabled": false,
- "promo_code_enabled": true,
- "password_reset_enabled": false,
- "frontend_url": "",
- "totp_enabled": false,
- "totp_encryption_key_configured": false,
- "passkey_enabled": false,
- "passkey_configured": false,
- "passkey_rp_id": "",
- "passkey_rp_origins": [],
- "session_binding_enabled": false,
- "step_up_enabled": false,
- "audit_log_retention_days": 180,
- "login_agreement_enabled": false,
- "login_agreement_mode": "modal",
- "login_agreement_updated_at": "2026-03-31",
- "login_agreement_documents": [
- {"id": "terms", "title": "服务条款", "content_md": ""},
- {"id": "usage-policy", "title": "使用政策", "content_md": ""},
- {"id": "supported-regions", "title": "支持的国家和地区", "content_md": ""},
- {"id": "service-specific-terms", "title": "服务特定条款", "content_md": ""}
- ],
- "smtp_host": "smtp.example.com",
- "smtp_port": 587,
- "smtp_username": "user",
- "smtp_password_configured": true,
- "smtp_from_email": "no-reply@example.com",
- "smtp_from_name": "Sub2API",
- "smtp_use_tls": true,
- "turnstile_enabled": true,
- "turnstile_site_key": "site-key",
- "turnstile_secret_key_configured": true,
- "tencent_captcha_enabled": false,
- "tencent_captcha_app_id": "",
- "tencent_captcha_app_secret_key_configured": false,
- "tencent_captcha_cloud_secret_id_configured": false,
- "tencent_captcha_cloud_secret_key_configured": false,
- "tencent_captcha_region": "cn",
- "aliyun_captcha_enabled": false,
- "aliyun_captcha_access_key_id": "",
- "aliyun_captcha_access_key_secret_configured": false,
- "aliyun_captcha_scene_id": "",
- "aliyun_captcha_prefix": "",
- "aliyun_captcha_region": "cn",
- "linuxdo_connect_enabled": false,
- "linuxdo_connect_client_id": "",
- "linuxdo_connect_client_secret_configured": false,
- "linuxdo_connect_redirect_url": "",
- "dingtalk_connect_enabled": false,
- "dingtalk_connect_bypass_registration": false,
- "dingtalk_connect_client_id": "",
- "dingtalk_connect_client_secret_configured": false,
- "dingtalk_connect_redirect_url": "",
- "dingtalk_connect_internal_corp_id": "",
- "dingtalk_connect_corp_restriction_policy": "",
- "dingtalk_connect_sync_corp_email": false,
- "dingtalk_connect_sync_corp_email_attr_key": "dingtalk_email",
- "dingtalk_connect_sync_corp_email_attr_name": "钉钉企业邮箱",
- "dingtalk_connect_sync_dept": false,
- "dingtalk_connect_sync_dept_attr_key": "dingtalk_department",
- "dingtalk_connect_sync_dept_attr_name": "钉钉部门",
- "dingtalk_connect_sync_display_name": false,
- "dingtalk_connect_sync_display_name_attr_key": "dingtalk_name",
- "dingtalk_connect_sync_display_name_attr_name": "钉钉姓名",
- "oidc_connect_enabled": false,
- "oidc_connect_provider_name": "OIDC",
- "oidc_connect_client_id": "",
- "oidc_connect_client_secret_configured": false,
- "oidc_connect_issuer_url": "",
- "oidc_connect_discovery_url": "",
- "oidc_connect_authorize_url": "",
- "oidc_connect_token_url": "",
- "oidc_connect_userinfo_url": "",
- "oidc_connect_jwks_url": "",
- "oidc_connect_scopes": "openid email profile",
- "oidc_connect_redirect_url": "",
- "oidc_connect_frontend_redirect_url": "/auth/oidc/callback",
- "oidc_connect_token_auth_method": "client_secret_post",
- "oidc_connect_use_pkce": true,
- "oidc_connect_validate_id_token": true,
- "oidc_connect_allowed_signing_algs": "RS256,ES256,PS256",
- "oidc_connect_clock_skew_seconds": 120,
- "oidc_connect_require_email_verified": false,
- "oidc_connect_userinfo_email_path": "",
- "oidc_connect_userinfo_id_path": "",
- "oidc_connect_userinfo_username_path": "",
- "github_oauth_enabled": false,
- "github_oauth_client_id": "",
- "github_oauth_client_secret_configured": false,
- "github_oauth_redirect_url": "",
- "github_oauth_frontend_redirect_url": "/auth/oauth/callback",
- "google_oauth_enabled": false,
- "google_oauth_client_id": "",
- "google_oauth_client_secret_configured": false,
- "google_oauth_redirect_url": "",
- "google_oauth_frontend_redirect_url": "/auth/oauth/callback",
- "ops_monitoring_enabled": false,
- "ops_realtime_monitoring_enabled": true,
- "ops_query_mode_default": "auto",
- "ops_metrics_interval_seconds": 60,
- "site_name": "Sub2API",
- "site_logo": "",
- "site_subtitle": "Subtitle",
- "api_base_url": "https://api.example.com",
- "api_key_acl_trust_forwarded_ip": false,
- "forwarded_client_ip_headers": [],
- "contact_info": "support",
- "doc_url": "https://docs.example.com",
- "auth_source_default_email_balance": 0,
- "auth_source_default_email_concurrency": 5,
- "auth_source_default_email_subscriptions": [],
- "auth_source_default_email_grant_on_signup": false,
- "auth_source_default_email_grant_on_first_bind": false,
- "auth_source_default_github_balance": 0,
- "auth_source_default_github_concurrency": 5,
- "auth_source_default_github_subscriptions": [],
- "auth_source_default_github_grant_on_signup": false,
- "auth_source_default_github_grant_on_first_bind": false,
- "auth_source_default_google_balance": 0,
- "auth_source_default_google_concurrency": 5,
- "auth_source_default_google_subscriptions": [],
- "auth_source_default_google_grant_on_signup": false,
- "auth_source_default_google_grant_on_first_bind": false,
- "auth_source_default_linuxdo_balance": 0,
- "auth_source_default_linuxdo_concurrency": 5,
- "auth_source_default_linuxdo_subscriptions": [],
- "auth_source_default_linuxdo_grant_on_signup": false,
- "auth_source_default_linuxdo_grant_on_first_bind": false,
- "auth_source_default_oidc_balance": 0,
- "auth_source_default_oidc_concurrency": 5,
- "auth_source_default_oidc_subscriptions": [],
- "auth_source_default_oidc_grant_on_signup": false,
- "auth_source_default_oidc_grant_on_first_bind": false,
- "auth_source_default_wechat_balance": 0,
- "auth_source_default_wechat_concurrency": 5,
- "auth_source_default_wechat_subscriptions": [],
- "auth_source_default_wechat_grant_on_signup": false,
- "auth_source_default_wechat_grant_on_first_bind": false,
- "auth_source_default_dingtalk_balance": 0,
- "auth_source_default_dingtalk_concurrency": 5,
- "auth_source_default_dingtalk_subscriptions": [],
- "auth_source_default_dingtalk_grant_on_signup": false,
- "auth_source_default_dingtalk_grant_on_first_bind": false,
- "force_email_on_third_party_signup": false,
- "default_concurrency": 5,
- "default_balance": 1.25,
- "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}},
- "auth_source_default_email_platform_quotas": null,
- "auth_source_default_github_platform_quotas": null,
- "auth_source_default_google_platform_quotas": null,
- "auth_source_default_linuxdo_platform_quotas": null,
- "auth_source_default_oidc_platform_quotas": null,
- "auth_source_default_wechat_platform_quotas": null,
- "auth_source_default_dingtalk_platform_quotas": null,
- "affiliate_rebate_rate": 20,
- "affiliate_rebate_freeze_hours": 0,
- "affiliate_rebate_duration_days": 0,
- "affiliate_rebate_per_invitee_cap": 0,
- "affiliate_admin_recharge_enabled": false,
- "default_user_rpm_limit": 0,
- "default_subscriptions": [],
- "enable_model_fallback": false,
- "fallback_model_anthropic": "claude-3-5-sonnet-20241022",
- "fallback_model_antigravity": "gemini-2.5-pro",
- "fallback_model_gemini": "gemini-2.5-pro",
- "fallback_model_openai": "gpt-4o",
- "enable_identity_patch": true,
- "identity_patch_prompt": "",
- "invitation_code_enabled": false,
- "home_content": "",
- "hide_ccs_import_button": false,
- "grok_default_text_model": "grok-4.6",
- "grok_default_base_url_mode": "cli",
- "grok_cross_client_model_map_enabled": true,
- "purchase_subscription_enabled": false,
- "purchase_subscription_url": "",
- "table_default_page_size": 20,
- "table_page_size_options": [10, 20, 50, 100],
- "min_claude_code_version": "",
- "max_claude_code_version": "",
- "min_codex_version": "",
- "max_codex_version": "",
- "codex_cli_only_blacklist": "",
- "codex_cli_only_whitelist": "",
- "compact_home_enabled": false,
- "codex_cli_only_allow_app_server_clients": false,
- "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}]",
- "allow_ungrouped_key_scheduling": false,
- "backend_mode_enabled": false,
- "enable_cch_signing": false,
- "enable_claude_oauth_system_prompt_injection": true,
- "claude_oauth_system_prompt": "",
- "claude_oauth_system_prompt_blocks": "",
- "enable_anthropic_cache_ttl_1h_injection": false,
- "rewrite_message_cache_control": false,
- "enable_client_dateline_normalization": true,
- "antigravity_user_agent_version": "",
- "enable_fingerprint_unification": true,
- "enable_metadata_passthrough": false,
- "web_search_emulation_enabled": false,
- "payment_visible_method_alipay_source": "easypay_alipay",
- "payment_visible_method_wxpay_source": "official_wxpay",
- "payment_visible_method_alipay_enabled": true,
- "payment_visible_method_wxpay_enabled": false,
- "openai_low_upstream_rate_priority_enabled": true,
- "openai_oauth_scheduling_rate_multiplier": 0.05,
- "openai_ttft_mode": "semantic",
- "openai_advanced_scheduler_enabled": true,
- "openai_advanced_scheduler_sticky_weighted_enabled": false,
- "openai_advanced_scheduler_subscription_priority_enabled": false,
- "openai_advanced_scheduler_lb_top_k": "",
- "openai_advanced_scheduler_weight_priority": "",
- "openai_advanced_scheduler_weight_load": "",
- "openai_advanced_scheduler_weight_queue": "",
- "openai_advanced_scheduler_weight_error_rate": "",
- "openai_advanced_scheduler_weight_ttft": "",
- "openai_advanced_scheduler_weight_reset": "",
- "openai_advanced_scheduler_weight_quota_headroom": "",
- "openai_advanced_scheduler_weight_upstream_cost": "",
- "openai_advanced_scheduler_weight_previous_response": "",
- "openai_advanced_scheduler_weight_session_sticky": "",
- "openai_advanced_scheduler_effective_lb_top_k": "7",
- "openai_advanced_scheduler_effective_weight_priority": "1",
- "openai_advanced_scheduler_effective_weight_load": "1",
- "openai_advanced_scheduler_effective_weight_queue": "0.7",
- "openai_advanced_scheduler_effective_weight_error_rate": "0.8",
- "openai_advanced_scheduler_effective_weight_ttft": "0.5",
- "openai_advanced_scheduler_effective_weight_reset": "0",
- "openai_advanced_scheduler_effective_weight_quota_headroom": "0",
- "openai_advanced_scheduler_effective_weight_upstream_cost": "0",
- "openai_advanced_scheduler_effective_weight_previous_response": "5",
- "openai_advanced_scheduler_effective_weight_session_sticky": "3",
- "openai_codex_user_agent": "",
- "openai_codex_client_version": "",
- "openai_codex_client_version_synced": "",
- "openai_codex_version_auto_sync_enabled": true,
- "openai_fast_policy_settings": {
- "rules": []
- },
- "custom_menu_items": [],
- "custom_endpoints": [],
- "payment_enabled": false,
- "payment_min_amount": 0,
- "payment_max_amount": 0,
- "payment_daily_limit": 0,
- "payment_order_timeout_minutes": 0,
- "payment_max_pending_orders": 0,
- "payment_balance_disabled": false,
- "payment_balance_recharge_multiplier": 0,
- "payment_subscription_usd_to_cny_rate": 0,
- "payment_recharge_fee_rate": 0,
- "payment_load_balance_strategy": "",
- "payment_product_name_prefix": "",
- "payment_product_name_suffix": "",
- "payment_help_image_url": "",
- "payment_help_text": "",
- "payment_enabled_types": null,
- "payment_cancel_rate_limit_enabled": false,
- "payment_cancel_rate_limit_max": 0,
- "payment_cancel_rate_limit_window": 0,
- "payment_cancel_rate_limit_unit": "",
- "payment_cancel_rate_limit_window_mode": "",
- "payment_alipay_force_qrcode": false,
- "payment_alipay_mobile_precreate_deep_link": false,
- "balance_low_notify_enabled": false,
- "account_quota_notify_enabled": false,
- "account_scheduling_thresholds": {"anthropic":100,"grok":100,"openai":100},
- "subscription_expiry_notify_enabled": true,
- "balance_low_notify_threshold": 0,
- "balance_low_notify_recharge_url": "",
- "account_quota_notify_emails": [],
- "channel_monitor_enabled": true,
- "channel_monitor_mode": "v1",
- "channel_monitor_hide_throughput": true,
- "channel_monitor_show_quota": false,
- "channel_monitor_hide_user_ranking": false,
- "channel_monitor_default_interval_seconds": 60,
- "available_channels_enabled": false,
- "model_plaza_enabled": false,
- "model_plaza_require_auth": false,
- "model_plaza_description": "",
- "plugin_management_enabled": false,
- "risk_control_enabled": false,
- "cyber_session_block_enabled": false,
- "cyber_session_block_ttl_seconds": 3600,
- "affiliate_enabled": false,
- "wechat_connect_enabled": false,
- "wechat_connect_app_id": "",
- "wechat_connect_app_secret_configured": false,
- "wechat_connect_mode": "open",
- "wechat_connect_open_enabled": false,
- "wechat_connect_open_app_id": "",
- "wechat_connect_open_app_secret_configured": false,
- "wechat_connect_mp_enabled": false,
- "wechat_connect_mp_app_id": "",
- "wechat_connect_mp_app_secret_configured": false,
- "wechat_connect_mobile_enabled": false,
- "wechat_connect_mobile_app_id": "",
- "wechat_connect_mobile_app_secret_configured": false,
- "wechat_connect_redirect_url": "",
- "wechat_connect_frontend_redirect_url": "/auth/wechat/callback",
- "wechat_connect_scopes": "snsapi_login",
- "allow_user_view_error_requests": false
- }
- }`,
- },
- {
- name: "GET /api/v1/admin/settings falls back to config oauth defaults",
- setup: func(t *testing.T, deps *contractDeps) {
- t.Helper()
- deps.cfg.OIDC = config.OIDCConnectConfig{
- Enabled: true,
- ProviderName: "ConfigOIDC",
- ClientID: "oidc-config-client",
- ClientSecret: "oidc-config-secret",
- IssuerURL: "https://issuer.example.com",
- RedirectURL: "https://api.example.com/api/v1/auth/oauth/oidc/callback",
- FrontendRedirectURL: "/auth/oidc/callback",
- Scopes: "openid email profile",
- TokenAuthMethod: "client_secret_post",
- UsePKCE: true,
- ValidateIDToken: true,
- AllowedSigningAlgs: "RS256,ES256,PS256",
- ClockSkewSeconds: 120,
- }
- deps.cfg.WeChat = config.WeChatConnectConfig{
- Enabled: true,
- OpenEnabled: true,
- OpenAppID: "wx-open-config",
- OpenAppSecret: "wx-open-secret",
- Mode: "open",
- Scopes: "snsapi_login",
- FrontendRedirectURL: "/auth/wechat/callback",
- }
- deps.settingRepo.SetAll(map[string]string{
- service.SettingKeyRegistrationEnabled: "true",
- service.SettingKeyEmailVerifyEnabled: "false",
- service.SettingKeyRegistrationEmailSuffixWhitelist: "[]",
- })
- },
- method: http.MethodGet,
- path: "/api/v1/admin/settings",
- wantStatus: http.StatusOK,
- wantJSON: `{
- "code": 0,
- "message": "success",
- "data": {
- "registration_enabled": true,
- "email_verify_enabled": false,
- "registration_email_suffix_whitelist": [],
- "registration_email_domain_quota_enabled": false,
- "promo_code_enabled": true,
- "password_reset_enabled": false,
- "frontend_url": "",
- "invitation_code_enabled": false,
- "totp_enabled": false,
- "totp_encryption_key_configured": false,
- "passkey_enabled": false,
- "passkey_configured": false,
- "passkey_rp_id": "",
- "passkey_rp_origins": [],
- "session_binding_enabled": false,
- "step_up_enabled": false,
- "audit_log_retention_days": 180,
- "login_agreement_enabled": false,
- "login_agreement_mode": "modal",
- "login_agreement_updated_at": "2026-03-31",
- "login_agreement_documents": [
- {"id": "terms", "title": "服务条款", "content_md": ""},
- {"id": "usage-policy", "title": "使用政策", "content_md": ""},
- {"id": "supported-regions", "title": "支持的国家和地区", "content_md": ""},
- {"id": "service-specific-terms", "title": "服务特定条款", "content_md": ""}
- ],
- "smtp_host": "",
- "smtp_port": 587,
- "smtp_username": "",
- "smtp_password_configured": false,
- "smtp_from_email": "",
- "smtp_from_name": "",
- "smtp_use_tls": false,
- "turnstile_enabled": false,
- "turnstile_site_key": "",
- "turnstile_secret_key_configured": false,
- "tencent_captcha_enabled": false,
- "tencent_captcha_app_id": "",
- "tencent_captcha_app_secret_key_configured": false,
- "tencent_captcha_cloud_secret_id_configured": false,
- "tencent_captcha_cloud_secret_key_configured": false,
- "tencent_captcha_region": "cn",
- "aliyun_captcha_enabled": false,
- "aliyun_captcha_access_key_id": "",
- "aliyun_captcha_access_key_secret_configured": false,
- "aliyun_captcha_scene_id": "",
- "aliyun_captcha_prefix": "",
- "aliyun_captcha_region": "cn",
- "linuxdo_connect_enabled": false,
- "linuxdo_connect_client_id": "",
- "linuxdo_connect_client_secret_configured": false,
- "linuxdo_connect_redirect_url": "",
- "dingtalk_connect_enabled": false,
- "dingtalk_connect_bypass_registration": false,
- "dingtalk_connect_client_id": "",
- "dingtalk_connect_client_secret_configured": false,
- "dingtalk_connect_redirect_url": "",
- "dingtalk_connect_internal_corp_id": "",
- "dingtalk_connect_corp_restriction_policy": "",
- "dingtalk_connect_sync_corp_email": false,
- "dingtalk_connect_sync_corp_email_attr_key": "dingtalk_email",
- "dingtalk_connect_sync_corp_email_attr_name": "钉钉企业邮箱",
- "dingtalk_connect_sync_dept": false,
- "dingtalk_connect_sync_dept_attr_key": "dingtalk_department",
- "dingtalk_connect_sync_dept_attr_name": "钉钉部门",
- "dingtalk_connect_sync_display_name": false,
- "dingtalk_connect_sync_display_name_attr_key": "dingtalk_name",
- "dingtalk_connect_sync_display_name_attr_name": "钉钉姓名",
- "oidc_connect_enabled": true,
- "oidc_connect_provider_name": "ConfigOIDC",
- "oidc_connect_client_id": "oidc-config-client",
- "oidc_connect_client_secret_configured": true,
- "oidc_connect_issuer_url": "https://issuer.example.com",
- "oidc_connect_discovery_url": "",
- "oidc_connect_authorize_url": "",
- "oidc_connect_token_url": "",
- "oidc_connect_userinfo_url": "",
- "oidc_connect_jwks_url": "",
- "oidc_connect_scopes": "openid email profile",
- "oidc_connect_redirect_url": "https://api.example.com/api/v1/auth/oauth/oidc/callback",
- "oidc_connect_frontend_redirect_url": "/auth/oidc/callback",
- "oidc_connect_token_auth_method": "client_secret_post",
- "oidc_connect_use_pkce": true,
- "oidc_connect_validate_id_token": true,
- "oidc_connect_allowed_signing_algs": "RS256,ES256,PS256",
- "oidc_connect_clock_skew_seconds": 120,
- "oidc_connect_require_email_verified": false,
- "oidc_connect_userinfo_email_path": "",
- "oidc_connect_userinfo_id_path": "",
- "oidc_connect_userinfo_username_path": "",
- "github_oauth_enabled": false,
- "github_oauth_client_id": "",
- "github_oauth_client_secret_configured": false,
- "github_oauth_redirect_url": "",
- "github_oauth_frontend_redirect_url": "/auth/oauth/callback",
- "google_oauth_enabled": false,
- "google_oauth_client_id": "",
- "google_oauth_client_secret_configured": false,
- "google_oauth_redirect_url": "",
- "google_oauth_frontend_redirect_url": "/auth/oauth/callback",
- "site_name": "Sub2API",
- "site_logo": "",
- "site_subtitle": "Subscription to API Conversion Platform",
- "api_base_url": "",
- "api_key_acl_trust_forwarded_ip": false,
- "forwarded_client_ip_headers": [],
- "contact_info": "",
- "doc_url": "",
- "home_content": "",
- "hide_ccs_import_button": false,
- "grok_default_text_model": "grok-4.6",
- "grok_default_base_url_mode": "cli",
- "grok_cross_client_model_map_enabled": true,
- "purchase_subscription_enabled": false,
- "purchase_subscription_url": "",
- "table_default_page_size": 20,
- "table_page_size_options": [10, 20, 50],
- "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}},
- "auth_source_default_email_platform_quotas": null,
- "auth_source_default_github_platform_quotas": null,
- "auth_source_default_google_platform_quotas": null,
- "auth_source_default_linuxdo_platform_quotas": null,
- "auth_source_default_oidc_platform_quotas": null,
- "auth_source_default_wechat_platform_quotas": null,
- "auth_source_default_dingtalk_platform_quotas": null,
- "custom_menu_items": [],
- "custom_endpoints": [],
- "default_concurrency": 0,
- "default_balance": 0,
- "affiliate_rebate_rate": 20,
- "affiliate_rebate_freeze_hours": 0,
- "affiliate_rebate_duration_days": 0,
- "affiliate_rebate_per_invitee_cap": 0,
- "affiliate_admin_recharge_enabled": false,
- "default_user_rpm_limit": 0,
- "default_subscriptions": [],
- "enable_model_fallback": false,
- "fallback_model_anthropic": "claude-3-5-sonnet-20241022",
- "fallback_model_openai": "gpt-4o",
- "fallback_model_gemini": "gemini-2.5-pro",
- "fallback_model_antigravity": "gemini-2.5-pro",
- "enable_identity_patch": true,
- "identity_patch_prompt": "",
- "ops_monitoring_enabled": false,
- "ops_realtime_monitoring_enabled": true,
- "ops_query_mode_default": "auto",
- "ops_metrics_interval_seconds": 60,
- "min_claude_code_version": "",
- "max_claude_code_version": "",
- "allow_ungrouped_key_scheduling": false,
- "backend_mode_enabled": false,
- "enable_fingerprint_unification": true,
- "enable_metadata_passthrough": false,
- "enable_cch_signing": false,
- "enable_claude_oauth_system_prompt_injection": true,
- "claude_oauth_system_prompt": "",
- "claude_oauth_system_prompt_blocks": "",
- "enable_anthropic_cache_ttl_1h_injection": false,
- "rewrite_message_cache_control": false,
- "enable_client_dateline_normalization": true,
- "antigravity_user_agent_version": "",
- "min_codex_version": "",
- "max_codex_version": "",
- "codex_cli_only_blacklist": "",
- "codex_cli_only_whitelist": "",
- "compact_home_enabled": false,
- "codex_cli_only_allow_app_server_clients": false,
- "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}]",
- "web_search_emulation_enabled": false,
- "payment_visible_method_alipay_source": "",
- "payment_visible_method_wxpay_source": "",
- "payment_visible_method_alipay_enabled": false,
- "payment_visible_method_wxpay_enabled": false,
- "openai_low_upstream_rate_priority_enabled": false,
- "openai_oauth_scheduling_rate_multiplier": 1,
- "openai_ttft_mode": "semantic",
- "openai_advanced_scheduler_enabled": false,
- "openai_advanced_scheduler_sticky_weighted_enabled": false,
- "openai_advanced_scheduler_subscription_priority_enabled": false,
- "openai_advanced_scheduler_lb_top_k": "",
- "openai_advanced_scheduler_weight_priority": "",
- "openai_advanced_scheduler_weight_load": "",
- "openai_advanced_scheduler_weight_queue": "",
- "openai_advanced_scheduler_weight_error_rate": "",
- "openai_advanced_scheduler_weight_ttft": "",
- "openai_advanced_scheduler_weight_reset": "",
- "openai_advanced_scheduler_weight_quota_headroom": "",
- "openai_advanced_scheduler_weight_upstream_cost": "",
- "openai_advanced_scheduler_weight_previous_response": "",
- "openai_advanced_scheduler_weight_session_sticky": "",
- "openai_advanced_scheduler_effective_lb_top_k": "7",
- "openai_advanced_scheduler_effective_weight_priority": "1",
- "openai_advanced_scheduler_effective_weight_load": "1",
- "openai_advanced_scheduler_effective_weight_queue": "0.7",
- "openai_advanced_scheduler_effective_weight_error_rate": "0.8",
- "openai_advanced_scheduler_effective_weight_ttft": "0.5",
- "openai_advanced_scheduler_effective_weight_reset": "0",
- "openai_advanced_scheduler_effective_weight_quota_headroom": "0",
- "openai_advanced_scheduler_effective_weight_upstream_cost": "0",
- "openai_advanced_scheduler_effective_weight_previous_response": "5",
- "openai_advanced_scheduler_effective_weight_session_sticky": "3",
- "openai_codex_user_agent": "",
- "openai_codex_client_version": "",
- "openai_codex_client_version_synced": "",
- "openai_codex_version_auto_sync_enabled": true,
- "openai_fast_policy_settings": {
- "rules": []
- },
- "payment_enabled": false,
- "payment_min_amount": 0,
- "payment_max_amount": 0,
- "payment_daily_limit": 0,
- "payment_order_timeout_minutes": 0,
- "payment_max_pending_orders": 0,
- "payment_enabled_types": null,
- "payment_balance_disabled": false,
- "payment_balance_recharge_multiplier": 0,
- "payment_subscription_usd_to_cny_rate": 0,
- "payment_recharge_fee_rate": 0,
- "payment_load_balance_strategy": "",
- "payment_product_name_prefix": "",
- "payment_product_name_suffix": "",
- "payment_help_image_url": "",
- "payment_help_text": "",
- "payment_cancel_rate_limit_enabled": false,
- "payment_cancel_rate_limit_max": 0,
- "payment_cancel_rate_limit_window": 0,
- "payment_cancel_rate_limit_unit": "",
- "payment_cancel_rate_limit_window_mode": "",
- "payment_alipay_force_qrcode": false,
- "payment_alipay_mobile_precreate_deep_link": false,
- "balance_low_notify_enabled": false,
- "account_quota_notify_enabled": false,
- "account_scheduling_thresholds": {"anthropic":100,"grok":100,"openai":100},
- "subscription_expiry_notify_enabled": true,
- "balance_low_notify_threshold": 0,
- "balance_low_notify_recharge_url": "",
- "account_quota_notify_emails": [],
- "channel_monitor_enabled": true,
- "channel_monitor_mode": "v1",
- "channel_monitor_hide_throughput": true,
- "channel_monitor_show_quota": false,
- "channel_monitor_hide_user_ranking": false,
- "channel_monitor_default_interval_seconds": 60,
- "available_channels_enabled": false,
- "model_plaza_enabled": false,
- "model_plaza_require_auth": false,
- "model_plaza_description": "",
- "plugin_management_enabled": false,
- "risk_control_enabled": false,
- "cyber_session_block_enabled": false,
- "cyber_session_block_ttl_seconds": 3600,
- "affiliate_enabled": false,
- "wechat_connect_enabled": true,
- "wechat_connect_app_id": "wx-open-config",
- "wechat_connect_app_secret_configured": true,
- "wechat_connect_mode": "open",
- "wechat_connect_open_enabled": true,
- "wechat_connect_open_app_id": "wx-open-config",
- "wechat_connect_open_app_secret_configured": true,
- "wechat_connect_mp_enabled": false,
- "wechat_connect_mp_app_id": "wx-open-config",
- "wechat_connect_mp_app_secret_configured": true,
- "wechat_connect_mobile_enabled": false,
- "wechat_connect_mobile_app_id": "wx-open-config",
- "wechat_connect_mobile_app_secret_configured": true,
- "wechat_connect_redirect_url": "",
- "wechat_connect_frontend_redirect_url": "/auth/wechat/callback",
- "wechat_connect_scopes": "snsapi_login",
- "auth_source_default_email_balance": 0,
- "auth_source_default_email_concurrency": 5,
- "auth_source_default_email_subscriptions": [],
- "auth_source_default_email_grant_on_signup": false,
- "auth_source_default_email_grant_on_first_bind": false,
- "auth_source_default_github_balance": 0,
- "auth_source_default_github_concurrency": 5,
- "auth_source_default_github_subscriptions": [],
- "auth_source_default_github_grant_on_signup": false,
- "auth_source_default_github_grant_on_first_bind": false,
- "auth_source_default_google_balance": 0,
- "auth_source_default_google_concurrency": 5,
- "auth_source_default_google_subscriptions": [],
- "auth_source_default_google_grant_on_signup": false,
- "auth_source_default_google_grant_on_first_bind": false,
- "auth_source_default_linuxdo_balance": 0,
- "auth_source_default_linuxdo_concurrency": 5,
- "auth_source_default_linuxdo_subscriptions": [],
- "auth_source_default_linuxdo_grant_on_signup": false,
- "auth_source_default_linuxdo_grant_on_first_bind": false,
- "auth_source_default_oidc_balance": 0,
- "auth_source_default_oidc_concurrency": 5,
- "auth_source_default_oidc_subscriptions": [],
- "auth_source_default_oidc_grant_on_signup": false,
- "auth_source_default_oidc_grant_on_first_bind": false,
- "auth_source_default_wechat_balance": 0,
- "auth_source_default_wechat_concurrency": 5,
- "auth_source_default_wechat_subscriptions": [],
- "auth_source_default_wechat_grant_on_signup": false,
- "auth_source_default_wechat_grant_on_first_bind": false,
- "auth_source_default_dingtalk_balance": 0,
- "auth_source_default_dingtalk_concurrency": 5,
- "auth_source_default_dingtalk_subscriptions": [],
- "auth_source_default_dingtalk_grant_on_signup": false,
- "auth_source_default_dingtalk_grant_on_first_bind": false,
- "force_email_on_third_party_signup": false,
- "allow_user_view_error_requests": false
- }
- }`,
- },
- {
- name: "POST /api/v1/admin/accounts/bulk-update",
- method: http.MethodPost,
- path: "/api/v1/admin/accounts/bulk-update",
- body: `{"account_ids":[101,102],"schedulable":false}`,
- headers: map[string]string{
- "Content-Type": "application/json",
- },
- wantStatus: http.StatusOK,
- wantJSON: `{
- "code": 0,
- "message": "success",
- "data": {
- "success": 2,
- "failed": 0,
- "success_ids": [101, 102],
- "failed_ids": [],
- "results": [
- {"account_id": 101, "success": true},
- {"account_id": 102, "success": true}
- ]
- }
- }`,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- deps := newContractDeps(t)
- if tt.setup != nil {
- tt.setup(t, deps)
- }
- status, body := doRequest(t, deps.router, tt.method, tt.path, tt.body, tt.headers)
- require.Equal(t, tt.wantStatus, status)
- require.JSONEq(t, tt.wantJSON, body)
- })
- }
- }
- type contractDeps struct {
- now time.Time
- router http.Handler
- cfg *config.Config
- apiKeyRepo *stubApiKeyRepo
- groupRepo *stubGroupRepo
- userSubRepo *stubUserSubscriptionRepo
- usageRepo *stubUsageLogRepo
- settingRepo *stubSettingRepo
- redeemRepo *stubRedeemCodeRepo
- }
- func newContractDeps(t *testing.T) *contractDeps {
- t.Helper()
- now := time.Date(2025, 1, 2, 3, 4, 5, 0, time.UTC)
- userRepo := &stubUserRepo{
- users: map[int64]*service.User{
- 1: {
- ID: 1,
- Email: "alice@example.com",
- Username: "alice",
- Notes: "hello",
- Role: service.RoleUser,
- Balance: 12.5,
- Concurrency: 5,
- Status: service.StatusActive,
- AllowedGroups: nil,
- CreatedAt: now,
- UpdatedAt: now,
- },
- },
- }
- apiKeyRepo := newStubApiKeyRepo(now)
- apiKeyCache := stubApiKeyCache{}
- groupRepo := &stubGroupRepo{}
- userSubRepo := &stubUserSubscriptionRepo{}
- accountRepo := stubAccountRepo{}
- proxyRepo := stubProxyRepo{}
- redeemRepo := &stubRedeemCodeRepo{}
- cfg := &config.Config{
- Default: config.DefaultConfig{
- APIKeyPrefix: "sk-",
- },
- RunMode: config.RunModeStandard,
- }
- userService := service.NewUserService(userRepo, nil, nil, nil)
- apiKeyService := service.NewAPIKeyService(apiKeyRepo, userRepo, groupRepo, userSubRepo, nil, apiKeyCache, cfg)
- usageRepo := newStubUsageLogRepo()
- usageService := service.NewUsageService(usageRepo, userRepo, nil, nil)
- subscriptionService := service.NewSubscriptionService(groupRepo, userSubRepo, nil, nil, cfg)
- subscriptionHandler := handler.NewSubscriptionHandler(subscriptionService)
- redeemService := service.NewRedeemService(redeemRepo, userRepo, subscriptionService, nil, nil, nil, nil, nil)
- redeemHandler := handler.NewRedeemHandler(redeemService)
- settingRepo := newStubSettingRepo()
- settingService := service.NewSettingService(settingRepo, cfg)
- 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)
- authHandler := handler.NewAuthHandler(cfg, nil, userService, settingService, nil, redeemService, nil, nil)
- apiKeyHandler := handler.NewAPIKeyHandler(apiKeyService)
- usageHandler := handler.NewUsageHandler(usageService, apiKeyService, nil, nil)
- adminSettingHandler := adminhandler.NewSettingHandler(settingService, nil, nil, nil, nil, nil, nil)
- adminAccountHandler := adminhandler.NewAccountHandler(adminService, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
- jwtAuth := func(c *gin.Context) {
- c.Set(string(middleware.ContextKeyUser), middleware.AuthSubject{
- UserID: 1,
- Concurrency: 5,
- })
- c.Set(string(middleware.ContextKeyUserRole), service.RoleUser)
- c.Next()
- }
- adminAuth := func(c *gin.Context) {
- c.Set(string(middleware.ContextKeyUser), middleware.AuthSubject{
- UserID: 1,
- Concurrency: 5,
- })
- c.Set(string(middleware.ContextKeyUserRole), service.RoleAdmin)
- c.Next()
- }
- r := gin.New()
- v1 := r.Group("/api/v1")
- v1Auth := v1.Group("")
- v1Auth.Use(jwtAuth)
- v1Auth.GET("/auth/me", authHandler.GetCurrentUser)
- v1Keys := v1.Group("")
- v1Keys.Use(jwtAuth)
- v1Keys.GET("/keys", apiKeyHandler.List)
- v1Keys.POST("/keys", apiKeyHandler.Create)
- v1Keys.GET("/groups/available", apiKeyHandler.GetAvailableGroups)
- v1Usage := v1.Group("")
- v1Usage.Use(jwtAuth)
- v1Usage.GET("/usage", usageHandler.List)
- v1Usage.GET("/usage/stats", usageHandler.Stats)
- v1Subs := v1.Group("")
- v1Subs.Use(jwtAuth)
- v1Subs.GET("/subscriptions", subscriptionHandler.List)
- v1Redeem := v1.Group("")
- v1Redeem.Use(jwtAuth)
- v1Redeem.GET("/redeem/history", redeemHandler.GetHistory)
- v1Admin := v1.Group("/admin")
- v1Admin.Use(adminAuth)
- v1Admin.GET("/settings", adminSettingHandler.GetSettings)
- v1Admin.POST("/accounts/bulk-update", adminAccountHandler.BulkUpdate)
- return &contractDeps{
- now: now,
- router: r,
- cfg: cfg,
- apiKeyRepo: apiKeyRepo,
- groupRepo: groupRepo,
- userSubRepo: userSubRepo,
- usageRepo: usageRepo,
- settingRepo: settingRepo,
- redeemRepo: redeemRepo,
- }
- }
- func doRequest(t *testing.T, router http.Handler, method, path, body string, headers map[string]string) (int, string) {
- t.Helper()
- req := httptest.NewRequest(method, path, bytes.NewBufferString(body))
- for k, v := range headers {
- req.Header.Set(k, v)
- }
- w := httptest.NewRecorder()
- router.ServeHTTP(w, req)
- respBody, err := io.ReadAll(w.Result().Body)
- require.NoError(t, err)
- return w.Result().StatusCode, string(respBody)
- }
- func ptr[T any](v T) *T { return &v }
- type stubUserRepo struct {
- users map[int64]*service.User
- }
- func (r *stubUserRepo) Create(ctx context.Context, user *service.User) error {
- return errors.New("not implemented")
- }
- func (r *stubUserRepo) CreateWithEmailAliasGuard(ctx context.Context, user *service.User) error {
- return errors.New("not implemented")
- }
- func (r *stubUserRepo) GetByID(ctx context.Context, id int64) (*service.User, error) {
- user, ok := r.users[id]
- if !ok {
- return nil, service.ErrUserNotFound
- }
- clone := *user
- return &clone, nil
- }
- func (r *stubUserRepo) GetByEmail(ctx context.Context, email string) (*service.User, error) {
- for _, user := range r.users {
- if user.Email == email {
- clone := *user
- return &clone, nil
- }
- }
- return nil, service.ErrUserNotFound
- }
- func (r *stubUserRepo) GetFirstAdmin(ctx context.Context) (*service.User, error) {
- for _, user := range r.users {
- if user.Role == service.RoleAdmin && user.Status == service.StatusActive {
- clone := *user
- return &clone, nil
- }
- }
- return nil, service.ErrUserNotFound
- }
- func (r *stubUserRepo) Update(ctx context.Context, user *service.User, fields service.UserUpdateFields) error {
- return errors.New("not implemented")
- }
- func (r *stubUserRepo) Delete(ctx context.Context, id int64) error {
- return errors.New("not implemented")
- }
- func (r *stubUserRepo) GetUserAvatar(ctx context.Context, userID int64) (*service.UserAvatar, error) {
- return nil, nil
- }
- func (r *stubUserRepo) UpsertUserAvatar(ctx context.Context, userID int64, input service.UpsertUserAvatarInput) (*service.UserAvatar, error) {
- return nil, errors.New("not implemented")
- }
- func (r *stubUserRepo) DeleteUserAvatar(ctx context.Context, userID int64) error {
- return errors.New("not implemented")
- }
- func (r *stubUserRepo) List(ctx context.Context, params pagination.PaginationParams) ([]service.User, *pagination.PaginationResult, error) {
- return nil, nil, errors.New("not implemented")
- }
- func (r *stubUserRepo) ListWithFilters(ctx context.Context, params pagination.PaginationParams, filters service.UserListFilters) ([]service.User, *pagination.PaginationResult, error) {
- return nil, nil, errors.New("not implemented")
- }
- func (r *stubUserRepo) UpdateBalance(ctx context.Context, id int64, amount float64) error {
- return errors.New("not implemented")
- }
- func (r *stubUserRepo) DeductBalance(ctx context.Context, id int64, amount float64) error {
- return errors.New("not implemented")
- }
- func (r *stubUserRepo) AdjustBalance(ctx context.Context, id int64, delta float64) (service.BalanceChange, error) {
- return service.BalanceChange{}, errors.New("not implemented")
- }
- func (r *stubUserRepo) SetBalance(ctx context.Context, id int64, value float64) (service.BalanceChange, error) {
- return service.BalanceChange{}, errors.New("not implemented")
- }
- func (r *stubUserRepo) UpdateConcurrency(ctx context.Context, id int64, amount int) error {
- return errors.New("not implemented")
- }
- func (r *stubUserRepo) BatchSetConcurrency(context.Context, []int64, int) (int, error) { return 0, nil }
- func (r *stubUserRepo) BatchAddConcurrency(context.Context, []int64, int) (int, error) { return 0, nil }
- func (r *stubUserRepo) BatchUpdateLimits(context.Context, []int64, *int, *int) (int, error) {
- return 0, nil
- }
- func (r *stubUserRepo) ExistsByEmail(ctx context.Context, email string) (bool, error) {
- return false, errors.New("not implemented")
- }
- func (r *stubUserRepo) ExistsByEmailAlias(ctx context.Context, email string) (bool, error) {
- return false, errors.New("not implemented")
- }
- func (r *stubUserRepo) RemoveGroupFromAllowedGroups(ctx context.Context, groupID int64) (int64, error) {
- return 0, errors.New("not implemented")
- }
- func (r *stubUserRepo) RemoveGroupFromUserAllowedGroups(ctx context.Context, userID int64, groupID int64) error {
- return errors.New("not implemented")
- }
- func (r *stubUserRepo) AddGroupToAllowedGroups(ctx context.Context, userID int64, groupID int64) error {
- return errors.New("not implemented")
- }
- func (r *stubUserRepo) ListUserAuthIdentities(ctx context.Context, userID int64) ([]service.UserAuthIdentityRecord, error) {
- return nil, nil
- }
- func (r *stubUserRepo) UnbindUserAuthProvider(context.Context, int64, string) error {
- return errors.New("not implemented")
- }
- func (r *stubUserRepo) GetLatestUsedAtByUserIDs(ctx context.Context, userIDs []int64) (map[int64]*time.Time, error) {
- return map[int64]*time.Time{}, nil
- }
- func (r *stubUserRepo) GetLatestUsedAtByUserID(ctx context.Context, userID int64) (*time.Time, error) {
- return nil, nil
- }
- func (r *stubUserRepo) UpdateUserLastActiveAt(ctx context.Context, userID int64, activeAt time.Time) error {
- return nil
- }
- func (r *stubUserRepo) UpdateTotpSecret(ctx context.Context, userID int64, encryptedSecret *string) error {
- return errors.New("not implemented")
- }
- func (r *stubUserRepo) EnableTotp(ctx context.Context, userID int64) error {
- return errors.New("not implemented")
- }
- func (r *stubUserRepo) DisableTotp(ctx context.Context, userID int64) error {
- return errors.New("not implemented")
- }
- func (r *stubUserRepo) GetByIDIncludeDeleted(ctx context.Context, id int64) (*service.User, error) {
- panic("unexpected GetByIDIncludeDeleted call")
- }
- type stubApiKeyCache struct{}
- func (stubApiKeyCache) GetCreateAttemptCount(ctx context.Context, userID int64) (int, error) {
- return 0, nil
- }
- func (stubApiKeyCache) IncrementCreateAttemptCount(ctx context.Context, userID int64) error {
- return nil
- }
- func (stubApiKeyCache) DeleteCreateAttemptCount(ctx context.Context, userID int64) error {
- return nil
- }
- func (stubApiKeyCache) IncrementDailyUsage(ctx context.Context, apiKey string) error {
- return nil
- }
- func (stubApiKeyCache) SetDailyUsageExpiry(ctx context.Context, apiKey string, ttl time.Duration) error {
- return nil
- }
- func (stubApiKeyCache) GetAuthCache(ctx context.Context, key string) (*service.APIKeyAuthCacheEntry, error) {
- return nil, nil
- }
- func (stubApiKeyCache) SetAuthCache(ctx context.Context, key string, entry *service.APIKeyAuthCacheEntry, ttl time.Duration) error {
- return nil
- }
- func (stubApiKeyCache) DeleteAuthCache(ctx context.Context, key string) error {
- return nil
- }
- func (stubApiKeyCache) PublishAuthCacheInvalidation(ctx context.Context, cacheKey string) error {
- return nil
- }
- func (stubApiKeyCache) SubscribeAuthCacheInvalidation(ctx context.Context, handler func(cacheKey string)) error {
- return nil
- }
- type stubGroupRepo struct {
- active []service.Group
- }
- func (r *stubGroupRepo) SetActive(groups []service.Group) {
- r.active = append([]service.Group(nil), groups...)
- }
- func (stubGroupRepo) Create(ctx context.Context, group *service.Group) error {
- return errors.New("not implemented")
- }
- func (stubGroupRepo) GetByID(ctx context.Context, id int64) (*service.Group, error) {
- return nil, service.ErrGroupNotFound
- }
- func (stubGroupRepo) GetByIDLite(ctx context.Context, id int64) (*service.Group, error) {
- return nil, service.ErrGroupNotFound
- }
- func (stubGroupRepo) Update(ctx context.Context, group *service.Group) error {
- return errors.New("not implemented")
- }
- func (stubGroupRepo) Delete(ctx context.Context, id int64) error {
- return errors.New("not implemented")
- }
- func (stubGroupRepo) DeleteCascade(ctx context.Context, id int64) ([]int64, error) {
- return nil, errors.New("not implemented")
- }
- func (stubGroupRepo) DeleteCascadeIfEmpty(ctx context.Context, id int64) ([]int64, error) {
- return nil, errors.New("not implemented")
- }
- func (stubGroupRepo) List(ctx context.Context, params pagination.PaginationParams) ([]service.Group, *pagination.PaginationResult, error) {
- return nil, nil, errors.New("not implemented")
- }
- func (stubGroupRepo) ListWithFilters(ctx context.Context, params pagination.PaginationParams, platform, status, search string, isExclusive *bool) ([]service.Group, *pagination.PaginationResult, error) {
- return nil, nil, errors.New("not implemented")
- }
- func (r *stubGroupRepo) ListActive(ctx context.Context) ([]service.Group, error) {
- return append([]service.Group(nil), r.active...), nil
- }
- func (r *stubGroupRepo) ListActiveByPlatform(ctx context.Context, platform string) ([]service.Group, error) {
- out := make([]service.Group, 0, len(r.active))
- for i := range r.active {
- g := r.active[i]
- if g.Platform == platform {
- out = append(out, g)
- }
- }
- return out, nil
- }
- func (stubGroupRepo) ExistsByName(ctx context.Context, name string) (bool, error) {
- return false, errors.New("not implemented")
- }
- func (stubGroupRepo) GetAccountCount(ctx context.Context, groupID int64) (int64, int64, error) {
- return 0, 0, errors.New("not implemented")
- }
- func (stubGroupRepo) DeleteAccountGroupsByGroupID(ctx context.Context, groupID int64) (int64, error) {
- return 0, errors.New("not implemented")
- }
- func (stubGroupRepo) BindAccountsToGroup(ctx context.Context, groupID int64, accountIDs []int64) error {
- return errors.New("not implemented")
- }
- func (stubGroupRepo) GetAccountIDsByGroupIDs(ctx context.Context, groupIDs []int64) ([]int64, error) {
- return nil, errors.New("not implemented")
- }
- func (stubGroupRepo) UpdateSortOrders(ctx context.Context, updates []service.GroupSortOrderUpdate) error {
- return nil
- }
- func (stubGroupRepo) FindByDuplicateOperationID(ctx context.Context, operationID string) (*service.Group, error) {
- return nil, nil
- }
- func (stubGroupRepo) CreateFromSource(ctx context.Context, group *service.Group, sourceGroupID int64) error {
- return errors.New("not implemented")
- }
- type stubAccountRepo struct {
- bulkUpdateIDs []int64
- }
- func (s *stubAccountRepo) Create(ctx context.Context, account *service.Account) error {
- return errors.New("not implemented")
- }
- func (s *stubAccountRepo) CreateWithAccountGroups(ctx context.Context, account *service.Account, groups []service.AccountGroup) error {
- return errors.New("not implemented")
- }
- func (s *stubAccountRepo) GetByID(ctx context.Context, id int64) (*service.Account, error) {
- return nil, service.ErrAccountNotFound
- }
- func (s *stubAccountRepo) GetByIDs(ctx context.Context, ids []int64) ([]*service.Account, error) {
- return nil, errors.New("not implemented")
- }
- func (s *stubAccountRepo) ExistsByID(ctx context.Context, id int64) (bool, error) {
- return false, errors.New("not implemented")
- }
- func (s *stubAccountRepo) GetByCRSAccountID(ctx context.Context, crsAccountID string) (*service.Account, error) {
- return nil, errors.New("not implemented")
- }
- func (s *stubAccountRepo) FindByExtraField(ctx context.Context, key string, value any) ([]service.Account, error) {
- return nil, errors.New("not implemented")
- }
- func (s *stubAccountRepo) Update(ctx context.Context, account *service.Account) error {
- return errors.New("not implemented")
- }
- func (s *stubAccountRepo) UpdateWithAccountBillingSettings(
- ctx context.Context,
- account *service.Account,
- probeEnabled *bool,
- rateSyncEnabled *bool,
- rateMultiplier *float64,
- ) error {
- return errors.New("not implemented")
- }
- func (s *stubAccountRepo) Delete(ctx context.Context, id int64) error {
- return errors.New("not implemented")
- }
- func (s *stubAccountRepo) List(ctx context.Context, params pagination.PaginationParams) ([]service.Account, *pagination.PaginationResult, error) {
- return nil, nil, errors.New("not implemented")
- }
- func (s *stubAccountRepo) ListAllWithFilters(context.Context, string, string, string, string, int64, string) ([]service.Account, error) {
- return nil, nil
- }
- func (s *stubAccountRepo) ListWithFilters(ctx context.Context, params pagination.PaginationParams, platform, accountType, status, search string, groupID int64, privacyMode string) ([]service.Account, *pagination.PaginationResult, error) {
- return nil, nil, errors.New("not implemented")
- }
- func (s *stubAccountRepo) ListByGroup(ctx context.Context, groupID int64) ([]service.Account, error) {
- return nil, errors.New("not implemented")
- }
- func (s *stubAccountRepo) ListActive(ctx context.Context) ([]service.Account, error) {
- return nil, errors.New("not implemented")
- }
- func (s *stubAccountRepo) ListOAuthRefreshCandidates(ctx context.Context) ([]service.Account, error) {
- return nil, errors.New("not implemented")
- }
- func (s *stubAccountRepo) ListByPlatform(ctx context.Context, platform string) ([]service.Account, error) {
- return nil, errors.New("not implemented")
- }
- func (s *stubAccountRepo) UpdateLastUsed(ctx context.Context, id int64) error {
- return errors.New("not implemented")
- }
- func (s *stubAccountRepo) BatchUpdateLastUsed(ctx context.Context, updates map[int64]time.Time) error {
- return errors.New("not implemented")
- }
- func (s *stubAccountRepo) SetError(ctx context.Context, id int64, errorMsg string) error {
- return errors.New("not implemented")
- }
- func (s *stubAccountRepo) ClearError(ctx context.Context, id int64) error {
- return errors.New("not implemented")
- }
- func (s *stubAccountRepo) SetSchedulable(ctx context.Context, id int64, schedulable bool) error {
- return errors.New("not implemented")
- }
- func (s *stubAccountRepo) AutoPauseExpiredAccounts(ctx context.Context, now time.Time) (int64, error) {
- return 0, errors.New("not implemented")
- }
- func (s *stubAccountRepo) BindGroups(ctx context.Context, accountID int64, groupIDs []int64) error {
- return errors.New("not implemented")
- }
- func (s *stubAccountRepo) ListShadowsByParent(ctx context.Context, parentID int64) ([]*service.Account, error) {
- return nil, errors.New("not implemented")
- }
- func (s *stubAccountRepo) ListSchedulable(ctx context.Context) ([]service.Account, error) {
- return nil, errors.New("not implemented")
- }
- func (s *stubAccountRepo) ListSchedulableByGroupID(ctx context.Context, groupID int64) ([]service.Account, error) {
- return nil, errors.New("not implemented")
- }
- func (s *stubAccountRepo) ListSchedulableByPlatform(ctx context.Context, platform string) ([]service.Account, error) {
- return nil, errors.New("not implemented")
- }
- func (s *stubAccountRepo) ListSchedulableByGroupIDAndPlatform(ctx context.Context, groupID int64, platform string) ([]service.Account, error) {
- return nil, errors.New("not implemented")
- }
- func (s *stubAccountRepo) ListSchedulableByPlatforms(ctx context.Context, platforms []string) ([]service.Account, error) {
- return nil, errors.New("not implemented")
- }
- func (s *stubAccountRepo) ListSchedulableByGroupIDAndPlatforms(ctx context.Context, groupID int64, platforms []string) ([]service.Account, error) {
- return nil, errors.New("not implemented")
- }
- func (s *stubAccountRepo) ListSchedulableUngroupedByPlatform(ctx context.Context, platform string) ([]service.Account, error) {
- return nil, errors.New("not implemented")
- }
- func (s *stubAccountRepo) ListSchedulableUngroupedByPlatforms(ctx context.Context, platforms []string) ([]service.Account, error) {
- return nil, errors.New("not implemented")
- }
- func (s *stubAccountRepo) ListModelAvailabilityCandidates(ctx context.Context, groupID *int64, platforms []string, includeGrouped bool) ([]service.Account, error) {
- return nil, errors.New("not implemented")
- }
- func (s *stubAccountRepo) SetRateLimited(ctx context.Context, id int64, resetAt time.Time) error {
- return errors.New("not implemented")
- }
- func (s *stubAccountRepo) SetModelRateLimit(ctx context.Context, id int64, scope string, resetAt time.Time, reason ...string) error {
- return errors.New("not implemented")
- }
- func (s *stubAccountRepo) SetOverloaded(ctx context.Context, id int64, until time.Time) error {
- return errors.New("not implemented")
- }
- func (s *stubAccountRepo) SetTempUnschedulable(ctx context.Context, id int64, until time.Time, reason string) error {
- return errors.New("not implemented")
- }
- func (s *stubAccountRepo) ClearTempUnschedulable(ctx context.Context, id int64) error {
- return errors.New("not implemented")
- }
- func (s *stubAccountRepo) ClearRateLimit(ctx context.Context, id int64) error {
- return errors.New("not implemented")
- }
- func (s *stubAccountRepo) ClearAntigravityQuotaScopes(ctx context.Context, id int64) error {
- return errors.New("not implemented")
- }
- func (s *stubAccountRepo) ClearModelRateLimits(ctx context.Context, id int64) error {
- return errors.New("not implemented")
- }
- func (s *stubAccountRepo) UpdateSessionWindow(ctx context.Context, id int64, start, end *time.Time, status string) error {
- return errors.New("not implemented")
- }
- func (s *stubAccountRepo) UpdateSessionWindowEnd(ctx context.Context, id int64, end time.Time) error {
- return errors.New("not implemented")
- }
- func (s *stubAccountRepo) UpdateExtra(ctx context.Context, id int64, updates map[string]any) error {
- return errors.New("not implemented")
- }
- func (s *stubAccountRepo) IncrementQuotaUsed(ctx context.Context, id int64, amount float64) error {
- return errors.New("not implemented")
- }
- func (s *stubAccountRepo) ResetQuotaUsedAndClearRateLimitCooldown(ctx context.Context, id int64) error {
- return errors.New("not implemented")
- }
- func (s *stubAccountRepo) BulkUpdate(ctx context.Context, ids []int64, updates service.AccountBulkUpdate) (int64, error) {
- s.bulkUpdateIDs = append([]int64{}, ids...)
- return int64(len(ids)), nil
- }
- func (s *stubAccountRepo) ListCRSAccountIDs(ctx context.Context) (map[string]int64, error) {
- return nil, errors.New("not implemented")
- }
- func (s *stubAccountRepo) RevertProxyFallback(ctx context.Context, accountID int64) error {
- return nil
- }
- type stubProxyRepo struct{}
- func (stubProxyRepo) Create(ctx context.Context, proxy *service.Proxy) error {
- return errors.New("not implemented")
- }
- func (stubProxyRepo) GetByID(ctx context.Context, id int64) (*service.Proxy, error) {
- return nil, service.ErrProxyNotFound
- }
- func (stubProxyRepo) ListByIDs(ctx context.Context, ids []int64) ([]service.Proxy, error) {
- return nil, errors.New("not implemented")
- }
- func (stubProxyRepo) Update(ctx context.Context, proxy *service.Proxy) error {
- return errors.New("not implemented")
- }
- func (stubProxyRepo) Delete(ctx context.Context, id int64) error {
- return errors.New("not implemented")
- }
- func (stubProxyRepo) List(ctx context.Context, params pagination.PaginationParams) ([]service.Proxy, *pagination.PaginationResult, error) {
- return nil, nil, errors.New("not implemented")
- }
- func (stubProxyRepo) ListWithFilters(ctx context.Context, params pagination.PaginationParams, protocol, status, search string) ([]service.Proxy, *pagination.PaginationResult, error) {
- return nil, nil, errors.New("not implemented")
- }
- func (stubProxyRepo) ListWithFiltersAndAccountCount(ctx context.Context, params pagination.PaginationParams, protocol, status, search string) ([]service.ProxyWithAccountCount, *pagination.PaginationResult, error) {
- return nil, nil, errors.New("not implemented")
- }
- func (stubProxyRepo) ListActive(ctx context.Context) ([]service.Proxy, error) {
- return nil, errors.New("not implemented")
- }
- func (stubProxyRepo) ListActiveWithAccountCount(ctx context.Context) ([]service.ProxyWithAccountCount, error) {
- return nil, errors.New("not implemented")
- }
- func (stubProxyRepo) ExistsByHostPortAuth(ctx context.Context, host string, port int, username, password string) (bool, error) {
- return false, errors.New("not implemented")
- }
- func (stubProxyRepo) CountAccountsByProxyID(ctx context.Context, proxyID int64) (int64, error) {
- return 0, errors.New("not implemented")
- }
- func (stubProxyRepo) ListAccountSummariesByProxyID(ctx context.Context, proxyID int64) ([]service.ProxyAccountSummary, error) {
- return nil, errors.New("not implemented")
- }
- func (stubProxyRepo) SweepExpiredProxies(ctx context.Context, now time.Time) (int64, error) {
- return 0, nil
- }
- func (stubProxyRepo) ListAllForFallback(ctx context.Context) ([]service.Proxy, error) {
- return nil, nil
- }
- func (stubProxyRepo) CountExpired(ctx context.Context) (int64, error) {
- return 0, nil
- }
- func (stubProxyRepo) CountExpiringSoon(ctx context.Context, now time.Time) (int64, error) {
- return 0, nil
- }
- type stubRedeemCodeRepo struct {
- byUser map[int64][]service.RedeemCode
- }
- func (r *stubRedeemCodeRepo) SetByUser(userID int64, codes []service.RedeemCode) {
- if r.byUser == nil {
- r.byUser = make(map[int64][]service.RedeemCode)
- }
- r.byUser[userID] = append([]service.RedeemCode(nil), codes...)
- }
- func (stubRedeemCodeRepo) Create(ctx context.Context, code *service.RedeemCode) error {
- return errors.New("not implemented")
- }
- func (stubRedeemCodeRepo) CreateBatch(ctx context.Context, codes []service.RedeemCode) error {
- return errors.New("not implemented")
- }
- func (stubRedeemCodeRepo) GetByID(ctx context.Context, id int64) (*service.RedeemCode, error) {
- return nil, service.ErrRedeemCodeNotFound
- }
- func (stubRedeemCodeRepo) GetByCode(ctx context.Context, code string) (*service.RedeemCode, error) {
- return nil, service.ErrRedeemCodeNotFound
- }
- func (stubRedeemCodeRepo) Update(ctx context.Context, code *service.RedeemCode) error {
- return errors.New("not implemented")
- }
- func (stubRedeemCodeRepo) BatchUpdate(ctx context.Context, ids []int64, fields service.RedeemCodeBatchUpdateFields) (int64, error) {
- return int64(len(ids)), nil
- }
- func (stubRedeemCodeRepo) Delete(ctx context.Context, id int64) error {
- return errors.New("not implemented")
- }
- func (stubRedeemCodeRepo) Use(ctx context.Context, id, userID int64) error {
- return errors.New("not implemented")
- }
- func (stubRedeemCodeRepo) List(ctx context.Context, params pagination.PaginationParams) ([]service.RedeemCode, *pagination.PaginationResult, error) {
- return nil, nil, errors.New("not implemented")
- }
- func (stubRedeemCodeRepo) ListWithFilters(ctx context.Context, params pagination.PaginationParams, codeType, status, search string) ([]service.RedeemCode, *pagination.PaginationResult, error) {
- return nil, nil, errors.New("not implemented")
- }
- func (r *stubRedeemCodeRepo) ListByUser(ctx context.Context, userID int64, limit int) ([]service.RedeemCode, error) {
- if r.byUser == nil {
- return nil, nil
- }
- codes := r.byUser[userID]
- if limit > 0 && len(codes) > limit {
- codes = codes[:limit]
- }
- return append([]service.RedeemCode(nil), codes...), nil
- }
- func (stubRedeemCodeRepo) ListByUserPaginated(ctx context.Context, userID int64, params pagination.PaginationParams, codeType string) ([]service.RedeemCode, *pagination.PaginationResult, error) {
- return nil, nil, errors.New("not implemented")
- }
- func (stubRedeemCodeRepo) SumPositiveBalanceByUser(ctx context.Context, userID int64) (float64, error) {
- return 0, errors.New("not implemented")
- }
- type stubUserSubscriptionRepo struct {
- byUser map[int64][]service.UserSubscription
- activeByUser map[int64][]service.UserSubscription
- }
- func (r *stubUserSubscriptionRepo) SetByUserID(userID int64, subs []service.UserSubscription) {
- if r.byUser == nil {
- r.byUser = make(map[int64][]service.UserSubscription)
- }
- r.byUser[userID] = append([]service.UserSubscription(nil), subs...)
- }
- func (r *stubUserSubscriptionRepo) SetActiveByUserID(userID int64, subs []service.UserSubscription) {
- if r.activeByUser == nil {
- r.activeByUser = make(map[int64][]service.UserSubscription)
- }
- r.activeByUser[userID] = append([]service.UserSubscription(nil), subs...)
- }
- func (stubUserSubscriptionRepo) Create(ctx context.Context, sub *service.UserSubscription) error {
- return errors.New("not implemented")
- }
- func (stubUserSubscriptionRepo) GetByID(ctx context.Context, id int64) (*service.UserSubscription, error) {
- return nil, errors.New("not implemented")
- }
- func (stubUserSubscriptionRepo) GetByIDForUpdate(ctx context.Context, id int64) (*service.UserSubscription, error) {
- return nil, errors.New("not implemented")
- }
- func (stubUserSubscriptionRepo) GetByIDIncludeDeleted(ctx context.Context, id int64) (*service.UserSubscription, error) {
- return nil, errors.New("not implemented")
- }
- func (stubUserSubscriptionRepo) GetByUserIDAndGroupID(ctx context.Context, userID, groupID int64) (*service.UserSubscription, error) {
- return nil, errors.New("not implemented")
- }
- func (stubUserSubscriptionRepo) GetActiveByUserIDAndGroupID(ctx context.Context, userID, groupID int64) (*service.UserSubscription, error) {
- return nil, errors.New("not implemented")
- }
- func (stubUserSubscriptionRepo) Update(ctx context.Context, sub *service.UserSubscription) error {
- return errors.New("not implemented")
- }
- func (stubUserSubscriptionRepo) Delete(ctx context.Context, id int64) error {
- return errors.New("not implemented")
- }
- func (stubUserSubscriptionRepo) Restore(ctx context.Context, subscriptionID int64, restoredStatus string) (*service.UserSubscription, error) {
- return nil, errors.New("not implemented")
- }
- func (r *stubUserSubscriptionRepo) ListByUserID(ctx context.Context, userID int64) ([]service.UserSubscription, error) {
- if r.byUser == nil {
- return nil, nil
- }
- return append([]service.UserSubscription(nil), r.byUser[userID]...), nil
- }
- func (r *stubUserSubscriptionRepo) ListActiveByUserID(ctx context.Context, userID int64) ([]service.UserSubscription, error) {
- if r.activeByUser == nil {
- return nil, nil
- }
- return append([]service.UserSubscription(nil), r.activeByUser[userID]...), nil
- }
- func (stubUserSubscriptionRepo) ListByGroupID(ctx context.Context, groupID int64, params pagination.PaginationParams) ([]service.UserSubscription, *pagination.PaginationResult, error) {
- return nil, nil, errors.New("not implemented")
- }
- func (stubUserSubscriptionRepo) List(ctx context.Context, params pagination.PaginationParams, userID, groupID *int64, status, platform, sortBy, sortOrder string) ([]service.UserSubscription, *pagination.PaginationResult, error) {
- return nil, nil, errors.New("not implemented")
- }
- func (stubUserSubscriptionRepo) ExistsByUserIDAndGroupID(ctx context.Context, userID, groupID int64) (bool, error) {
- return false, errors.New("not implemented")
- }
- func (stubUserSubscriptionRepo) ExistsActiveByUserIDAndGroupID(ctx context.Context, userID, groupID int64) (bool, error) {
- return false, errors.New("not implemented")
- }
- func (stubUserSubscriptionRepo) ExtendExpiry(ctx context.Context, subscriptionID int64, newExpiresAt time.Time) error {
- return errors.New("not implemented")
- }
- func (stubUserSubscriptionRepo) UpdateStatus(ctx context.Context, subscriptionID int64, status string) error {
- return errors.New("not implemented")
- }
- func (stubUserSubscriptionRepo) UpdateNotes(ctx context.Context, subscriptionID int64, notes string) error {
- return errors.New("not implemented")
- }
- func (stubUserSubscriptionRepo) ActivateWindows(ctx context.Context, id int64, dailyStart, periodicStart time.Time) error {
- return errors.New("not implemented")
- }
- func (stubUserSubscriptionRepo) ResetUsageWindows(ctx context.Context, id int64, resetDaily, resetWeekly, resetMonthly bool, dailyStart, periodicStart time.Time) error {
- return errors.New("not implemented")
- }
- func (stubUserSubscriptionRepo) ResetDailyUsage(ctx context.Context, id int64, expectedWindowStart *time.Time, newWindowStart time.Time) error {
- return errors.New("not implemented")
- }
- func (stubUserSubscriptionRepo) ResetWeeklyUsage(ctx context.Context, id int64, expectedWindowStart *time.Time, newWindowStart time.Time) error {
- return errors.New("not implemented")
- }
- func (stubUserSubscriptionRepo) ResetMonthlyUsage(ctx context.Context, id int64, expectedWindowStart *time.Time, newWindowStart time.Time) error {
- return errors.New("not implemented")
- }
- func (stubUserSubscriptionRepo) IncrementUsage(ctx context.Context, id int64, costUSD float64) error {
- return errors.New("not implemented")
- }
- func (stubUserSubscriptionRepo) BatchUpdateExpiredStatus(ctx context.Context) (int64, error) {
- return 0, errors.New("not implemented")
- }
- type stubApiKeyRepo struct {
- now time.Time
- nextID int64
- byID map[int64]*service.APIKey
- byKey map[string]*service.APIKey
- }
- func newStubApiKeyRepo(now time.Time) *stubApiKeyRepo {
- return &stubApiKeyRepo{
- now: now,
- nextID: 100,
- byID: make(map[int64]*service.APIKey),
- byKey: make(map[string]*service.APIKey),
- }
- }
- func (r *stubApiKeyRepo) MustSeed(key *service.APIKey) {
- if key == nil {
- return
- }
- clone := *key
- r.byID[clone.ID] = &clone
- r.byKey[clone.Key] = &clone
- }
- func (r *stubApiKeyRepo) Create(ctx context.Context, key *service.APIKey) error {
- if key == nil {
- return errors.New("nil key")
- }
- if key.ID == 0 {
- key.ID = r.nextID
- r.nextID++
- }
- if key.CreatedAt.IsZero() {
- key.CreatedAt = r.now
- }
- if key.UpdatedAt.IsZero() {
- key.UpdatedAt = r.now
- }
- clone := *key
- r.byID[clone.ID] = &clone
- r.byKey[clone.Key] = &clone
- return nil
- }
- func (r *stubApiKeyRepo) GetByID(ctx context.Context, id int64) (*service.APIKey, error) {
- key, ok := r.byID[id]
- if !ok {
- return nil, service.ErrAPIKeyNotFound
- }
- clone := *key
- return &clone, nil
- }
- func (r *stubApiKeyRepo) GetKeyAndOwnerID(ctx context.Context, id int64) (string, int64, error) {
- key, ok := r.byID[id]
- if !ok {
- return "", 0, service.ErrAPIKeyNotFound
- }
- return key.Key, key.UserID, nil
- }
- func (r *stubApiKeyRepo) GetByKey(ctx context.Context, key string) (*service.APIKey, error) {
- found, ok := r.byKey[key]
- if !ok {
- return nil, service.ErrAPIKeyNotFound
- }
- clone := *found
- return &clone, nil
- }
- func (r *stubApiKeyRepo) GetByKeyForAuth(ctx context.Context, key string) (*service.APIKey, error) {
- return r.GetByKey(ctx, key)
- }
- func (r *stubApiKeyRepo) Update(ctx context.Context, key *service.APIKey, _ service.APIKeyUpdateFields) error {
- if key == nil {
- return errors.New("nil key")
- }
- if _, ok := r.byID[key.ID]; !ok {
- return service.ErrAPIKeyNotFound
- }
- if key.UpdatedAt.IsZero() {
- key.UpdatedAt = r.now
- }
- clone := *key
- r.byID[clone.ID] = &clone
- r.byKey[clone.Key] = &clone
- return nil
- }
- func (r *stubApiKeyRepo) Delete(ctx context.Context, id int64) error {
- key, ok := r.byID[id]
- if !ok {
- return service.ErrAPIKeyNotFound
- }
- delete(r.byID, id)
- delete(r.byKey, key.Key)
- return nil
- }
- func (r *stubApiKeyRepo) DeleteWithAudit(ctx context.Context, id int64) error {
- return r.Delete(ctx, id)
- }
- func (r *stubApiKeyRepo) ListByUserID(ctx context.Context, userID int64, params pagination.PaginationParams, _ service.APIKeyListFilters) ([]service.APIKey, *pagination.PaginationResult, error) {
- ids := make([]int64, 0, len(r.byID))
- for id := range r.byID {
- if r.byID[id].UserID == userID {
- ids = append(ids, id)
- }
- }
- sort.Slice(ids, func(i, j int) bool { return ids[i] > ids[j] })
- start := params.Offset()
- if start > len(ids) {
- start = len(ids)
- }
- end := start + params.Limit()
- if end > len(ids) {
- end = len(ids)
- }
- out := make([]service.APIKey, 0, end-start)
- for _, id := range ids[start:end] {
- clone := *r.byID[id]
- out = append(out, clone)
- }
- total := int64(len(ids))
- pageSize := params.Limit()
- pages := int(math.Ceil(float64(total) / float64(pageSize)))
- if pages < 1 {
- pages = 1
- }
- return out, &pagination.PaginationResult{
- Total: total,
- Page: params.Page,
- PageSize: pageSize,
- Pages: pages,
- }, nil
- }
- func (r *stubApiKeyRepo) VerifyOwnership(ctx context.Context, userID int64, apiKeyIDs []int64) ([]int64, error) {
- if len(apiKeyIDs) == 0 {
- return []int64{}, nil
- }
- seen := make(map[int64]struct{}, len(apiKeyIDs))
- out := make([]int64, 0, len(apiKeyIDs))
- for _, id := range apiKeyIDs {
- if _, ok := seen[id]; ok {
- continue
- }
- seen[id] = struct{}{}
- key, ok := r.byID[id]
- if ok && key.UserID == userID {
- out = append(out, id)
- }
- }
- return out, nil
- }
- func (r *stubApiKeyRepo) CountByUserID(ctx context.Context, userID int64) (int64, error) {
- var count int64
- for _, key := range r.byID {
- if key.UserID == userID {
- count++
- }
- }
- return count, nil
- }
- func (r *stubApiKeyRepo) ExistsByKey(ctx context.Context, key string) (bool, error) {
- _, ok := r.byKey[key]
- return ok, nil
- }
- func (r *stubApiKeyRepo) ListByGroupID(ctx context.Context, groupID int64, params pagination.PaginationParams) ([]service.APIKey, *pagination.PaginationResult, error) {
- return nil, nil, errors.New("not implemented")
- }
- func (r *stubApiKeyRepo) SearchAPIKeys(ctx context.Context, userID int64, keyword string, limit int) ([]service.APIKey, error) {
- return nil, errors.New("not implemented")
- }
- func (r *stubApiKeyRepo) ClearGroupIDByGroupID(ctx context.Context, groupID int64) (int64, error) {
- return 0, errors.New("not implemented")
- }
- func (r *stubApiKeyRepo) UpdateGroupIDByUserAndGroup(ctx context.Context, userID, oldGroupID, newGroupID int64) (int64, error) {
- var updated int64
- for id, key := range r.byID {
- if key.UserID != userID || key.GroupID == nil || *key.GroupID != oldGroupID {
- continue
- }
- clone := *key
- gid := newGroupID
- clone.GroupID = &gid
- r.byID[id] = &clone
- r.byKey[clone.Key] = &clone
- updated++
- }
- return updated, nil
- }
- func (r *stubApiKeyRepo) CountByGroupID(ctx context.Context, groupID int64) (int64, error) {
- return 0, errors.New("not implemented")
- }
- func (r *stubApiKeyRepo) ListKeysByUserID(ctx context.Context, userID int64) ([]string, error) {
- return nil, errors.New("not implemented")
- }
- func (r *stubApiKeyRepo) ListKeysByGroupID(ctx context.Context, groupID int64) ([]string, error) {
- return nil, errors.New("not implemented")
- }
- func (r *stubApiKeyRepo) IncrementQuotaUsed(ctx context.Context, id int64, amount float64) (float64, error) {
- return 0, errors.New("not implemented")
- }
- func (r *stubApiKeyRepo) UpdateLastUsed(ctx context.Context, id int64, usedAt time.Time) error {
- key, ok := r.byID[id]
- if !ok {
- return service.ErrAPIKeyNotFound
- }
- ts := usedAt
- key.LastUsedAt = &ts
- key.UpdatedAt = usedAt
- clone := *key
- r.byID[id] = &clone
- r.byKey[clone.Key] = &clone
- return nil
- }
- func (r *stubApiKeyRepo) IncrementRateLimitUsage(ctx context.Context, id int64, cost float64) error {
- return nil
- }
- func (r *stubApiKeyRepo) ResetRateLimitWindows(ctx context.Context, id int64) error {
- return nil
- }
- func (r *stubApiKeyRepo) GetRateLimitData(ctx context.Context, id int64) (*service.APIKeyRateLimitData, error) {
- return nil, nil
- }
- type stubUsageLogRepo struct {
- userLogs map[int64][]service.UsageLog
- }
- func newStubUsageLogRepo() *stubUsageLogRepo {
- return &stubUsageLogRepo{userLogs: make(map[int64][]service.UsageLog)}
- }
- func (r *stubUsageLogRepo) SetUserLogs(userID int64, logs []service.UsageLog) {
- r.userLogs[userID] = logs
- }
- func (r *stubUsageLogRepo) Create(ctx context.Context, log *service.UsageLog) (bool, error) {
- return false, errors.New("not implemented")
- }
- func (r *stubUsageLogRepo) GetByID(ctx context.Context, id int64) (*service.UsageLog, error) {
- return nil, errors.New("not implemented")
- }
- func (r *stubUsageLogRepo) Delete(ctx context.Context, id int64) error {
- return errors.New("not implemented")
- }
- func (r *stubUsageLogRepo) ListByUser(ctx context.Context, userID int64, params pagination.PaginationParams) ([]service.UsageLog, *pagination.PaginationResult, error) {
- logs := r.userLogs[userID]
- total := int64(len(logs))
- out := paginateLogs(logs, params)
- return out, paginationResult(total, params), nil
- }
- func (r *stubUsageLogRepo) ListByAPIKey(ctx context.Context, apiKeyID int64, params pagination.PaginationParams) ([]service.UsageLog, *pagination.PaginationResult, error) {
- return nil, nil, errors.New("not implemented")
- }
- func (r *stubUsageLogRepo) ListByAccount(ctx context.Context, accountID int64, params pagination.PaginationParams) ([]service.UsageLog, *pagination.PaginationResult, error) {
- return nil, nil, errors.New("not implemented")
- }
- func (r *stubUsageLogRepo) ListByUserAndTimeRange(ctx context.Context, userID int64, startTime, endTime time.Time) ([]service.UsageLog, *pagination.PaginationResult, error) {
- logs := r.userLogs[userID]
- return logs, paginationResult(int64(len(logs)), pagination.PaginationParams{Page: 1, PageSize: 100}), nil
- }
- func (r *stubUsageLogRepo) ListByAPIKeyAndTimeRange(ctx context.Context, apiKeyID int64, startTime, endTime time.Time) ([]service.UsageLog, *pagination.PaginationResult, error) {
- return nil, nil, errors.New("not implemented")
- }
- func (r *stubUsageLogRepo) ListByAccountAndTimeRange(ctx context.Context, accountID int64, startTime, endTime time.Time) ([]service.UsageLog, *pagination.PaginationResult, error) {
- return nil, nil, errors.New("not implemented")
- }
- func (r *stubUsageLogRepo) ListByModelAndTimeRange(ctx context.Context, modelName string, startTime, endTime time.Time) ([]service.UsageLog, *pagination.PaginationResult, error) {
- return nil, nil, errors.New("not implemented")
- }
- func (r *stubUsageLogRepo) GetAccountWindowStats(ctx context.Context, accountID int64, startTime time.Time) (*usagestats.AccountStats, error) {
- return nil, errors.New("not implemented")
- }
- func (r *stubUsageLogRepo) GetAccountTodayStats(ctx context.Context, accountID int64) (*usagestats.AccountStats, error) {
- return nil, errors.New("not implemented")
- }
- func (r *stubUsageLogRepo) GetDashboardStats(ctx context.Context) (*usagestats.DashboardStats, error) {
- return nil, errors.New("not implemented")
- }
- 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) {
- return nil, errors.New("not implemented")
- }
- 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) {
- return nil, errors.New("not implemented")
- }
- 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) {
- return nil, errors.New("not implemented")
- }
- 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) {
- return nil, errors.New("not implemented")
- }
- 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) {
- return nil, errors.New("not implemented")
- }
- func (r *stubUsageLogRepo) GetUserBreakdownStats(ctx context.Context, startTime, endTime time.Time, dim usagestats.UserBreakdownDimension, limit int) ([]usagestats.UserBreakdownItem, error) {
- return nil, errors.New("not implemented")
- }
- func (r *stubUsageLogRepo) GetAPIKeyUsageTrend(ctx context.Context, startTime, endTime time.Time, granularity string, limit int) ([]usagestats.APIKeyUsageTrendPoint, error) {
- return nil, errors.New("not implemented")
- }
- func (r *stubUsageLogRepo) GetUserUsageTrend(ctx context.Context, startTime, endTime time.Time, granularity string, limit int) ([]usagestats.UserUsageTrendPoint, error) {
- return nil, errors.New("not implemented")
- }
- func (r *stubUsageLogRepo) GetUserSpendingRanking(ctx context.Context, startTime, endTime time.Time, limit int) (*usagestats.UserSpendingRankingResponse, error) {
- return nil, errors.New("not implemented")
- }
- func (r *stubUsageLogRepo) GetUserStatsAggregated(ctx context.Context, userID int64, startTime, endTime time.Time) (*usagestats.UsageStats, error) {
- logs := r.userLogs[userID]
- if len(logs) == 0 {
- return &usagestats.UsageStats{}, nil
- }
- var totalRequests int64
- var totalInputTokens int64
- var totalOutputTokens int64
- var totalCacheTokens int64
- var totalCacheCreationTokens int64
- var totalCacheReadTokens int64
- var totalCost float64
- var totalActualCost float64
- var totalDuration int64
- var durationCount int64
- for _, log := range logs {
- totalRequests++
- totalInputTokens += int64(log.InputTokens)
- totalOutputTokens += int64(log.OutputTokens)
- totalCacheTokens += int64(log.CacheCreationTokens + log.CacheReadTokens)
- totalCacheCreationTokens += int64(log.CacheCreationTokens)
- totalCacheReadTokens += int64(log.CacheReadTokens)
- totalCost += log.TotalCost
- totalActualCost += log.ActualCost
- if log.DurationMs != nil {
- totalDuration += int64(*log.DurationMs)
- durationCount++
- }
- }
- var avgDuration float64
- if durationCount > 0 {
- avgDuration = float64(totalDuration) / float64(durationCount)
- }
- return &usagestats.UsageStats{
- TotalRequests: totalRequests,
- TotalInputTokens: totalInputTokens,
- TotalOutputTokens: totalOutputTokens,
- TotalCacheTokens: totalCacheTokens,
- TotalCacheCreationTokens: totalCacheCreationTokens,
- TotalCacheReadTokens: totalCacheReadTokens,
- TotalTokens: totalInputTokens + totalOutputTokens + totalCacheTokens,
- TotalCost: totalCost,
- TotalActualCost: totalActualCost,
- AverageDurationMs: avgDuration,
- }, nil
- }
- func (r *stubUsageLogRepo) GetAPIKeyStatsAggregated(ctx context.Context, apiKeyID int64, startTime, endTime time.Time) (*usagestats.UsageStats, error) {
- return nil, errors.New("not implemented")
- }
- func (r *stubUsageLogRepo) GetAccountStatsAggregated(ctx context.Context, accountID int64, startTime, endTime time.Time) (*usagestats.UsageStats, error) {
- return nil, errors.New("not implemented")
- }
- func (r *stubUsageLogRepo) GetModelStatsAggregated(ctx context.Context, modelName string, startTime, endTime time.Time) (*usagestats.UsageStats, error) {
- return nil, errors.New("not implemented")
- }
- func (r *stubUsageLogRepo) GetDailyStatsAggregated(ctx context.Context, userID int64, startTime, endTime time.Time) ([]map[string]any, error) {
- return nil, errors.New("not implemented")
- }
- func (r *stubUsageLogRepo) GetBatchUserUsageStats(ctx context.Context, userIDs []int64, startTime, endTime time.Time) (map[int64]*usagestats.BatchUserUsageStats, error) {
- return nil, errors.New("not implemented")
- }
- func (r *stubUsageLogRepo) GetBatchAPIKeyUsageStats(ctx context.Context, apiKeyIDs []int64, startTime, endTime time.Time) (map[int64]*usagestats.BatchAPIKeyUsageStats, error) {
- return nil, errors.New("not implemented")
- }
- func (r *stubUsageLogRepo) GetUserDashboardStats(ctx context.Context, userID int64) (*usagestats.UserDashboardStats, error) {
- return nil, errors.New("not implemented")
- }
- func (r *stubUsageLogRepo) GetAPIKeyDashboardStats(ctx context.Context, apiKeyID int64) (*usagestats.UserDashboardStats, error) {
- return nil, errors.New("not implemented")
- }
- func (r *stubUsageLogRepo) GetUserUsageTrendByUserID(ctx context.Context, userID int64, startTime, endTime time.Time, granularity string) ([]usagestats.TrendDataPoint, error) {
- return nil, errors.New("not implemented")
- }
- func (r *stubUsageLogRepo) GetUserModelStats(ctx context.Context, userID int64, startTime, endTime time.Time) ([]usagestats.ModelStat, error) {
- return nil, errors.New("not implemented")
- }
- func (r *stubUsageLogRepo) ListWithFilters(ctx context.Context, params pagination.PaginationParams, filters usagestats.UsageLogFilters) ([]service.UsageLog, *pagination.PaginationResult, error) {
- logs := r.userLogs[filters.UserID]
- // Apply filters
- var filtered []service.UsageLog
- for _, log := range logs {
- // Apply APIKeyID filter
- if filters.APIKeyID > 0 && log.APIKeyID != filters.APIKeyID {
- continue
- }
- // Apply Model filter
- if filters.Model != "" && stubUsageLogFilterModel(log, filters.ModelFilterSource) != filters.Model {
- continue
- }
- // Apply Stream filter
- if filters.Stream != nil && log.Stream != *filters.Stream {
- continue
- }
- // Apply BillingType filter
- if filters.BillingType != nil && log.BillingType != *filters.BillingType {
- continue
- }
- // Apply time range filters
- if filters.StartTime != nil && log.CreatedAt.Before(*filters.StartTime) {
- continue
- }
- if filters.EndTime != nil && log.CreatedAt.After(*filters.EndTime) {
- continue
- }
- filtered = append(filtered, log)
- }
- total := int64(len(filtered))
- out := paginateLogs(filtered, params)
- return out, paginationResult(total, params), nil
- }
- func stubUsageLogFilterModel(log service.UsageLog, source string) string {
- if source == usagestats.ModelSourceRequested && log.RequestedModel != "" {
- return log.RequestedModel
- }
- return log.Model
- }
- func (r *stubUsageLogRepo) GetGlobalStats(ctx context.Context, startTime, endTime time.Time) (*usagestats.UsageStats, error) {
- return nil, errors.New("not implemented")
- }
- func (r *stubUsageLogRepo) GetAccountUsageStats(ctx context.Context, accountID int64, startTime, endTime time.Time) (*usagestats.AccountUsageStatsResponse, error) {
- return nil, errors.New("not implemented")
- }
- func (r *stubUsageLogRepo) GetStatsWithFilters(ctx context.Context, filters usagestats.UsageLogFilters) (*usagestats.UsageStats, error) {
- logs, _, err := r.ListWithFilters(ctx, pagination.PaginationParams{Page: 1, PageSize: 100000}, filters)
- if err != nil {
- return nil, err
- }
- var totalRequests int64
- var totalInputTokens int64
- var totalOutputTokens int64
- var totalCacheTokens int64
- var totalCacheCreationTokens int64
- var totalCacheReadTokens int64
- var totalCost float64
- var totalActualCost float64
- var totalDuration int64
- var durationCount int64
- for _, log := range logs {
- totalRequests++
- totalInputTokens += int64(log.InputTokens)
- totalOutputTokens += int64(log.OutputTokens)
- totalCacheTokens += int64(log.CacheCreationTokens + log.CacheReadTokens)
- totalCacheCreationTokens += int64(log.CacheCreationTokens)
- totalCacheReadTokens += int64(log.CacheReadTokens)
- totalCost += log.TotalCost
- totalActualCost += log.ActualCost
- if log.DurationMs != nil {
- totalDuration += int64(*log.DurationMs)
- durationCount++
- }
- }
- var avgDuration float64
- if durationCount > 0 {
- avgDuration = float64(totalDuration) / float64(durationCount)
- }
- return &usagestats.UsageStats{
- TotalRequests: totalRequests,
- TotalInputTokens: totalInputTokens,
- TotalOutputTokens: totalOutputTokens,
- TotalCacheTokens: totalCacheTokens,
- TotalCacheCreationTokens: totalCacheCreationTokens,
- TotalCacheReadTokens: totalCacheReadTokens,
- TotalTokens: totalInputTokens + totalOutputTokens + totalCacheTokens,
- TotalCost: totalCost,
- TotalActualCost: totalActualCost,
- AverageDurationMs: avgDuration,
- Endpoints: []usagestats.EndpointStat{},
- }, nil
- }
- func (r *stubUsageLogRepo) GetAllGroupUsageSummary(ctx context.Context, todayStart time.Time) ([]usagestats.GroupUsageSummary, error) {
- return nil, errors.New("not implemented")
- }
- type stubSettingRepo struct {
- all map[string]string
- }
- func newStubSettingRepo() *stubSettingRepo {
- return &stubSettingRepo{all: make(map[string]string)}
- }
- func (r *stubSettingRepo) SetAll(values map[string]string) {
- r.all = make(map[string]string, len(values))
- for k, v := range values {
- r.all[k] = v
- }
- }
- func (r *stubSettingRepo) Get(ctx context.Context, key string) (*service.Setting, error) {
- value, ok := r.all[key]
- if !ok {
- return nil, service.ErrSettingNotFound
- }
- return &service.Setting{Key: key, Value: value}, nil
- }
- func (r *stubSettingRepo) GetValue(ctx context.Context, key string) (string, error) {
- value, ok := r.all[key]
- if !ok {
- return "", service.ErrSettingNotFound
- }
- return value, nil
- }
- func (r *stubSettingRepo) Set(ctx context.Context, key, value string) error {
- r.all[key] = value
- return nil
- }
- func (r *stubSettingRepo) GetMultiple(ctx context.Context, keys []string) (map[string]string, error) {
- out := make(map[string]string, len(keys))
- for _, key := range keys {
- out[key] = r.all[key]
- }
- return out, nil
- }
- func (r *stubSettingRepo) SetMultiple(ctx context.Context, settings map[string]string) error {
- for k, v := range settings {
- r.all[k] = v
- }
- return nil
- }
- func (r *stubSettingRepo) GetAll(ctx context.Context) (map[string]string, error) {
- out := make(map[string]string, len(r.all))
- for k, v := range r.all {
- out[k] = v
- }
- return out, nil
- }
- func (r *stubSettingRepo) Delete(ctx context.Context, key string) error {
- delete(r.all, key)
- return nil
- }
- func paginateLogs(logs []service.UsageLog, params pagination.PaginationParams) []service.UsageLog {
- start := params.Offset()
- if start > len(logs) {
- start = len(logs)
- }
- end := start + params.Limit()
- if end > len(logs) {
- end = len(logs)
- }
- out := make([]service.UsageLog, 0, end-start)
- out = append(out, logs[start:end]...)
- return out
- }
- func paginationResult(total int64, params pagination.PaginationParams) *pagination.PaginationResult {
- pageSize := params.Limit()
- pages := int(math.Ceil(float64(total) / float64(pageSize)))
- if pages < 1 {
- pages = 1
- }
- return &pagination.PaginationResult{
- Total: total,
- Page: params.Page,
- PageSize: pageSize,
- Pages: pages,
- }
- }
- // Ensure compile-time interface compliance.
- var (
- _ service.UserRepository = (*stubUserRepo)(nil)
- _ service.APIKeyRepository = (*stubApiKeyRepo)(nil)
- _ service.APIKeyCache = (*stubApiKeyCache)(nil)
- _ service.AdminGroupRepository = (*stubGroupRepo)(nil)
- _ service.UserSubscriptionRepository = (*stubUserSubscriptionRepo)(nil)
- _ service.UsageLogRepository = (*stubUsageLogRepo)(nil)
- _ service.SettingRepository = (*stubSettingRepo)(nil)
- )
|