Files
2026-07-30 12:23:05 +08:00

206 lines
6.6 KiB
Go

package service
import (
"context"
"crypto/x509"
"encoding/pem"
"errors"
"strings"
"testing"
"time"
"oci-portal/internal/model"
"oci-portal/internal/oci"
)
// apiKeyClient 记录 API key 相关调用的 fake。
type apiKeyClient struct {
oci.Client
keys []oci.TenantUserApiKey
uploadFp string
uploadErr error
uploadedPub string
deleted []string
validateErr error
validated []string // ValidateKey 收到的指纹序列
validatedUser string // ValidateKey 最近一次使用的签名用户
}
func (f *apiKeyClient) ListTenantUserApiKeys(ctx context.Context, cred oci.Credentials, homeRegion, userID string) ([]oci.TenantUserApiKey, error) {
return f.keys, nil
}
func (f *apiKeyClient) UploadTenantUserApiKey(ctx context.Context, cred oci.Credentials, homeRegion, userID, publicKeyPEM string) (string, error) {
if f.uploadErr != nil {
return "", f.uploadErr
}
f.uploadedPub = publicKeyPEM
return f.uploadFp, nil
}
func (f *apiKeyClient) DeleteTenantUserApiKey(ctx context.Context, cred oci.Credentials, homeRegion, userID, fingerprint string) error {
f.deleted = append(f.deleted, fingerprint)
return nil
}
func (f *apiKeyClient) ValidateKey(ctx context.Context, cred oci.Credentials) (oci.TenancyInfo, error) {
f.validated = append(f.validated, cred.Fingerprint)
f.validatedUser = cred.UserOCID
if f.validateErr != nil {
return oci.TenancyInfo{}, f.validateErr
}
return oci.TenancyInfo{Name: "t"}, nil
}
const testKeyPEM = "-----BEGIN RSA PRIVATE KEY-----\nfake\n-----END RSA PRIVATE KEY-----"
// seedApiKeyConfig 落一条可解密的配置,当前签名 key 指纹为 aa:bb。
func seedApiKeyConfig(t *testing.T, s *OciConfigService) *model.OciConfig {
t.Helper()
enc, err := s.cipher.EncryptString(testKeyPEM)
if err != nil {
t.Fatalf("encrypt: %v", err)
}
cfg := &model.OciConfig{
Alias: "t1", UserOCID: "ocid1.user.oc1..me", TenancyOCID: "ocid1.tenancy.oc1..t",
Fingerprint: "aa:bb", Region: "ap-tokyo-1", PrivateKeyEnc: enc,
}
if err := s.db.Create(cfg).Error; err != nil {
t.Fatalf("seed config: %v", err)
}
return cfg
}
func TestDeleteTenantUserApiKey(t *testing.T) {
tests := []struct {
name string
userID string
fingerprint string
wantErr error
wantDeleted bool
}{
{"当前签名 key 拒删", "ocid1.user.oc1..me", "aa:bb", ErrCurrentApiKey, false},
{"当前用户其他指纹可删", "ocid1.user.oc1..me", "cc:dd", nil, true},
{"其他用户同指纹可删", "ocid1.user.oc1..other", "aa:bb", nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fc := &apiKeyClient{}
s := newTestService(t, fc)
cfg := seedApiKeyConfig(t, s)
err := s.DeleteTenantUserApiKey(context.Background(), cfg.ID, tt.userID, tt.fingerprint)
if !errors.Is(err, tt.wantErr) {
t.Fatalf("err = %v, want %v", err, tt.wantErr)
}
if got := len(fc.deleted) > 0; got != tt.wantDeleted {
t.Fatalf("deleted = %v, want deleted=%v", fc.deleted, tt.wantDeleted)
}
})
}
}
func TestTenantUserApiKeys(t *testing.T) {
fc := &apiKeyClient{keys: []oci.TenantUserApiKey{{Fingerprint: "aa:bb", IsCurrent: true}}}
s := newTestService(t, fc)
cfg := seedApiKeyConfig(t, s)
items, err := s.TenantUserApiKeys(context.Background(), cfg.ID, "ocid1.user.oc1..me")
if err != nil {
t.Fatalf("list: %v", err)
}
if len(items) != 1 || !items[0].IsCurrent {
t.Fatalf("items = %+v", items)
}
if !strings.Contains(items[0].ConfigIni, "fingerprint=aa:bb") {
t.Fatalf("configIni missing fingerprint:\n%s", items[0].ConfigIni)
}
}
func TestAddTenantUserApiKey(t *testing.T) {
fc := &apiKeyClient{uploadFp: "11:22"}
s := newTestService(t, fc)
cfg := seedApiKeyConfig(t, s)
created, err := s.AddTenantUserApiKey(context.Background(), cfg.ID, "ocid1.user.oc1..other")
if err != nil {
t.Fatalf("add: %v", err)
}
if created.Fingerprint != "11:22" {
t.Fatalf("fingerprint = %q", created.Fingerprint)
}
block, _ := pem.Decode([]byte(created.PrivateKey))
if block == nil || block.Type != "RSA PRIVATE KEY" {
t.Fatalf("private key not PKCS#1 PEM")
}
if _, err := x509.ParsePKCS1PrivateKey(block.Bytes); err != nil {
t.Fatalf("parse private key: %v", err)
}
if !strings.Contains(fc.uploadedPub, "PUBLIC KEY") {
t.Fatalf("uploaded public key = %q", fc.uploadedPub)
}
for _, want := range []string{"user=ocid1.user.oc1..other", "fingerprint=11:22", "tenancy=ocid1.tenancy.oc1..t", "region=ap-tokyo-1"} {
if !strings.Contains(created.ConfigIni, want) {
t.Fatalf("configIni missing %q:\n%s", want, created.ConfigIni)
}
}
}
func TestActivateApiKey(t *testing.T) {
apiKeyVerifyDelay = time.Millisecond
const newKey = "-----BEGIN RSA PRIVATE KEY-----\nnew\n-----END RSA PRIVATE KEY-----"
tests := []struct {
name string
userID string
privateKey string
validateErr error
wantErr bool
wantFp string // 期望落库指纹
wantUser string // 期望落库签名用户
}{
{"成功:验证通过后落库,不删任何 key", "", newKey, nil, false, "11:22", "ocid1.user.oc1..me"},
{"切换用户:以新用户验证并一并落库", "ocid1.user.oc1..other", newKey, nil, false, "11:22", "ocid1.user.oc1..other"},
{"验证失败:配置不动", "", newKey, errors.New("401"), true, "aa:bb", "ocid1.user.oc1..me"},
{"私钥非 PEM:直接拒绝", "", "not-a-pem", nil, true, "aa:bb", "ocid1.user.oc1..me"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fc := &apiKeyClient{validateErr: tt.validateErr}
s := newTestService(t, fc)
cfg := seedApiKeyConfig(t, s)
err := s.ActivateApiKey(context.Background(), cfg.ID, tt.userID, "11:22", tt.privateKey)
if (err != nil) != tt.wantErr {
t.Fatalf("err = %v, wantErr %v", err, tt.wantErr)
}
var got model.OciConfig
if err := s.db.First(&got, cfg.ID).Error; err != nil {
t.Fatalf("reload: %v", err)
}
if got.Fingerprint != tt.wantFp {
t.Fatalf("fingerprint = %q, want %q", got.Fingerprint, tt.wantFp)
}
if got.UserOCID != tt.wantUser {
t.Fatalf("userOCID = %q, want %q", got.UserOCID, tt.wantUser)
}
if len(fc.deleted) != 0 {
t.Fatalf("deleted = %v, want none", fc.deleted)
}
if tt.wantErr {
return
}
// 成功路径:落库私钥可解密且与回传一致,验证调用用的是新指纹与目标用户
plain, err := s.cipher.DecryptString(got.PrivateKeyEnc)
if err != nil || plain != newKey {
t.Fatalf("persisted key mismatch (err=%v)", err)
}
if len(fc.validated) == 0 || fc.validated[0] != "11:22" {
t.Fatalf("validated = %v", fc.validated)
}
if fc.validatedUser != tt.wantUser {
t.Fatalf("validatedUser = %q, want %q", fc.validatedUser, tt.wantUser)
}
})
}
}