Add Phase 2 artifact repository: types, blob store, gRPC service
Build the complete artifact pillar with five packages: - artifacts: Artifact, Snapshot, Citation, Publisher types with Get/Store DB methods, tag/category management, metadata ops, YAML import - blob: content-addressable store (SHA256, hierarchical dir layout) - proto: protobuf definitions (common.proto, artifacts.proto) with buf linting and code generation - server: gRPC ArtifactService implementation (create/get artifacts, store/retrieve blobs, manage tags/categories, search by tag) All FK insertion ordering is correct (parent rows before children). Full test coverage across artifacts, blob, and server packages. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
183
server/server_test.go
Normal file
183
server/server_test.go
Normal file
@@ -0,0 +1,183 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"git.wntrmute.dev/kyle/exo/blob"
|
||||
"git.wntrmute.dev/kyle/exo/db"
|
||||
pb "git.wntrmute.dev/kyle/exo/proto/exo/v1"
|
||||
)
|
||||
|
||||
func setup(t *testing.T) *ArtifactServer {
|
||||
t.Helper()
|
||||
dbPath := filepath.Join(t.TempDir(), "test.db")
|
||||
database, err := db.Open(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Open failed: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = database.Close() })
|
||||
if err := db.Migrate(database); err != nil {
|
||||
t.Fatalf("Migrate failed: %v", err)
|
||||
}
|
||||
blobStore := blob.NewStore(t.TempDir())
|
||||
return NewArtifactServer(database, blobStore)
|
||||
}
|
||||
|
||||
func TestCreateAndGetArtifact(t *testing.T) {
|
||||
srv := setup(t)
|
||||
ctx := context.Background()
|
||||
|
||||
createResp, err := srv.CreateArtifact(ctx, &pb.CreateArtifactRequest{
|
||||
Artifact: &pb.Artifact{
|
||||
Type: "Article",
|
||||
Citation: &pb.Citation{
|
||||
Title: "Test Article",
|
||||
Year: 2024,
|
||||
Published: "2024-01-15 00:00:00",
|
||||
Authors: []string{"Alice", "Bob"},
|
||||
Publisher: &pb.Publisher{Name: "Test Press", Address: "Testville"},
|
||||
Source: "https://example.com/article",
|
||||
},
|
||||
Tags: []string{"test", "grpc"},
|
||||
Categories: []string{"cs/testing"},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateArtifact failed: %v", err)
|
||||
}
|
||||
if createResp.Id == "" {
|
||||
t.Fatal("expected non-empty artifact ID")
|
||||
}
|
||||
|
||||
getResp, err := srv.GetArtifact(ctx, &pb.GetArtifactRequest{Id: createResp.Id})
|
||||
if err != nil {
|
||||
t.Fatalf("GetArtifact failed: %v", err)
|
||||
}
|
||||
|
||||
art := getResp.Artifact
|
||||
if art.Type != "Article" {
|
||||
t.Fatalf("type mismatch: got %q", art.Type)
|
||||
}
|
||||
if art.Citation.Title != "Test Article" {
|
||||
t.Fatalf("title mismatch: got %q", art.Citation.Title)
|
||||
}
|
||||
if len(art.Citation.Authors) != 2 {
|
||||
t.Fatalf("expected 2 authors, got %d", len(art.Citation.Authors))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateTagAndList(t *testing.T) {
|
||||
srv := setup(t)
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := srv.CreateTag(ctx, &pb.CreateTagRequest{Tag: "alpha"})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateTag failed: %v", err)
|
||||
}
|
||||
_, err = srv.CreateTag(ctx, &pb.CreateTagRequest{Tag: "beta"})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateTag failed: %v", err)
|
||||
}
|
||||
|
||||
resp, err := srv.ListTags(ctx, &pb.ListTagsRequest{})
|
||||
if err != nil {
|
||||
t.Fatalf("ListTags failed: %v", err)
|
||||
}
|
||||
if len(resp.Tags) != 2 {
|
||||
t.Fatalf("expected 2 tags, got %d", len(resp.Tags))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateCategoryAndList(t *testing.T) {
|
||||
srv := setup(t)
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := srv.CreateCategory(ctx, &pb.CreateCategoryRequest{Category: "cs/ai"})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateCategory failed: %v", err)
|
||||
}
|
||||
|
||||
resp, err := srv.ListCategories(ctx, &pb.ListCategoriesRequest{})
|
||||
if err != nil {
|
||||
t.Fatalf("ListCategories failed: %v", err)
|
||||
}
|
||||
if len(resp.Categories) != 1 || resp.Categories[0] != "cs/ai" {
|
||||
t.Fatalf("unexpected categories: %v", resp.Categories)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreBlobAndGet(t *testing.T) {
|
||||
srv := setup(t)
|
||||
ctx := context.Background()
|
||||
|
||||
storeResp, err := srv.StoreBlob(ctx, &pb.StoreBlobRequest{
|
||||
Format: "application/pdf",
|
||||
Data: []byte("fake PDF data"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("StoreBlob failed: %v", err)
|
||||
}
|
||||
if storeResp.Id == "" {
|
||||
t.Fatal("expected non-empty blob ID")
|
||||
}
|
||||
|
||||
getResp, err := srv.GetBlob(ctx, &pb.GetBlobRequest{Id: storeResp.Id})
|
||||
if err != nil {
|
||||
t.Fatalf("GetBlob failed: %v", err)
|
||||
}
|
||||
if string(getResp.Data) != "fake PDF data" {
|
||||
t.Fatalf("blob data mismatch: %q", getResp.Data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchByTag(t *testing.T) {
|
||||
srv := setup(t)
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := srv.CreateArtifact(ctx, &pb.CreateArtifactRequest{
|
||||
Artifact: &pb.Artifact{
|
||||
Type: "Paper",
|
||||
Citation: &pb.Citation{
|
||||
Title: "Searchable Paper",
|
||||
Year: 2024,
|
||||
Published: "2024-06-01 00:00:00",
|
||||
Publisher: &pb.Publisher{Name: "ACM", Address: "NYC"},
|
||||
Source: "test",
|
||||
},
|
||||
Tags: []string{"searchable"},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateArtifact failed: %v", err)
|
||||
}
|
||||
|
||||
resp, err := srv.SearchByTag(ctx, &pb.SearchByTagRequest{Tag: "searchable"})
|
||||
if err != nil {
|
||||
t.Fatalf("SearchByTag failed: %v", err)
|
||||
}
|
||||
if len(resp.ArtifactIds) != 1 {
|
||||
t.Fatalf("expected 1 result, got %d", len(resp.ArtifactIds))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateArtifactNil(t *testing.T) {
|
||||
srv := setup(t)
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := srv.CreateArtifact(ctx, &pb.CreateArtifactRequest{})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for nil artifact")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetArtifactMissing(t *testing.T) {
|
||||
srv := setup(t)
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := srv.GetArtifact(ctx, &pb.GetArtifactRequest{Id: "nonexistent"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing artifact")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user