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 收到的指纹序列 } 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) 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 privateKey string validateErr error wantErr bool wantFp string // 期望落库指纹 }{ {"成功:验证通过后落库,不删任何 key", newKey, nil, false, "11:22"}, {"验证失败:配置不动", newKey, errors.New("401"), true, "aa:bb"}, {"私钥非 PEM:直接拒绝", "not-a-pem", nil, true, "aa:bb"}, } 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, "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 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) } }) } }