從 edge-ai-platform POC 轉為正式產品的雲端後端,含以下整合階段:
- Phase 0:雛形骨架 — `cmd/api-server` (REST :3721) + `cmd/remote-proxy`
(tunnel :3800 / internal :3801) 雙 binary 共用 internal/,沿用 POC 的
WebSocket+yamux tunnel 協定但解耦 relay 與 API
- Phase 0.6:OIDC BFF 接 Innovedus Member Center
- internal/oidc package(coreos/go-oidc + PKCE S256 + state + nonce)
- internal/usersession package(HMAC-SHA256 cookie + RotateSessionID
防 session fixation, OWASP ASVS V3.2.1)
- 4 個 OIDC handler(/api/auth/login|callback|me|logout)+ AuthMiddleware
- 完全拔除 StaticAuthProvider,OIDC 是唯一認證路徑
- 9 個 ADR(含 ADR-010 BFF / ADR-011 取代 static auth /
ADR-012 pending session shared cookie / ADR-013 PKCE-only public client)
- Phase 0.7:A1 改造 + security audit 修復
- OIDC ClientSecret 變選填,支援 stage MC 的 public PKCE-only client
(AuthStyleInParams 強制 token endpoint 不送 client_secret)
- 預留 ServiceClient* 欄位給未來 client_credentials grant
- 移除 13+ 處 resolveUserID(uc, StaticUserID) fallback 改 strict mode
(Audit C1:multi-tenant 隔離破口)
- Pairing exchange MarkUsed 失敗 abort + revoke session token(Audit M3)
- 新增 all_endpoints_require_auth_test 整合測試(51 endpoint × 401)
驗證:go test -race -count=3 ./... 17 packages 全綠 / go vet 0 warning
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
121 lines
3.2 KiB
Go
121 lines
3.2 KiB
Go
package device
|
||
|
||
import (
|
||
"context"
|
||
"testing"
|
||
|
||
"github.com/stretchr/testify/assert"
|
||
"github.com/stretchr/testify/require"
|
||
)
|
||
|
||
func TestInMemoryRepository_SaveAndGet(t *testing.T) {
|
||
ctx := context.Background()
|
||
r := NewInMemoryRepository()
|
||
|
||
d := &Device{
|
||
ID: "dev-1",
|
||
OwnerUserID: "user-1",
|
||
Name: "Lab KL520",
|
||
DeviceType: "kl520",
|
||
SerialNumber: "KL520-AAA",
|
||
RemoteStatus: RemoteStatusOffline,
|
||
Status: USBStatusUnknown,
|
||
}
|
||
require.NoError(t, r.Save(ctx, d))
|
||
|
||
got, err := r.Get(ctx, "dev-1")
|
||
require.NoError(t, err)
|
||
assert.Equal(t, "Lab KL520", got.Name)
|
||
assert.False(t, got.CreatedAt.IsZero())
|
||
assert.False(t, got.UpdatedAt.IsZero())
|
||
}
|
||
|
||
func TestInMemoryRepository_Get_NotFound(t *testing.T) {
|
||
r := NewInMemoryRepository()
|
||
_, err := r.Get(context.Background(), "nope")
|
||
assert.ErrorIs(t, err, ErrNotFound)
|
||
}
|
||
|
||
func TestInMemoryRepository_Save_RequiresID(t *testing.T) {
|
||
r := NewInMemoryRepository()
|
||
err := r.Save(context.Background(), &Device{Name: "no-id"})
|
||
assert.Error(t, err)
|
||
}
|
||
|
||
func TestInMemoryRepository_GetBySerial(t *testing.T) {
|
||
ctx := context.Background()
|
||
r := NewInMemoryRepository()
|
||
|
||
require.NoError(t, r.Save(ctx, &Device{
|
||
ID: "dev-1", OwnerUserID: "user-A", SerialNumber: "S-1",
|
||
}))
|
||
require.NoError(t, r.Save(ctx, &Device{
|
||
ID: "dev-2", OwnerUserID: "user-B", SerialNumber: "S-1",
|
||
}))
|
||
|
||
got, err := r.GetBySerial(ctx, "user-A", "S-1")
|
||
require.NoError(t, err)
|
||
assert.Equal(t, "dev-1", got.ID)
|
||
|
||
_, err = r.GetBySerial(ctx, "user-C", "S-1")
|
||
assert.ErrorIs(t, err, ErrNotFound)
|
||
}
|
||
|
||
func TestInMemoryRepository_List_ByOwner(t *testing.T) {
|
||
ctx := context.Background()
|
||
r := NewInMemoryRepository()
|
||
|
||
require.NoError(t, r.Save(ctx, &Device{ID: "a", OwnerUserID: "u1"}))
|
||
require.NoError(t, r.Save(ctx, &Device{ID: "b", OwnerUserID: "u1"}))
|
||
require.NoError(t, r.Save(ctx, &Device{ID: "c", OwnerUserID: "u2"}))
|
||
|
||
listU1, err := r.List(ctx, "u1")
|
||
require.NoError(t, err)
|
||
assert.Len(t, listU1, 2)
|
||
|
||
listU3, err := r.List(ctx, "u3")
|
||
require.NoError(t, err)
|
||
assert.Empty(t, listU3)
|
||
}
|
||
|
||
func TestInMemoryRepository_Delete_SoftDelete(t *testing.T) {
|
||
ctx := context.Background()
|
||
r := NewInMemoryRepository()
|
||
|
||
require.NoError(t, r.Save(ctx, &Device{ID: "dev-1", OwnerUserID: "u"}))
|
||
|
||
require.NoError(t, r.Delete(ctx, "dev-1"))
|
||
|
||
// Get 應該找不到
|
||
_, err := r.Get(ctx, "dev-1")
|
||
assert.ErrorIs(t, err, ErrNotFound)
|
||
|
||
// List 也不該列出
|
||
list, _ := r.List(ctx, "u")
|
||
assert.Empty(t, list)
|
||
|
||
// 再次 Delete 應回 ErrNotFound(已軟刪除)
|
||
assert.ErrorIs(t, r.Delete(ctx, "dev-1"), ErrNotFound)
|
||
}
|
||
|
||
func TestInMemoryRepository_Save_PreservesCreatedAt(t *testing.T) {
|
||
ctx := context.Background()
|
||
r := NewInMemoryRepository()
|
||
|
||
require.NoError(t, r.Save(ctx, &Device{ID: "dev-1", OwnerUserID: "u"}))
|
||
first, err := r.Get(ctx, "dev-1")
|
||
require.NoError(t, err)
|
||
createdAt := first.CreatedAt
|
||
|
||
// 更新(應保留 CreatedAt)
|
||
updated := *first
|
||
updated.Name = "Updated"
|
||
require.NoError(t, r.Save(ctx, &updated))
|
||
|
||
got, err := r.Get(ctx, "dev-1")
|
||
require.NoError(t, err)
|
||
assert.Equal(t, "Updated", got.Name)
|
||
assert.Equal(t, createdAt, got.CreatedAt, "CreatedAt 應保留原值")
|
||
assert.True(t, got.UpdatedAt.After(createdAt) || got.UpdatedAt.Equal(createdAt))
|
||
}
|