test(project): achieve ≥70% package coverage across all internal packages
- store/sqlite: 75.4% (repos + db coverage) - host/sub2api: 80.8% (httptest mock server, pure function tests) - app: 74.2% (handler error paths, NewActionSet closures) - pack: 72.4% - provision: 75.2% - access: 77.3% - config: 94.7% (lookup mock tests) All tests pass: build, vet, race, coverage gates.
This commit is contained in:
80
internal/access/closure.go
Normal file
80
internal/access/closure.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package access
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"sub2api-cn-relay-manager/internal/host/sub2api"
|
||||
)
|
||||
|
||||
const (
|
||||
ModeSubscription = "subscription"
|
||||
ModeSelfService = "self_service"
|
||||
)
|
||||
|
||||
type SubscriptionTarget struct {
|
||||
UserID string
|
||||
DurationDays int
|
||||
}
|
||||
|
||||
type ClosureRequest struct {
|
||||
Mode string
|
||||
ProbeAPIKey string
|
||||
Subscriptions []SubscriptionTarget
|
||||
GroupID string
|
||||
ExpectedModel string
|
||||
}
|
||||
|
||||
type Host interface {
|
||||
AssignSubscription(ctx context.Context, req sub2api.AssignSubscriptionRequest) (sub2api.SubscriptionRef, error)
|
||||
CheckGatewayAccess(ctx context.Context, req sub2api.GatewayAccessCheckRequest) (sub2api.GatewayAccessResult, error)
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
host Host
|
||||
}
|
||||
|
||||
func NewService(host Host) *Service {
|
||||
return &Service{host: host}
|
||||
}
|
||||
|
||||
func Validate(req ClosureRequest) error {
|
||||
switch strings.TrimSpace(req.Mode) {
|
||||
case ModeSubscription:
|
||||
if len(req.Subscriptions) == 0 {
|
||||
return fmt.Errorf("subscription access requires at least one subscription target")
|
||||
}
|
||||
case ModeSelfService:
|
||||
if strings.TrimSpace(req.ProbeAPIKey) == "" {
|
||||
return fmt.Errorf("self_service access requires probe api key")
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unsupported access mode %q", req.Mode)
|
||||
}
|
||||
if strings.TrimSpace(req.ProbeAPIKey) == "" {
|
||||
return fmt.Errorf("access probe api key is required to verify gateway closure")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) Close(ctx context.Context, req ClosureRequest) (sub2api.GatewayAccessResult, error) {
|
||||
if s == nil || s.host == nil {
|
||||
return sub2api.GatewayAccessResult{}, fmt.Errorf("access host is required")
|
||||
}
|
||||
if err := Validate(req); err != nil {
|
||||
return sub2api.GatewayAccessResult{}, err
|
||||
}
|
||||
if strings.TrimSpace(req.Mode) == ModeSubscription {
|
||||
for _, target := range req.Subscriptions {
|
||||
if _, err := s.host.AssignSubscription(ctx, sub2api.AssignSubscriptionRequest{UserID: target.UserID, GroupID: req.GroupID, DurationDays: target.DurationDays}); err != nil {
|
||||
return sub2api.GatewayAccessResult{}, fmt.Errorf("assign subscription for %s: %w", target.UserID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
result, err := s.host.CheckGatewayAccess(ctx, sub2api.GatewayAccessCheckRequest{APIKey: req.ProbeAPIKey, ExpectedModel: req.ExpectedModel})
|
||||
if err != nil {
|
||||
return sub2api.GatewayAccessResult{}, fmt.Errorf("check gateway access: %w", err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
91
internal/access/closure_test.go
Normal file
91
internal/access/closure_test.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package access
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"sub2api-cn-relay-manager/internal/host/sub2api"
|
||||
)
|
||||
|
||||
func TestValidateRejectsMissingProbeAPIKeyForSelfService(t *testing.T) {
|
||||
err := Validate(ClosureRequest{Mode: "self_service"})
|
||||
if err == nil {
|
||||
t.Fatal("Validate() error = nil, want validation error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRejectsMissingSubscriptionsForSubscriptionMode(t *testing.T) {
|
||||
err := Validate(ClosureRequest{Mode: "subscription", ProbeAPIKey: "user-key"})
|
||||
if err == nil {
|
||||
t.Fatal("Validate() error = nil, want validation error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceCloseAssignsSubscriptionsAndProbesGateway(t *testing.T) {
|
||||
host := &fakeClosureHost{
|
||||
gatewayResult: sub2api.GatewayAccessResult{OK: true, StatusCode: 200, HasExpectedModel: true, Models: []string{"deepseek-chat"}},
|
||||
}
|
||||
service := NewService(host)
|
||||
result, err := service.Close(context.Background(), ClosureRequest{
|
||||
Mode: "subscription",
|
||||
ProbeAPIKey: "user-key",
|
||||
GroupID: "group-1",
|
||||
ExpectedModel: "deepseek-chat",
|
||||
Subscriptions: []SubscriptionTarget{{UserID: "user-1", DurationDays: 30}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Close() error = %v", err)
|
||||
}
|
||||
if len(host.assigned) != 1 {
|
||||
t.Fatalf("assigned subscriptions = %d, want 1", len(host.assigned))
|
||||
}
|
||||
if host.gatewayProbe.APIKey != "user-key" || host.gatewayProbe.ExpectedModel != "deepseek-chat" {
|
||||
t.Fatalf("gateway probe = %+v, want api key + expected model", host.gatewayProbe)
|
||||
}
|
||||
if !result.OK || !result.HasExpectedModel {
|
||||
t.Fatalf("gateway result = %+v, want success", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceCloseReturnsSubscriptionErrorBeforeGatewayProbe(t *testing.T) {
|
||||
host := &fakeClosureHost{assignErr: errors.New("assign failed")}
|
||||
service := NewService(host)
|
||||
_, err := service.Close(context.Background(), ClosureRequest{
|
||||
Mode: "subscription",
|
||||
ProbeAPIKey: "user-key",
|
||||
GroupID: "group-1",
|
||||
ExpectedModel: "deepseek-chat",
|
||||
Subscriptions: []SubscriptionTarget{{UserID: "user-1", DurationDays: 30}},
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("Close() error = nil, want subscription failure")
|
||||
}
|
||||
if host.gatewayProbe.APIKey != "" {
|
||||
t.Fatalf("gateway probe should not run after subscription error, got %+v", host.gatewayProbe)
|
||||
}
|
||||
}
|
||||
|
||||
type fakeClosureHost struct {
|
||||
assigned []sub2api.AssignSubscriptionRequest
|
||||
assignErr error
|
||||
gatewayProbe sub2api.GatewayAccessCheckRequest
|
||||
gatewayResult sub2api.GatewayAccessResult
|
||||
gatewayErr error
|
||||
}
|
||||
|
||||
func (f *fakeClosureHost) AssignSubscription(_ context.Context, req sub2api.AssignSubscriptionRequest) (sub2api.SubscriptionRef, error) {
|
||||
if f.assignErr != nil {
|
||||
return sub2api.SubscriptionRef{}, f.assignErr
|
||||
}
|
||||
f.assigned = append(f.assigned, req)
|
||||
return sub2api.SubscriptionRef{ID: "sub-1"}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClosureHost) CheckGatewayAccess(_ context.Context, req sub2api.GatewayAccessCheckRequest) (sub2api.GatewayAccessResult, error) {
|
||||
f.gatewayProbe = req
|
||||
if f.gatewayErr != nil {
|
||||
return sub2api.GatewayAccessResult{}, f.gatewayErr
|
||||
}
|
||||
return f.gatewayResult, nil
|
||||
}
|
||||
Reference in New Issue
Block a user