2026-05-12 22:44:30 +08:00
package app
import (
2026-05-15 19:26:25 +08:00
"bytes"
2026-05-12 22:44:30 +08:00
"context"
2026-05-15 19:26:25 +08:00
"encoding/json"
2026-05-12 22:44:30 +08:00
"errors"
2026-05-20 22:09:40 +08:00
"fmt"
2026-05-12 22:44:30 +08:00
"io"
"net"
"net/http"
2026-05-15 19:26:25 +08:00
"net/http/httptest"
2026-05-20 22:09:40 +08:00
"path/filepath"
2026-05-15 19:26:25 +08:00
"strings"
2026-05-12 22:44:30 +08:00
"testing"
"time"
2026-05-15 19:26:25 +08:00
"sub2api-cn-relay-manager/internal/host/sub2api"
"sub2api-cn-relay-manager/internal/pack"
"sub2api-cn-relay-manager/internal/provision"
"sub2api-cn-relay-manager/internal/store/sqlite"
2026-05-12 22:44:30 +08:00
)
func TestServeExposesHealthz ( t * testing . T ) {
2026-05-15 19:26:25 +08:00
server := NewServer ( "127.0.0.1:0" , NewAPIHandler ( "admin-token" , ActionSet { } ) , nil )
2026-05-12 22:44:30 +08:00
listener , err := net . Listen ( "tcp" , "127.0.0.1:0" )
if err != nil {
t . Fatalf ( "net.Listen() error = %v" , err )
}
ctx , cancel := context . WithCancel ( context . Background ( ) )
defer cancel ( )
errCh := make ( chan error , 1 )
go func ( ) {
errCh <- server . Serve ( ctx , listener )
} ( )
response := waitForHealthz ( t , "http://" + listener . Addr ( ) . String ( ) + "/healthz" )
defer response . Body . Close ( )
body , err := io . ReadAll ( response . Body )
if err != nil {
t . Fatalf ( "ReadAll() error = %v" , err )
}
if string ( body ) != "ok" {
t . Fatalf ( "healthz body = %q, want %q" , string ( body ) , "ok" )
}
cancel ( )
if err := <- errCh ; err != nil {
t . Fatalf ( "Serve() error = %v, want nil" , err )
}
}
func TestRunReturnsAfterContextCancellation ( t * testing . T ) {
listener , err := net . Listen ( "tcp" , "127.0.0.1:0" )
if err != nil {
t . Fatalf ( "net.Listen() error = %v" , err )
}
2026-05-15 19:26:25 +08:00
server := NewServer ( "127.0.0.1:0" , NewAPIHandler ( "admin-token" , ActionSet { } ) , func ( string , string ) ( net . Listener , error ) {
2026-05-12 22:44:30 +08:00
return listener , nil
} )
ctx , cancel := context . WithCancel ( context . Background ( ) )
errCh := make ( chan error , 1 )
go func ( ) {
errCh <- server . Run ( ctx )
} ( )
response := waitForHealthz ( t , "http://" + listener . Addr ( ) . String ( ) + "/healthz" )
response . Body . Close ( )
cancel ( )
select {
case err := <- errCh :
if err != nil {
t . Fatalf ( "Run() error = %v, want nil" , err )
}
case <- time . After ( 2 * time . Second ) :
t . Fatal ( "Run() did not return after context cancellation" )
}
}
func TestRunReturnsListenError ( t * testing . T ) {
wantErr := errors . New ( "listen failed" )
2026-05-15 19:26:25 +08:00
server := NewServer ( "127.0.0.1:0" , NewAPIHandler ( "admin-token" , ActionSet { } ) , func ( string , string ) ( net . Listener , error ) {
2026-05-12 22:44:30 +08:00
return nil , wantErr
} )
err := server . Run ( context . Background ( ) )
if ! errors . Is ( err , wantErr ) {
t . Fatalf ( "Run() error = %v, want %v" , err , wantErr )
}
}
func TestServeReturnsListenerError ( t * testing . T ) {
2026-05-15 19:26:25 +08:00
server := NewServer ( "127.0.0.1:0" , NewAPIHandler ( "admin-token" , ActionSet { } ) , nil )
2026-05-12 22:44:30 +08:00
listener , err := net . Listen ( "tcp" , "127.0.0.1:0" )
if err != nil {
t . Fatalf ( "net.Listen() error = %v" , err )
}
if err := listener . Close ( ) ; err != nil {
t . Fatalf ( "listener.Close() error = %v" , err )
}
err = server . Serve ( context . Background ( ) , listener )
if err == nil {
t . Fatal ( "Serve() error = nil, want listener startup error" )
}
}
2026-05-15 19:26:25 +08:00
func TestAPIRejectsMissingAdminToken ( t * testing . T ) {
handler := NewAPIHandler ( "secret-token" , ActionSet { } )
request := httptestRequest ( t , http . MethodPost , "/api/packs/install" , map [ string ] any { "host_base_url" : "https://sub2api.example.com" , "pack_path" : "/tmp/pack.zip" } , "" )
response := httptestRecorder ( handler , request )
assertStatusCode ( t , response , http . StatusUnauthorized )
assertJSONContains ( t , response . Body ( ) . Bytes ( ) , "error.code" , "unauthorized" )
}
func TestAPIInstallPackReturnsSummary ( t * testing . T ) {
handler := NewAPIHandler ( "secret-token" , ActionSet {
InstallPack : func ( context . Context , InstallPackRequest ) ( provision . PackInstallResult , error ) {
return provision . PackInstallResult {
Pack : sqlite . Pack { PackID : "openai-cn-pack" , Version : "1.0.0" } ,
HostVersion : "0.1.126" ,
Providers : [ ] sqlite . Provider { { ProviderID : "deepseek" , DisplayName : "DeepSeek" } } ,
} , nil
} ,
} )
request := httptestRequest ( t , http . MethodPost , "/api/packs/install" , map [ string ] any { "host_base_url" : "https://sub2api.example.com" , "pack_path" : "/tmp/openai-pack.zip" } , "secret-token" )
response := httptestRecorder ( handler , request )
assertStatusCode ( t , response , http . StatusOK )
assertJSONContains ( t , response . Body ( ) . Bytes ( ) , "pack_id" , "openai-cn-pack" )
assertJSONContains ( t , response . Body ( ) . Bytes ( ) , "host_version" , "0.1.126" )
}
func TestAPIPreviewProviderReturnsSummary ( t * testing . T ) {
handler := NewAPIHandler ( "secret-token" , ActionSet {
PreviewProvider : func ( _ context . Context , req PreviewProviderRequest ) ( provision . PreviewReport , error ) {
if req . ProviderID != "deepseek" {
t . Fatalf ( "ProviderID = %q, want deepseek" , req . ProviderID )
}
return provision . PreviewReport {
AcceptedKeys : [ ] string { "k1" , "k2" } ,
Names : provision . ResourceNames { Group : "g" , Channel : "c" , Plan : "p" } ,
Decisions : map [ string ] provision . PreviewDecision {
"group" : { Action : provision . PreviewActionCreate } ,
} ,
} , nil
} ,
} )
request := httptestRequest ( t , http . MethodPost , "/api/providers/deepseek/preview-import" , map [ string ] any { "host_base_url" : "https://sub2api.example.com" , "pack_path" : "/tmp/openai-pack.zip" , "keys" : [ ] string { "k1" , "k2" } , "mode" : "partial" } , "secret-token" )
response := httptestRecorder ( handler , request )
assertStatusCode ( t , response , http . StatusOK )
assertJSONContains ( t , response . Body ( ) . Bytes ( ) , "accepted_keys_count" , float64 ( 2 ) )
}
func TestAPIImportProviderReturnsConflictWithBatchStatus ( t * testing . T ) {
handler := NewAPIHandler ( "secret-token" , ActionSet {
ImportProvider : func ( context . Context , ImportProviderRequest ) ( provision . RuntimeImportResult , error ) {
return provision . RuntimeImportResult {
BatchID : 12 ,
Report : provision . ImportReport {
BatchStatus : provision . BatchStatusFailed ,
ProviderStatus : provision . ProviderStatusFailed ,
AccessStatus : provision . AccessStatusBroken ,
Accounts : [ ] provision . AccountImportResult { { } } ,
} ,
} , errors . New ( "strict import failed" )
} ,
} )
request := httptestRequest ( t , http . MethodPost , "/api/providers/deepseek/import" , map [ string ] any { "host_base_url" : "https://sub2api.example.com" , "pack_path" : "/tmp/openai-pack.zip" , "keys" : [ ] string { "k1" } , "mode" : "strict" , "access_mode" : "self_service" , "access_api_key" : "user-key" } , "secret-token" )
response := httptestRecorder ( handler , request )
assertStatusCode ( t , response , http . StatusConflict )
assertJSONContains ( t , response . Body ( ) . Bytes ( ) , "batch_id" , float64 ( 12 ) )
assertJSONContains ( t , response . Body ( ) . Bytes ( ) , "batch_status" , provision . BatchStatusFailed )
}
func TestAPIBatchDetailReturnsSummary ( t * testing . T ) {
handler := NewAPIHandler ( "secret-token" , ActionSet {
BatchDetail : func ( context . Context , BatchDetailRequest ) ( provision . BatchDetailResult , error ) {
return provision . BatchDetailResult {
Batch : sqlite . ImportBatch { ID : 7 , BatchStatus : "running" , AccessStatus : "pending" } ,
Items : [ ] sqlite . ImportBatchItem { { ID : 1 , KeyFingerprint : "sha256:abc" , AccountStatus : "passed" } } ,
} , nil
} ,
} )
request := httptestRequest ( t , http . MethodGet , "/api/import-batches/7" , nil , "secret-token" )
response := httptestRecorder ( handler , request )
assertStatusCode ( t , response , http . StatusOK )
assertJSONContains ( t , response . Body ( ) . Bytes ( ) , "batch.batch_status" , "running" )
assertJSONContains ( t , response . Body ( ) . Bytes ( ) , "items_count" , float64 ( 1 ) )
}
func TestAPIProviderStatusReturnsSummary ( t * testing . T ) {
handler := NewAPIHandler ( "secret-token" , ActionSet {
GetProviderStatus : func ( _ context . Context , req ProviderQueryRequest ) ( provision . ProviderSnapshot , error ) {
if req . ProviderID != "deepseek" {
t . Fatalf ( "ProviderID = %q, want deepseek" , req . ProviderID )
}
if req . PackID != "openai-cn-pack" {
t . Fatalf ( "PackID = %q, want openai-cn-pack" , req . PackID )
}
return provision . ProviderSnapshot {
Host : sqlite . Host { HostID : "host-1" , BaseURL : "https://sub2api.example.com" , HostVersion : "0.1.126" } ,
Pack : sqlite . Pack { PackID : "openai-cn-pack" , Version : "1.0.0" } ,
Provider : sqlite . Provider { ProviderID : "deepseek" , DisplayName : "DeepSeek" , Platform : "openai" } ,
Batch : sqlite . ImportBatch { ID : 7 , BatchStatus : provision . BatchStatusSucceeded , AccessStatus : provision . AccessStatusSelfServiceReady , Mode : provision . ImportModeStrict } ,
ProviderStatus : "drifted" ,
LatestAccessStatus : provision . AccessStatusSelfServiceReady ,
LatestReconcileStatus : "drifted" ,
LatestReconcileSummary : map [ string ] any { "missing_count" : 1 } ,
ManagedResources : [ ] sqlite . ManagedResource { { } , { } } ,
AccessClosures : [ ] sqlite . AccessClosureRecord { { } } ,
ReconcileRuns : [ ] sqlite . ReconcileRun { { } } ,
} , nil
} ,
} )
request := httptestRequest ( t , http . MethodGet , "/api/providers/deepseek/status?pack_id=openai-cn-pack" , nil , "secret-token" )
response := httptestRecorder ( handler , request )
assertStatusCode ( t , response , http . StatusOK )
assertJSONContains ( t , response . Body ( ) . Bytes ( ) , "provider_status" , "drifted" )
assertJSONContains ( t , response . Body ( ) . Bytes ( ) , "managed_resources_count" , float64 ( 2 ) )
assertJSONContains ( t , response . Body ( ) . Bytes ( ) , "latest_reconcile_summary.missing_count" , float64 ( 1 ) )
}
func TestAPIProviderAccessStatusReturnsSummary ( t * testing . T ) {
handler := NewAPIHandler ( "secret-token" , ActionSet {
GetProviderAccessStatus : func ( _ context . Context , req ProviderQueryRequest ) ( provision . ProviderSnapshot , error ) {
if req . ProviderID != "deepseek" {
t . Fatalf ( "ProviderID = %q, want deepseek" , req . ProviderID )
}
return provision . ProviderSnapshot {
Pack : sqlite . Pack { PackID : "openai-cn-pack" } ,
Provider : sqlite . Provider { ProviderID : "deepseek" } ,
Batch : sqlite . ImportBatch { ID : 7 , AccessStatus : provision . AccessStatusSelfServiceReady } ,
LatestAccessStatus : provision . AccessStatusSelfServiceReady ,
AccessClosures : [ ] sqlite . AccessClosureRecord { { ID : 2 , ClosureType : provision . AccessModeSelfService , Status : provision . AccessStatusSelfServiceReady , DetailsJSON : ` { "ok":true} ` } } ,
} , nil
} ,
} )
request := httptestRequest ( t , http . MethodGet , "/api/providers/deepseek/access/status" , nil , "secret-token" )
response := httptestRecorder ( handler , request )
assertStatusCode ( t , response , http . StatusOK )
assertJSONContains ( t , response . Body ( ) . Bytes ( ) , "latest_access_status" , provision . AccessStatusSelfServiceReady )
assertJSONContains ( t , response . Body ( ) . Bytes ( ) , "closures_count" , float64 ( 1 ) )
if ! strings . Contains ( response . Body ( ) . String ( ) , ` "closure_type":"self_service" ` ) {
t . Fatalf ( "access status payload missing closure type: %s" , response . Body ( ) . String ( ) )
}
}
func TestAPIProviderResourcesReturnsSummary ( t * testing . T ) {
handler := NewAPIHandler ( "secret-token" , ActionSet {
GetProviderResources : func ( _ context . Context , req ProviderQueryRequest ) ( provision . ProviderSnapshot , error ) {
if req . ProviderID != "deepseek" {
t . Fatalf ( "ProviderID = %q, want deepseek" , req . ProviderID )
}
return provision . ProviderSnapshot {
Pack : sqlite . Pack { PackID : "openai-cn-pack" } ,
Provider : sqlite . Provider { ProviderID : "deepseek" } ,
Batch : sqlite . ImportBatch { ID : 7 } ,
ManagedResources : [ ] sqlite . ManagedResource { { ID : 1 , ResourceType : "group" , HostResourceID : "group-1" , ResourceName : "deepseek-group" } } ,
AccessClosures : [ ] sqlite . AccessClosureRecord { { ID : 2 , ClosureType : provision . AccessModeSelfService , Status : provision . AccessStatusSelfServiceReady , DetailsJSON : ` { "ok":true} ` } } ,
ReconcileRuns : [ ] sqlite . ReconcileRun { { ID : 3 , Status : "active" , SummaryJSON : ` { "missing_count":0} ` } } ,
} , nil
} ,
} )
request := httptestRequest ( t , http . MethodGet , "/api/providers/deepseek/resources" , nil , "secret-token" )
response := httptestRecorder ( handler , request )
assertStatusCode ( t , response , http . StatusOK )
assertJSONContains ( t , response . Body ( ) . Bytes ( ) , "provider_id" , "deepseek" )
assertJSONContains ( t , response . Body ( ) . Bytes ( ) , "pack_id" , "openai-cn-pack" )
if ! strings . Contains ( response . Body ( ) . String ( ) , ` "resource_type":"group" ` ) {
t . Fatalf ( "resources payload missing group resource: %s" , response . Body ( ) . String ( ) )
}
if ! strings . Contains ( response . Body ( ) . String ( ) , ` "status":"self_service_ready" ` ) {
t . Fatalf ( "resources payload missing access closure status: %s" , response . Body ( ) . String ( ) )
}
if ! strings . Contains ( response . Body ( ) . String ( ) , ` "summary_json":" { \"missing_count\":0}" ` ) {
t . Fatalf ( "resources payload missing reconcile summary: %s" , response . Body ( ) . String ( ) )
}
}
func TestAPIRollbackProviderReturnsSummary ( t * testing . T ) {
handler := NewAPIHandler ( "secret-token" , ActionSet {
RollbackProvider : func ( context . Context , RollbackProviderRequest ) ( provision . RollbackReport , error ) {
return provision . RollbackReport { AccountsDeleted : 2 , PlansDeleted : 1 , ChannelsDeleted : 1 , GroupsDeleted : 1 } , nil
} ,
} )
request := httptestRequest ( t , http . MethodPost , "/api/providers/deepseek/rollback" , map [ string ] any { "host_base_url" : "https://sub2api.example.com" , "pack_path" : "/tmp/openai-pack.zip" } , "secret-token" )
response := httptestRecorder ( handler , request )
assertStatusCode ( t , response , http . StatusOK )
assertJSONContains ( t , response . Body ( ) . Bytes ( ) , "deleted_accounts" , float64 ( 2 ) )
assertJSONContains ( t , response . Body ( ) . Bytes ( ) , "provider_id" , "deepseek" )
}
func TestAPIReconcileProviderReturnsSummary ( t * testing . T ) {
handler := NewAPIHandler ( "secret-token" , ActionSet {
ReconcileProvider : func ( _ context . Context , req ReconcileProviderRequest ) ( provision . ReconcileResult , error ) {
if req . AccessAPIKey != "user-key" {
t . Fatalf ( "AccessAPIKey = %q, want user-key" , req . AccessAPIKey )
}
return provision . ReconcileResult { BatchID : 7 , Status : "drifted" , MissingCount : 1 , ExtraCount : 2 , ProbeFailureCount : 1 , AccessStatus : provision . AccessStatusBroken , Summary : map [ string ] any { "probe_failures" : 1 } } , nil
} ,
} )
request := httptestRequest ( t , http . MethodPost , "/api/providers/deepseek/reconcile" , map [ string ] any { "host_base_url" : "https://sub2api.example.com" , "pack_path" : "/tmp/openai-pack.zip" , "access_api_key" : "user-key" } , "secret-token" )
response := httptestRecorder ( handler , request )
assertStatusCode ( t , response , http . StatusOK )
assertJSONContains ( t , response . Body ( ) . Bytes ( ) , "status" , "drifted" )
assertJSONContains ( t , response . Body ( ) . Bytes ( ) , "missing_count" , float64 ( 1 ) )
assertJSONContains ( t , response . Body ( ) . Bytes ( ) , "summary.probe_failures" , float64 ( 1 ) )
}
2026-05-12 22:44:30 +08:00
func waitForHealthz ( t * testing . T , url string ) * http . Response {
t . Helper ( )
client := & http . Client { Timeout : 100 * time . Millisecond }
deadline := time . Now ( ) . Add ( 2 * time . Second )
for time . Now ( ) . Before ( deadline ) {
response , err := client . Get ( url )
if err == nil && response . StatusCode == http . StatusOK {
return response
}
if response != nil {
response . Body . Close ( )
}
time . Sleep ( 20 * time . Millisecond )
}
t . Fatalf ( "health endpoint %q was not reachable before deadline" , url )
return nil
}
2026-05-15 19:26:25 +08:00
func httptestRequest ( t * testing . T , method , path string , body any , token string ) * http . Request {
t . Helper ( )
payload , err := json . Marshal ( body )
if err != nil {
t . Fatalf ( "json.Marshal() error = %v" , err )
}
request , err := http . NewRequest ( method , path , bytes . NewReader ( payload ) )
if err != nil {
t . Fatalf ( "http.NewRequest() error = %v" , err )
}
request . Header . Set ( "Content-Type" , "application/json" )
if token != "" {
request . Header . Set ( "Authorization" , "Bearer " + token )
}
return request
}
func httptestRecorder ( handler http . Handler , request * http . Request ) * responseRecorder {
recorder := & responseRecorder { header : make ( http . Header ) }
handler . ServeHTTP ( recorder , request )
return recorder
}
type responseRecorder struct {
header http . Header
body bytes . Buffer
code int
}
func ( r * responseRecorder ) Header ( ) http . Header { return r . header }
func ( r * responseRecorder ) Write ( body [ ] byte ) ( int , error ) { return r . body . Write ( body ) }
func ( r * responseRecorder ) WriteHeader ( statusCode int ) { r . code = statusCode }
func ( r * responseRecorder ) Body ( ) * bytes . Buffer { return & r . body }
func assertStatusCode ( t * testing . T , recorder * responseRecorder , want int ) {
t . Helper ( )
if recorder . code != want {
t . Fatalf ( "status code = %d, want %d; body=%s" , recorder . code , want , recorder . body . String ( ) )
}
}
func TestServerAddrReturnsConfiguredAddress ( t * testing . T ) {
server := NewServer ( "127.0.0.1:9999" , nil , nil )
if got := server . Addr ( ) ; got != "127.0.0.1:9999" {
t . Fatalf ( "Addr() = %q, want %q" , got , "127.0.0.1:9999" )
}
}
func TestClassifyError ( t * testing . T ) {
tests := [ ] struct {
name string
err error
wantStatusCode int
wantCode string
wantUpstream int
} {
{ name : "nil" , err : nil } ,
{ name : "http error passthrough" , err : & httpError { StatusCode : http . StatusTeapot , Code : "teapot" , Message : "brew" } , wantStatusCode : http . StatusTeapot , wantCode : "teapot" } ,
{ name : "upstream error" , err : & sub2api . HTTPError { Method : http . MethodGet , Path : "/x" , StatusCode : http . StatusForbidden , Body : "nope" } , wantStatusCode : http . StatusBadGateway , wantCode : "host_request_failed" , wantUpstream : http . StatusForbidden } ,
{ name : "pack conflict already installed" , err : errors . New ( "pack already installed" ) , wantStatusCode : http . StatusConflict , wantCode : "pack_conflict" } ,
{ name : "pack conflict checksum drift" , err : errors . New ( "checksum drift detected" ) , wantStatusCode : http . StatusConflict , wantCode : "pack_conflict" } ,
2026-05-18 22:22:22 +08:00
{ name : "reconcile blocked rolled_back" , err : errors . New ( "latest import batch is rolled_back; run import again before reconcile" ) , wantStatusCode : http . StatusConflict , wantCode : "batch_not_reconcilable" } ,
{ name : "not found generic" , err : errors . New ( "host x not found" ) , wantStatusCode : http . StatusNotFound , wantCode : "not_found" } ,
2026-05-15 19:26:25 +08:00
{ name : "provider not found" , err : errors . New ( "provider \"deepseek\" not found in pack \"openai\"" ) , wantStatusCode : http . StatusBadRequest , wantCode : "provider_not_found" } ,
{ name : "bad request pack path" , err : errors . New ( "pack path is required" ) , wantStatusCode : http . StatusBadRequest , wantCode : "bad_request" } ,
{ name : "bad request decode" , err : errors . New ( "decode pack.json failed" ) , wantStatusCode : http . StatusBadRequest , wantCode : "bad_request" } ,
{ name : "internal error" , err : errors . New ( "boom" ) , wantStatusCode : http . StatusInternalServerError , wantCode : "internal_error" } ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
got := classifyError ( tt . err )
if tt . err == nil {
if got != nil {
t . Fatalf ( "classifyError(nil) = %#v, want nil" , got )
}
return
}
if got == nil {
t . Fatal ( "classifyError() = nil, want error" )
}
if got . StatusCode != tt . wantStatusCode {
t . Fatalf ( "StatusCode = %d, want %d" , got . StatusCode , tt . wantStatusCode )
}
if got . Code != tt . wantCode {
t . Fatalf ( "Code = %q, want %q" , got . Code , tt . wantCode )
}
if got . UpstreamStatus != tt . wantUpstream {
t . Fatalf ( "UpstreamStatus = %d, want %d" , got . UpstreamStatus , tt . wantUpstream )
}
} )
}
}
func TestWriteHTTPError ( t * testing . T ) {
t . Run ( "default error when nil" , func ( t * testing . T ) {
recorder := & responseRecorder { header : make ( http . Header ) }
writeHTTPError ( recorder , nil )
assertStatusCode ( t , recorder , http . StatusInternalServerError )
if got := recorder . Header ( ) . Get ( "Content-Type" ) ; got != "application/json" {
t . Fatalf ( "Content-Type = %q, want application/json" , got )
}
assertJSONContains ( t , recorder . Body ( ) . Bytes ( ) , "error.code" , "internal_error" )
assertJSONContains ( t , recorder . Body ( ) . Bytes ( ) , "error.message" , "internal server error" )
} )
t . Run ( "writes provided error" , func ( t * testing . T ) {
recorder := & responseRecorder { header : make ( http . Header ) }
writeHTTPError ( recorder , & httpError { StatusCode : http . StatusBadRequest , Code : "bad_request" , Message : "invalid input" , UpstreamStatus : http . StatusConflict } )
assertStatusCode ( t , recorder , http . StatusBadRequest )
assertJSONContains ( t , recorder . Body ( ) . Bytes ( ) , "error.code" , "bad_request" )
assertJSONContains ( t , recorder . Body ( ) . Bytes ( ) , "error.upstream_status" , float64 ( http . StatusConflict ) )
} )
}
func TestDecodeJSON ( t * testing . T ) {
t . Run ( "success" , func ( t * testing . T ) {
request := httptest . NewRequest ( http . MethodPost , "/" , strings . NewReader ( ` { "host_base_url":"https://example.com","pack_path":"/tmp/pack.zip"} ` ) )
var got InstallPackRequest
if err := decodeJSON ( request , & got ) ; err != nil {
t . Fatalf ( "decodeJSON() error = %v, want nil" , err )
}
if got . HostBaseURL != "https://example.com" || got . PackPath != "/tmp/pack.zip" {
t . Fatalf ( "decoded request = %#v, want expected fields" , got )
}
} )
t . Run ( "rejects unknown fields" , func ( t * testing . T ) {
request := httptest . NewRequest ( http . MethodPost , "/" , strings . NewReader ( ` { "host_base_url":"https://example.com","unknown":true} ` ) )
var got InstallPackRequest
err := decodeJSON ( request , & got )
if err == nil {
t . Fatal ( "decodeJSON() error = nil, want error" )
}
if err . StatusCode != http . StatusBadRequest || err . Code != "bad_request" {
t . Fatalf ( "decodeJSON() = %#v, want bad_request" , err )
}
if ! strings . Contains ( err . Message , "unknown field" ) {
t . Fatalf ( "Message = %q, want unknown field" , err . Message )
}
} )
t . Run ( "rejects trailing non-object payload" , func ( t * testing . T ) {
request := httptest . NewRequest ( http . MethodPost , "/" , strings . NewReader ( ` { "host_base_url":"https://example.com"}[] ` ) )
var got InstallPackRequest
err := decodeJSON ( request , & got )
if err == nil {
t . Fatal ( "decodeJSON() error = nil, want error" )
}
if err . Message != "request body must contain a single JSON object" {
t . Fatalf ( "Message = %q, want single object error" , err . Message )
}
} )
}
func TestWriteJSON ( t * testing . T ) {
recorder := & responseRecorder { header : make ( http . Header ) }
writeJSON ( recorder , http . StatusCreated , map [ string ] any { "ok" : true , "count" : 2 } )
assertStatusCode ( t , recorder , http . StatusCreated )
if got := recorder . Header ( ) . Get ( "Content-Type" ) ; got != "application/json" {
t . Fatalf ( "Content-Type = %q, want application/json" , got )
}
assertJSONContains ( t , recorder . Body ( ) . Bytes ( ) , "ok" , true )
assertJSONContains ( t , recorder . Body ( ) . Bytes ( ) , "count" , float64 ( 2 ) )
}
func TestFindProvider ( t * testing . T ) {
loaded := pack . LoadedPack {
Manifest : pack . Manifest { PackID : "openai-cn-pack" } ,
Providers : [ ] pack . ProviderManifest {
{ ProviderID : "deepseek" , DisplayName : "DeepSeek" } ,
{ ProviderID : "openai" , DisplayName : "OpenAI" } ,
} ,
}
provider , err := findProvider ( loaded , " deepseek " )
if err != nil {
t . Fatalf ( "findProvider() error = %v, want nil" , err )
}
if provider . ProviderID != "deepseek" {
t . Fatalf ( "ProviderID = %q, want deepseek" , provider . ProviderID )
}
_ , err = findProvider ( loaded , "missing" )
if err == nil {
t . Fatal ( "findProvider() error = nil, want error" )
}
if ! strings . Contains ( err . Error ( ) , ` provider "missing" not found in pack "openai-cn-pack" ` ) {
t . Fatalf ( "findProvider() error = %v, want provider not found message" , err )
}
}
func TestAPIRequiresConfiguredAdminToken ( t * testing . T ) {
handler := NewAPIHandler ( "" , ActionSet { } )
request := httptestRequest ( t , http . MethodPost , "/api/packs/install" , map [ string ] any { "host_base_url" : "https://sub2api.example.com" } , "any-token" )
response := httptestRecorder ( handler , request )
assertStatusCode ( t , response , http . StatusInternalServerError )
assertJSONContains ( t , response . Body ( ) . Bytes ( ) , "error.code" , "server_misconfigured" )
}
func TestAPIBatchDetailRejectsInvalidBatchID ( t * testing . T ) {
handler := NewAPIHandler ( "secret-token" , ActionSet { BatchDetail : func ( context . Context , BatchDetailRequest ) ( provision . BatchDetailResult , error ) {
t . Fatal ( "BatchDetail should not be called for invalid batch id" )
return provision . BatchDetailResult { } , nil
} } )
request := httptestRequest ( t , http . MethodGet , "/api/import-batches/not-a-number" , nil , "secret-token" )
response := httptestRecorder ( handler , request )
assertStatusCode ( t , response , http . StatusBadRequest )
assertJSONContains ( t , response . Body ( ) . Bytes ( ) , "error.code" , "bad_request" )
assertJSONContains ( t , response . Body ( ) . Bytes ( ) , "error.message" , "batch_id must be a positive integer" )
}
func TestAPIInstallPackRejectsInvalidJSON ( t * testing . T ) {
handler := NewAPIHandler ( "secret-token" , ActionSet { InstallPack : func ( context . Context , InstallPackRequest ) ( provision . PackInstallResult , error ) {
t . Fatal ( "InstallPack should not be called for invalid JSON" )
return provision . PackInstallResult { } , nil
} } )
request , err := http . NewRequest ( http . MethodPost , "/api/packs/install" , strings . NewReader ( ` { "host_base_url":"https://sub2api.example.com","unknown":true} ` ) )
if err != nil {
t . Fatalf ( "http.NewRequest() error = %v" , err )
}
request . Header . Set ( "Authorization" , "Bearer secret-token" )
request . Header . Set ( "Content-Type" , "application/json" )
response := httptestRecorder ( handler , request )
assertStatusCode ( t , response , http . StatusBadRequest )
assertJSONContains ( t , response . Body ( ) . Bytes ( ) , "error.code" , "bad_request" )
}
func TestAPIImportProviderReturnsClassifiedErrorWithoutBatch ( t * testing . T ) {
handler := NewAPIHandler ( "secret-token" , ActionSet {
ImportProvider : func ( context . Context , ImportProviderRequest ) ( provision . RuntimeImportResult , error ) {
return provision . RuntimeImportResult { } , errors . New ( "pack path is required" )
} ,
} )
request := httptestRequest ( t , http . MethodPost , "/api/providers/deepseek/import" , map [ string ] any { "host_base_url" : "https://sub2api.example.com" , "pack_path" : "/tmp/openai-pack.zip" } , "secret-token" )
response := httptestRecorder ( handler , request )
assertStatusCode ( t , response , http . StatusBadRequest )
assertJSONContains ( t , response . Body ( ) . Bytes ( ) , "error.code" , "bad_request" )
assertJSONContains ( t , response . Body ( ) . Bytes ( ) , "batch_id" , float64 ( 0 ) )
}
func TestAPIPreviewProviderReturnsUpstreamError ( t * testing . T ) {
handler := NewAPIHandler ( "secret-token" , ActionSet {
PreviewProvider : func ( context . Context , PreviewProviderRequest ) ( provision . PreviewReport , error ) {
return provision . PreviewReport { } , & sub2api . HTTPError { Method : http . MethodPost , Path : "/preview" , StatusCode : http . StatusTooManyRequests , Body : "rate limited" }
} ,
} )
request := httptestRequest ( t , http . MethodPost , "/api/providers/deepseek/preview-import" , map [ string ] any { "host_base_url" : "https://sub2api.example.com" , "pack_path" : "/tmp/openai-pack.zip" } , "secret-token" )
response := httptestRecorder ( handler , request )
assertStatusCode ( t , response , http . StatusBadGateway )
assertJSONContains ( t , response . Body ( ) . Bytes ( ) , "error.code" , "host_request_failed" )
assertJSONContains ( t , response . Body ( ) . Bytes ( ) , "error.upstream_status" , float64 ( http . StatusTooManyRequests ) )
}
func TestAPIRollbackProviderReturnsConfiguredError ( t * testing . T ) {
handler := NewAPIHandler ( "secret-token" , ActionSet {
RollbackProvider : func ( context . Context , RollbackProviderRequest ) ( provision . RollbackReport , error ) {
return provision . RollbackReport { } , & httpError { StatusCode : http . StatusGone , Code : "rolled_back" , Message : "already removed" }
} ,
} )
request := httptestRequest ( t , http . MethodPost , "/api/providers/deepseek/rollback" , map [ string ] any { "host_base_url" : "https://sub2api.example.com" , "pack_path" : "/tmp/openai-pack.zip" } , "secret-token" )
response := httptestRecorder ( handler , request )
assertStatusCode ( t , response , http . StatusGone )
assertJSONContains ( t , response . Body ( ) . Bytes ( ) , "error.code" , "rolled_back" )
}
func TestAPIReconcileProviderRejectsTrailingNonObjectPayload ( t * testing . T ) {
handler := NewAPIHandler ( "secret-token" , ActionSet { ReconcileProvider : func ( context . Context , ReconcileProviderRequest ) ( provision . ReconcileResult , error ) {
t . Fatal ( "ReconcileProvider should not be called for invalid JSON" )
return provision . ReconcileResult { } , nil
} } )
request , err := http . NewRequest ( http . MethodPost , "/api/providers/deepseek/reconcile" , strings . NewReader ( ` { "host_base_url":"https://sub2api.example.com"}[] ` ) )
if err != nil {
t . Fatalf ( "http.NewRequest() error = %v" , err )
}
request . Header . Set ( "Authorization" , "Bearer secret-token" )
request . Header . Set ( "Content-Type" , "application/json" )
response := httptestRecorder ( handler , request )
assertStatusCode ( t , response , http . StatusBadRequest )
assertJSONContains ( t , response . Body ( ) . Bytes ( ) , "error.message" , "request body must contain a single JSON object" )
}
// --- Coverage edge cases ---
func TestHTTPErrorError ( t * testing . T ) {
e := & httpError { StatusCode : http . StatusTeapot , Code : "teapot" , Message : "i'm a teapot" }
if got := e . Error ( ) ; got != "i'm a teapot" {
t . Fatalf ( "httpError.Error() = %q, want %q" , got , "i'm a teapot" )
}
}
func TestProviderStatusFnNil ( t * testing . T ) {
handler := NewAPIHandler ( "t" , ActionSet { } )
req := httptestRequest ( t , http . MethodGet , "/api/providers/x/status" , nil , "t" )
res := httptestRecorder ( handler , req )
assertStatusCode ( t , res , http . StatusInternalServerError )
assertJSONContains ( t , res . Body ( ) . Bytes ( ) , "error.code" , "server_misconfigured" )
}
func TestProviderAccessStatusFnNil ( t * testing . T ) {
handler := NewAPIHandler ( "t" , ActionSet { } )
req := httptestRequest ( t , http . MethodGet , "/api/providers/x/access/status" , nil , "t" )
res := httptestRecorder ( handler , req )
assertStatusCode ( t , res , http . StatusInternalServerError )
assertJSONContains ( t , res . Body ( ) . Bytes ( ) , "error.code" , "server_misconfigured" )
}
func TestProviderResourcesFnNil ( t * testing . T ) {
handler := NewAPIHandler ( "t" , ActionSet { } )
req := httptestRequest ( t , http . MethodGet , "/api/providers/x/resources" , nil , "t" )
res := httptestRecorder ( handler , req )
assertStatusCode ( t , res , http . StatusInternalServerError )
assertJSONContains ( t , res . Body ( ) . Bytes ( ) , "error.code" , "server_misconfigured" )
}
func TestProviderStatusReturnsError ( t * testing . T ) {
handler := NewAPIHandler ( "t" , ActionSet {
GetProviderStatus : func ( context . Context , ProviderQueryRequest ) ( provision . ProviderSnapshot , error ) {
return provision . ProviderSnapshot { } , errors . New ( ` provider "x" not found in pack "p" ` )
} ,
} )
req := httptestRequest ( t , http . MethodGet , "/api/providers/x/status" , nil , "t" )
res := httptestRecorder ( handler , req )
assertStatusCode ( t , res , http . StatusBadRequest )
assertJSONContains ( t , res . Body ( ) . Bytes ( ) , "error.code" , "provider_not_found" )
}
func TestPostHandlersFnNil ( t * testing . T ) {
tests := [ ] struct {
name string
method string
path string
body string
} {
{ name : "install-pack" , method : http . MethodPost , path : "/api/packs/install" , body : ` { } ` } ,
{ name : "preview" , method : http . MethodPost , path : "/api/providers/x/preview-import" , body : ` { } ` } ,
{ name : "import" , method : http . MethodPost , path : "/api/providers/x/import" , body : ` { } ` } ,
{ name : "rollback" , method : http . MethodPost , path : "/api/providers/x/rollback" , body : ` { } ` } ,
{ name : "reconcile" , method : http . MethodPost , path : "/api/providers/x/reconcile" , body : ` { } ` } ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
handler := NewAPIHandler ( "t" , ActionSet { } )
req , _ := http . NewRequest ( tt . method , tt . path , strings . NewReader ( tt . body ) )
req . Header . Set ( "Authorization" , "Bearer t" )
req . Header . Set ( "Content-Type" , "application/json" )
res := httptestRecorder ( handler , req )
assertStatusCode ( t , res , http . StatusInternalServerError )
assertJSONContains ( t , res . Body ( ) . Bytes ( ) , "error.code" , "server_misconfigured" )
} )
}
}
2026-05-18 22:22:22 +08:00
func TestGetHandlersFnNil ( t * testing . T ) {
tests := [ ] struct {
name string
path string
} {
{ name : "list-hosts" , path : "/api/hosts" } ,
{ name : "get-host" , path : "/api/hosts/my-host" } ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
handler := NewAPIHandler ( "t" , ActionSet { } )
req , _ := http . NewRequest ( http . MethodGet , tt . path , nil )
req . Header . Set ( "Authorization" , "Bearer t" )
res := httptestRecorder ( handler , req )
assertStatusCode ( t , res , http . StatusInternalServerError )
assertJSONContains ( t , res . Body ( ) . Bytes ( ) , "error.code" , "server_misconfigured" )
} )
}
}
func TestDeleteHandlersFnNil ( t * testing . T ) {
tests := [ ] struct {
name string
path string
} {
{ name : "delete-host" , path : "/api/hosts/my-host" } ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
handler := NewAPIHandler ( "t" , ActionSet { } )
req , _ := http . NewRequest ( http . MethodDelete , tt . path , nil )
req . Header . Set ( "Authorization" , "Bearer t" )
res := httptestRecorder ( handler , req )
assertStatusCode ( t , res , http . StatusInternalServerError )
assertJSONContains ( t , res . Body ( ) . Bytes ( ) , "error.code" , "server_misconfigured" )
} )
}
}
2026-05-15 19:26:25 +08:00
func TestHandlerErrorPaths ( t * testing . T ) {
tests := [ ] struct {
name string
method string
path string
body string
actionSet ActionSet
wantStatus int
wantCode string
} {
{
2026-05-18 22:22:22 +08:00
name : "access-status-error" ,
2026-05-15 19:26:25 +08:00
method : http . MethodGet ,
2026-05-18 22:22:22 +08:00
path : "/api/providers/x/access/status" ,
2026-05-15 19:26:25 +08:00
actionSet : ActionSet {
GetProviderAccessStatus : func ( context . Context , ProviderQueryRequest ) ( provision . ProviderSnapshot , error ) {
return provision . ProviderSnapshot { } , errors . New ( "boom" )
} ,
} ,
wantStatus : http . StatusInternalServerError ,
wantCode : "internal_error" ,
} ,
{
2026-05-18 22:22:22 +08:00
name : "preview-error" ,
2026-05-15 19:26:25 +08:00
method : http . MethodPost ,
2026-05-18 22:22:22 +08:00
path : "/api/providers/x/preview-import" ,
body : ` { } ` ,
2026-05-15 19:26:25 +08:00
actionSet : ActionSet {
PreviewProvider : func ( context . Context , PreviewProviderRequest ) ( provision . PreviewReport , error ) {
return provision . PreviewReport { } , errors . New ( "boom" )
} ,
} ,
wantStatus : http . StatusInternalServerError ,
wantCode : "internal_error" ,
} ,
{
2026-05-18 22:22:22 +08:00
name : "rollback-error" ,
2026-05-15 19:26:25 +08:00
method : http . MethodPost ,
2026-05-18 22:22:22 +08:00
path : "/api/providers/x/rollback" ,
body : ` { } ` ,
2026-05-15 19:26:25 +08:00
actionSet : ActionSet {
RollbackProvider : func ( context . Context , RollbackProviderRequest ) ( provision . RollbackReport , error ) {
return provision . RollbackReport { } , errors . New ( "boom" )
} ,
} ,
wantStatus : http . StatusInternalServerError ,
wantCode : "internal_error" ,
} ,
{
2026-05-18 22:22:22 +08:00
name : "reconcile-error" ,
2026-05-15 19:26:25 +08:00
method : http . MethodPost ,
2026-05-18 22:22:22 +08:00
path : "/api/providers/x/reconcile" ,
body : ` { } ` ,
2026-05-15 19:26:25 +08:00
actionSet : ActionSet {
ReconcileProvider : func ( context . Context , ReconcileProviderRequest ) ( provision . ReconcileResult , error ) {
return provision . ReconcileResult { } , errors . New ( "boom" )
} ,
} ,
wantStatus : http . StatusInternalServerError ,
wantCode : "internal_error" ,
} ,
2026-05-18 22:22:22 +08:00
{
name : "list-hosts-error" ,
method : http . MethodGet ,
path : "/api/hosts" ,
actionSet : ActionSet {
ListHosts : func ( context . Context ) ( [ ] HostInfo , error ) {
return nil , errors . New ( "boom" )
} ,
} ,
wantStatus : http . StatusInternalServerError ,
wantCode : "internal_error" ,
} ,
{
name : "get-host-error" ,
method : http . MethodGet ,
path : "/api/hosts/my-host" ,
actionSet : ActionSet {
GetHost : func ( context . Context , string ) ( HostInfo , error ) {
return HostInfo { } , errors . New ( "boom" )
} ,
} ,
wantStatus : http . StatusInternalServerError ,
wantCode : "internal_error" ,
} ,
{
name : "get-host-not-found" ,
method : http . MethodGet ,
path : "/api/hosts/unknown" ,
actionSet : ActionSet {
GetHost : func ( context . Context , string ) ( HostInfo , error ) {
return HostInfo { } , errors . New ( "host unknown not found" )
} ,
} ,
wantStatus : http . StatusNotFound ,
wantCode : "not_found" ,
} ,
{
name : "delete-host-error" ,
method : http . MethodDelete ,
path : "/api/hosts/my-host" ,
actionSet : ActionSet {
DeleteHost : func ( context . Context , string ) error {
return errors . New ( "boom" )
} ,
} ,
wantStatus : http . StatusInternalServerError ,
wantCode : "internal_error" ,
} ,
{
name : "delete-host-not-found" ,
method : http . MethodDelete ,
path : "/api/hosts/unknown" ,
actionSet : ActionSet {
DeleteHost : func ( context . Context , string ) error {
return errors . New ( "host unknown not found" )
} ,
} ,
wantStatus : http . StatusNotFound ,
wantCode : "not_found" ,
} ,
2026-05-15 19:26:25 +08:00
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
handler := NewAPIHandler ( "t" , tt . actionSet )
var req * http . Request
if tt . body != "" {
req , _ = http . NewRequest ( tt . method , tt . path , strings . NewReader ( tt . body ) )
req . Header . Set ( "Content-Type" , "application/json" )
} else {
var err error
req , err = http . NewRequest ( tt . method , tt . path , nil )
if err != nil {
t . Fatal ( err )
}
}
req . Header . Set ( "Authorization" , "Bearer t" )
res := httptestRecorder ( handler , req )
assertStatusCode ( t , res , tt . wantStatus )
assertJSONContains ( t , res . Body ( ) . Bytes ( ) , "error.code" , tt . wantCode )
} )
}
}
2026-05-20 22:09:40 +08:00
func TestResolveLatestAccessStatusAggregatesAcrossModeBatches ( t * testing . T ) {
store := openAppTestStore ( t )
defer closeAppTestStore ( t , store )
ctx := context . Background ( )
hostID , err := store . Hosts ( ) . Create ( ctx , sqlite . Host { HostID : "host-1" , BaseURL : "https://sub2api.example.com" , HostVersion : "0.1.126" , AuthType : "apikey" , AuthToken : "token" } )
if err != nil {
t . Fatalf ( "Hosts().Create() error = %v" , err )
}
packID , err := store . Packs ( ) . Create ( ctx , sqlite . Pack { PackID : "openai-cn-pack" , Version : "1.0.0" , TargetHost : "sub2api" , Checksum : "checksum-1" } )
if err != nil {
t . Fatalf ( "Packs().Create() error = %v" , err )
}
providerID , err := store . Providers ( ) . Create ( ctx , sqlite . Provider { PackID : packID , ProviderID : "deepseek" , DisplayName : "DeepSeek" , BaseURL : "https://api.deepseek.com" , Platform : "openai" } )
if err != nil {
t . Fatalf ( "Providers().Create() error = %v" , err )
}
batchSubscription , err := store . ImportBatches ( ) . Create ( ctx , sqlite . ImportBatch { HostID : hostID , PackID : packID , ProviderID : providerID , Mode : provision . ImportModePartial , BatchStatus : provision . BatchStatusSucceeded , AccessStatus : provision . AccessStatusSubscriptionReady } )
if err != nil {
t . Fatalf ( "ImportBatches().Create(subscription) error = %v" , err )
}
if _ , err := store . AccessClosures ( ) . Create ( ctx , sqlite . AccessClosureRecord { BatchID : batchSubscription , ClosureType : provision . AccessModeSubscription , Status : provision . AccessStatusSubscriptionReady , DetailsJSON : "{}" } ) ; err != nil {
t . Fatalf ( "AccessClosures().Create(subscription) error = %v" , err )
}
batchSelf , err := store . ImportBatches ( ) . Create ( ctx , sqlite . ImportBatch { HostID : hostID , PackID : packID , ProviderID : providerID , Mode : provision . ImportModePartial , BatchStatus : provision . BatchStatusSucceeded , AccessStatus : provision . AccessStatusSelfServiceReady } )
if err != nil {
t . Fatalf ( "ImportBatches().Create(self_service) error = %v" , err )
}
if _ , err := store . AccessClosures ( ) . Create ( ctx , sqlite . AccessClosureRecord { BatchID : batchSelf , ClosureType : provision . AccessModeSelfService , Status : provision . AccessStatusSelfServiceReady , DetailsJSON : "{}" } ) ; err != nil {
t . Fatalf ( "AccessClosures().Create(self_service) error = %v" , err )
}
got , err := resolveLatestAccessStatus ( ctx , store , sqlite . Provider { ID : providerID , ProviderID : "deepseek" } , "host-1" )
if err != nil {
t . Fatalf ( "resolveLatestAccessStatus() error = %v" , err )
}
if got != provision . AccessStatusFullyReady {
t . Fatalf ( "resolveLatestAccessStatus() = %q, want %q" , got , provision . AccessStatusFullyReady )
}
}
2026-05-15 19:26:25 +08:00
func TestProviderAccessStatusMultipleClosures ( t * testing . T ) {
handler := NewAPIHandler ( "t" , ActionSet {
GetProviderAccessStatus : func ( context . Context , ProviderQueryRequest ) ( provision . ProviderSnapshot , error ) {
return provision . ProviderSnapshot {
Pack : sqlite . Pack { PackID : "p" } ,
Provider : sqlite . Provider { ProviderID : "dp" } ,
Batch : sqlite . ImportBatch { ID : 1 } ,
LatestAccessStatus : "ready" ,
AccessClosures : [ ] sqlite . AccessClosureRecord {
{ ID : 1 , ClosureType : "preview" , Status : "done" , DetailsJSON : ` { "v":1} ` } ,
{ ID : 2 , ClosureType : "self_service" , Status : "active" , DetailsJSON : ` { "v":2} ` } ,
} ,
} , nil
} ,
} )
req := httptestRequest ( t , http . MethodGet , "/api/providers/dp/access/status" , nil , "t" )
res := httptestRecorder ( handler , req )
assertStatusCode ( t , res , http . StatusOK )
// Should report the last closure (index n-1)
if ! strings . Contains ( res . Body ( ) . String ( ) , ` "closure_type":"self_service" ` ) {
t . Fatalf ( "expected latest closure to be self_service, got: %s" , res . Body ( ) . String ( ) )
}
}
2026-05-18 22:22:22 +08:00
func TestAccessStatusSupportsMode ( t * testing . T ) {
tests := [ ] struct {
name string
status string
mode string
want bool
} {
{ name : "subscription ready supports subscription" , status : provision . AccessStatusSubscriptionReady , mode : provision . AccessModeSubscription , want : true } ,
{ name : "subscription ready does not support self service" , status : provision . AccessStatusSubscriptionReady , mode : provision . AccessModeSelfService , want : false } ,
{ name : "fully ready supports self service" , status : provision . AccessStatusFullyReady , mode : provision . AccessModeSelfService , want : true } ,
{ name : "broken does not support any" , status : provision . AccessStatusBroken , mode : "" , want : false } ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
if got := accessStatusSupportsMode ( tt . status , tt . mode ) ; got != tt . want {
t . Fatalf ( "accessStatusSupportsMode(%q, %q) = %v, want %v" , tt . status , tt . mode , got , tt . want )
}
} )
}
}
func TestHostSupportStatusRequiresPlansCapability ( t * testing . T ) {
status := hostSupportStatus ( sub2api . HostCapabilities { Groups : true , Channels : true , Plans : false , Accounts : true , AccountTest : true , AccountModels : true , Subscriptions : true } )
if status != "unsupported" {
t . Fatalf ( "hostSupportStatus() = %q, want unsupported when plans capability is missing" , status )
}
}
2026-05-20 22:09:40 +08:00
func openAppTestStore ( t * testing . T ) * sqlite . DB {
t . Helper ( )
dbPath := filepath . Join ( t . TempDir ( ) , "state.db" )
dsn := fmt . Sprintf ( "file:%s?_busy_timeout=5000&_pragma=foreign_keys(0)" , filepath . ToSlash ( dbPath ) )
store , err := sqlite . Open ( context . Background ( ) , dsn )
if err != nil {
t . Fatalf ( "sqlite.Open() error = %v" , err )
}
return store
}
func closeAppTestStore ( t * testing . T , store * sqlite . DB ) {
t . Helper ( )
if err := store . Close ( ) ; err != nil {
t . Fatalf ( "store.Close() error = %v" , err )
}
}
2026-05-15 19:26:25 +08:00
func assertJSONContains ( t * testing . T , payload [ ] byte , key string , want any ) {
t . Helper ( )
var decoded map [ string ] any
if err := json . Unmarshal ( payload , & decoded ) ; err != nil {
t . Fatalf ( "json.Unmarshal() error = %v; payload=%s" , err , string ( payload ) )
}
if strings . Contains ( key , "." ) {
parts := strings . Split ( key , "." )
current := any ( decoded )
for _ , part := range parts {
object , ok := current . ( map [ string ] any )
if ! ok {
t . Fatalf ( "key %q not found in payload %s" , key , string ( payload ) )
}
current = object [ part ]
}
if current != want {
t . Fatalf ( "json key %q = %#v, want %#v; payload=%s" , key , current , want , string ( payload ) )
}
return
}
if decoded [ key ] != want {
t . Fatalf ( "json key %q = %#v, want %#v; payload=%s" , key , decoded [ key ] , want , string ( payload ) )
}
}
func TestNewActionSetReturnsNonNil ( t * testing . T ) {
as := NewActionSet ( "file::memory:?cache=shared" )
t . Run ( "InstallPack" , func ( t * testing . T ) {
if as . InstallPack == nil {
t . Fatal ( "is nil" )
}
} )
t . Run ( "BatchDetail" , func ( t * testing . T ) {
if as . BatchDetail == nil {
t . Fatal ( "is nil" )
}
} )
t . Run ( "GetProviderStatus" , func ( t * testing . T ) {
if as . GetProviderStatus == nil {
t . Fatal ( "is nil" )
}
} )
t . Run ( "GetProviderResources" , func ( t * testing . T ) {
if as . GetProviderResources == nil {
t . Fatal ( "is nil" )
}
} )
t . Run ( "GetProviderAccessStatus" , func ( t * testing . T ) {
if as . GetProviderAccessStatus == nil {
t . Fatal ( "is nil" )
}
} )
t . Run ( "PreviewProvider" , func ( t * testing . T ) {
if as . PreviewProvider == nil {
t . Fatal ( "is nil" )
}
} )
t . Run ( "ImportProvider" , func ( t * testing . T ) {
if as . ImportProvider == nil {
t . Fatal ( "is nil" )
}
} )
t . Run ( "RollbackProvider" , func ( t * testing . T ) {
if as . RollbackProvider == nil {
t . Fatal ( "is nil" )
}
} )
t . Run ( "ReconcileProvider" , func ( t * testing . T ) {
if as . ReconcileProvider == nil {
t . Fatal ( "is nil" )
}
} )
2026-05-18 22:22:22 +08:00
t . Run ( "ListHosts" , func ( t * testing . T ) {
if as . ListHosts == nil {
t . Fatal ( "is nil" )
}
} )
t . Run ( "GetHost" , func ( t * testing . T ) {
if as . GetHost == nil {
t . Fatal ( "is nil" )
}
} )
t . Run ( "DeleteHost" , func ( t * testing . T ) {
if as . DeleteHost == nil {
t . Fatal ( "is nil" )
}
} )
t . Run ( "ProbeHost" , func ( t * testing . T ) {
if as . ProbeHost == nil {
t . Fatal ( "is nil" )
}
} )
t . Run ( "ListProviderImportBatches" , func ( t * testing . T ) {
if as . ListProviderImportBatches == nil {
t . Fatal ( "is nil" )
}
} )
t . Run ( "RollbackBatch" , func ( t * testing . T ) {
if as . RollbackBatch == nil {
t . Fatal ( "is nil" )
}
} )
2026-05-15 19:26:25 +08:00
}
func TestBatchDetailReturnsNotFoundForMissingBatch ( t * testing . T ) {
as := NewActionSet ( "file::memory:?cache=shared" )
_ , err := as . BatchDetail ( context . Background ( ) , BatchDetailRequest { BatchID : 999 } )
if err == nil {
t . Fatal ( "BatchDetail() error = nil for missing batch, want error" )
}
}
func TestNewActionSetSQLiteClosures ( t * testing . T ) {
dsn := "file::memory:?cache=shared"
as := NewActionSet ( dsn )
ctx := context . Background ( )
t . Run ( "GetProviderStatus on empty DB" , func ( t * testing . T ) {
_ , err := as . GetProviderStatus ( ctx , ProviderQueryRequest { ProviderID : "x" , PackID : "p" } )
if err == nil {
t . Fatal ( "expected error from empty DB, got nil" )
}
} )
t . Run ( "GetProviderResources on empty DB" , func ( t * testing . T ) {
_ , err := as . GetProviderResources ( ctx , ProviderQueryRequest { ProviderID : "x" , PackID : "p" } )
if err == nil {
t . Fatal ( "expected error from empty DB, got nil" )
}
} )
t . Run ( "GetProviderAccessStatus on empty DB" , func ( t * testing . T ) {
_ , err := as . GetProviderAccessStatus ( ctx , ProviderQueryRequest { ProviderID : "x" , PackID : "p" } )
if err == nil {
t . Fatal ( "expected error from empty DB, got nil" )
}
} )
2026-05-18 22:22:22 +08:00
t . Run ( "ListHosts on empty DB" , func ( t * testing . T ) {
hosts , err := as . ListHosts ( ctx )
if err != nil {
t . Fatalf ( "ListHosts() on empty DB error = %v, want nil" , err )
}
if len ( hosts ) != 0 {
t . Fatalf ( "ListHosts() len = %d, want 0" , len ( hosts ) )
}
} )
t . Run ( "GetHost on empty DB" , func ( t * testing . T ) {
_ , err := as . GetHost ( ctx , "nonexistent" )
if err == nil {
t . Fatal ( "expected error from empty DB, got nil" )
}
} )
t . Run ( "ListProviderImportBatches on empty DB" , func ( t * testing . T ) {
batches , err := as . ListProviderImportBatches ( ctx , ProviderQueryRequest { ProviderID : "x" } )
if err != nil {
t . Fatalf ( "ListProviderImportBatches() on empty DB error = %v, want nil" , err )
}
if len ( batches ) != 0 {
t . Fatalf ( "ListProviderImportBatches() len = %d, want 0" , len ( batches ) )
}
} )
}
func TestAPIProbeHostReturnsHostSnapshot ( t * testing . T ) {
handler := NewAPIHandler ( "secret-token" , ActionSet {
ProbeHost : func ( _ context . Context , req ProbeHostRequest ) ( HostInfo , error ) {
if req . HostID != "prod-sub2api" {
t . Fatalf ( "ProbeHost hostID = %q, want prod-sub2api" , req . HostID )
}
if req . Auth . Type != "bearer" || req . Auth . Token != "probe-token" {
t . Fatalf ( "ProbeHost auth = %#v, want bearer/probe-token" , req . Auth )
}
return HostInfo { HostID : req . HostID , BaseURL : "https://sub2api.example.com" , HostVersion : "0.1.126" , Status : "supported" } , nil
} ,
} )
req := httptestRequest ( t , http . MethodPost , "/api/hosts/prod-sub2api/probe" , map [ string ] any {
"auth" : map [ string ] any { "type" : "bearer" , "token" : "probe-token" } ,
} , "secret-token" )
res := httptestRecorder ( handler , req )
assertStatusCode ( t , res , http . StatusOK )
assertJSONContains ( t , res . Body ( ) . Bytes ( ) , "host_id" , "prod-sub2api" )
assertJSONContains ( t , res . Body ( ) . Bytes ( ) , "host_version" , "0.1.126" )
assertJSONContains ( t , res . Body ( ) . Bytes ( ) , "status" , "supported" )
}
func TestAPIListProviderImportBatchesReturnsItems ( t * testing . T ) {
handler := NewAPIHandler ( "secret-token" , ActionSet {
ListProviderImportBatches : func ( _ context . Context , req ProviderQueryRequest ) ( [ ] ImportBatchInfo , error ) {
if req . ProviderID != "deepseek" {
t . Fatalf ( "ListProviderImportBatches providerID = %q, want deepseek" , req . ProviderID )
}
return [ ] ImportBatchInfo { { BatchID : 7 , BatchStatus : provision . BatchStatusSucceeded , AccessStatus : provision . AccessStatusSelfServiceReady } } , nil
} ,
} )
req := httptestRequest ( t , http . MethodGet , "/api/providers/deepseek/import-batches" , nil , "secret-token" )
res := httptestRecorder ( handler , req )
assertStatusCode ( t , res , http . StatusOK )
body := res . Body ( ) . String ( )
if ! strings . Contains ( body , ` "batch_id":7 ` ) || ! strings . Contains ( body , ` "batch_status":"succeeded" ` ) || ! strings . Contains ( body , ` "access_status":"self_service_ready" ` ) {
t . Fatalf ( "unexpected import batch payload: %s" , body )
}
}
func TestAPIRollbackBatchReturnsSummary ( t * testing . T ) {
handler := NewAPIHandler ( "secret-token" , ActionSet {
RollbackBatch : func ( _ context . Context , req RollbackBatchRequest ) ( provision . RollbackReport , error ) {
if req . BatchID != 11 {
t . Fatalf ( "RollbackBatch batchID = %d, want 11" , req . BatchID )
}
if req . Auth . Type != "apikey" || req . Auth . Token != "admin-key" {
t . Fatalf ( "RollbackBatch auth = %#v, want apikey/admin-key" , req . Auth )
}
return provision . RollbackReport { AccountsDeleted : 2 , PlansDeleted : 1 , ChannelsDeleted : 1 , GroupsDeleted : 1 } , nil
} ,
} )
req := httptestRequest ( t , http . MethodPost , "/api/import-batches/11/rollback" , map [ string ] any {
"auth" : map [ string ] any { "type" : "apikey" , "token" : "admin-key" } ,
} , "secret-token" )
res := httptestRecorder ( handler , req )
assertStatusCode ( t , res , http . StatusOK )
assertJSONContains ( t , res . Body ( ) . Bytes ( ) , "deleted_accounts" , float64 ( 2 ) )
assertJSONContains ( t , res . Body ( ) . Bytes ( ) , "deleted_plans" , float64 ( 1 ) )
assertJSONContains ( t , res . Body ( ) . Bytes ( ) , "deleted_channels" , float64 ( 1 ) )
assertJSONContains ( t , res . Body ( ) . Bytes ( ) , "deleted_groups" , float64 ( 1 ) )
2026-05-15 19:26:25 +08:00
}
func TestNewActionSetPackErrorPaths ( t * testing . T ) {
dsn := "file::memory:?cache=shared"
as := NewActionSet ( dsn )
ctx := context . Background ( )
t . Run ( "InstallPack bad path" , func ( t * testing . T ) {
_ , err := as . InstallPack ( ctx , InstallPackRequest { PackPath : "/nonexistent/pack" } )
if err == nil {
t . Fatal ( "expected error from bad pack path" )
}
} )
t . Run ( "PreviewProvider bad path" , func ( t * testing . T ) {
_ , err := as . PreviewProvider ( ctx , PreviewProviderRequest { PackPath : "/nonexistent/pack" , ProviderID : "x" } )
if err == nil {
t . Fatal ( "expected error from bad pack path" )
}
} )
t . Run ( "ImportProvider bad path" , func ( t * testing . T ) {
_ , err := as . ImportProvider ( ctx , ImportProviderRequest { PackPath : "/nonexistent/pack" , ProviderID : "x" , HostBaseURL : "http://h:8080" } )
if err == nil {
t . Fatal ( "expected error from bad pack path" )
}
} )
t . Run ( "RollbackProvider bad path" , func ( t * testing . T ) {
_ , err := as . RollbackProvider ( ctx , RollbackProviderRequest { PackPath : "/nonexistent/pack" , ProviderID : "x" , HostBaseURL : "http://h:8080" } )
if err == nil {
t . Fatal ( "expected error from bad pack path" )
}
} )
t . Run ( "ReconcileProvider bad path" , func ( t * testing . T ) {
_ , err := as . ReconcileProvider ( ctx , ReconcileProviderRequest { PackPath : "/nonexistent/pack" , ProviderID : "x" , HostBaseURL : "http://h:8080" } )
if err == nil {
t . Fatal ( "expected error from bad pack path" )
}
} )
}