- Defined all 8 data models (Page, Widget, Bookmark, Note, TagAssociation, Group, Share, Preferences) - Implemented DynamoDB table creation for all tables with proper schemas - Added GSIs for efficient querying (UserBookmarksIndex, UserNotesIndex, TagItemsIndex, UserSharesIndex) - Comprehensive test coverage for all table schemas - Updated init-db command to create all tables
899 lines
25 KiB
Go
899 lines
25 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/aws"
|
|
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
|
|
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
|
)
|
|
|
|
func setupTestClient(t *testing.T) (*DynamoDBClient, context.Context) {
|
|
t.Helper()
|
|
|
|
// Set dummy AWS credentials for local DynamoDB
|
|
os.Setenv("AWS_ACCESS_KEY_ID", "dummy")
|
|
os.Setenv("AWS_SECRET_ACCESS_KEY", "dummy")
|
|
os.Setenv("AWS_REGION", "us-east-1")
|
|
|
|
endpoint := os.Getenv("DYNAMODB_ENDPOINT")
|
|
if endpoint == "" {
|
|
endpoint = "http://localhost:8000"
|
|
}
|
|
|
|
ctx := context.Background()
|
|
client, err := NewDynamoDBClient(ctx, endpoint)
|
|
if err != nil {
|
|
t.Skipf("Skipping test: DynamoDB not available: %v", err)
|
|
}
|
|
|
|
// Test connection by listing tables
|
|
_, err = client.client.ListTables(ctx, &dynamodb.ListTablesInput{})
|
|
if err != nil {
|
|
t.Skipf("Skipping test: Cannot connect to DynamoDB: %v", err)
|
|
}
|
|
|
|
return client, ctx
|
|
}
|
|
|
|
func TestNewDynamoDBClient(t *testing.T) {
|
|
client, _ := setupTestClient(t)
|
|
|
|
if client == nil {
|
|
t.Fatal("Expected non-nil client")
|
|
}
|
|
|
|
if client.client == nil {
|
|
t.Fatal("Expected non-nil underlying client")
|
|
}
|
|
}
|
|
|
|
func TestTransactWriteItems(t *testing.T) {
|
|
client, ctx := setupTestClient(t)
|
|
|
|
// Create test table
|
|
tableName := "TestTransactions"
|
|
createTestTable(t, ctx, client, tableName)
|
|
defer deleteTestTable(t, ctx, client, tableName)
|
|
|
|
// Test transaction with two puts
|
|
testID1 := "test-txn-1"
|
|
testID2 := "test-txn-2"
|
|
|
|
input := &dynamodb.TransactWriteItemsInput{
|
|
TransactItems: []types.TransactWriteItem{
|
|
{
|
|
Put: &types.Put{
|
|
TableName: aws.String(tableName),
|
|
Item: map[string]types.AttributeValue{
|
|
"id": &types.AttributeValueMemberS{Value: testID1},
|
|
"value": &types.AttributeValueMemberS{Value: "value1"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Put: &types.Put{
|
|
TableName: aws.String(tableName),
|
|
Item: map[string]types.AttributeValue{
|
|
"id": &types.AttributeValueMemberS{Value: testID2},
|
|
"value": &types.AttributeValueMemberS{Value: "value2"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
err := client.TransactWriteItems(ctx, input)
|
|
if err != nil {
|
|
t.Fatalf("TransactWriteItems failed: %v", err)
|
|
}
|
|
|
|
// Verify both items were written
|
|
output1, err := client.GetItem(ctx, &dynamodb.GetItemInput{
|
|
TableName: aws.String(tableName),
|
|
Key: map[string]types.AttributeValue{
|
|
"id": &types.AttributeValueMemberS{Value: testID1},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("GetItem failed: %v", err)
|
|
}
|
|
if len(output1.Item) == 0 {
|
|
t.Fatal("Expected item to exist after transaction")
|
|
}
|
|
|
|
output2, err := client.GetItem(ctx, &dynamodb.GetItemInput{
|
|
TableName: aws.String(tableName),
|
|
Key: map[string]types.AttributeValue{
|
|
"id": &types.AttributeValueMemberS{Value: testID2},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("GetItem failed: %v", err)
|
|
}
|
|
if len(output2.Item) == 0 {
|
|
t.Fatal("Expected item to exist after transaction")
|
|
}
|
|
}
|
|
|
|
func TestBatchGetItems(t *testing.T) {
|
|
client, ctx := setupTestClient(t)
|
|
|
|
// Create test table
|
|
tableName := "TestBatchGet"
|
|
createTestTable(t, ctx, client, tableName)
|
|
defer deleteTestTable(t, ctx, client, tableName)
|
|
|
|
// Put test items
|
|
testIDs := []string{"batch-1", "batch-2", "batch-3"}
|
|
for _, id := range testIDs {
|
|
err := client.PutItem(ctx, &dynamodb.PutItemInput{
|
|
TableName: aws.String(tableName),
|
|
Item: map[string]types.AttributeValue{
|
|
"id": &types.AttributeValueMemberS{Value: id},
|
|
"value": &types.AttributeValueMemberS{Value: "test-value"},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("PutItem failed: %v", err)
|
|
}
|
|
}
|
|
|
|
// Batch get items
|
|
keys := make([]map[string]types.AttributeValue, len(testIDs))
|
|
for i, id := range testIDs {
|
|
keys[i] = map[string]types.AttributeValue{
|
|
"id": &types.AttributeValueMemberS{Value: id},
|
|
}
|
|
}
|
|
|
|
output, err := client.BatchGetItems(ctx, &dynamodb.BatchGetItemInput{
|
|
RequestItems: map[string]types.KeysAndAttributes{
|
|
tableName: {
|
|
Keys: keys,
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("BatchGetItems failed: %v", err)
|
|
}
|
|
|
|
if len(output.Responses[tableName]) != len(testIDs) {
|
|
t.Fatalf("Expected %d items, got %d", len(testIDs), len(output.Responses[tableName]))
|
|
}
|
|
}
|
|
|
|
func TestBatchWriteItems(t *testing.T) {
|
|
client, ctx := setupTestClient(t)
|
|
|
|
// Create test table
|
|
tableName := "TestBatchWrite"
|
|
createTestTable(t, ctx, client, tableName)
|
|
defer deleteTestTable(t, ctx, client, tableName)
|
|
|
|
// Batch write items
|
|
testIDs := []string{"write-1", "write-2", "write-3"}
|
|
writeRequests := make([]types.WriteRequest, len(testIDs))
|
|
for i, id := range testIDs {
|
|
writeRequests[i] = types.WriteRequest{
|
|
PutRequest: &types.PutRequest{
|
|
Item: map[string]types.AttributeValue{
|
|
"id": &types.AttributeValueMemberS{Value: id},
|
|
"value": &types.AttributeValueMemberS{Value: "batch-value"},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
err := client.BatchWriteItems(ctx, &dynamodb.BatchWriteItemInput{
|
|
RequestItems: map[string][]types.WriteRequest{
|
|
tableName: writeRequests,
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("BatchWriteItems failed: %v", err)
|
|
}
|
|
|
|
// Verify items were written
|
|
for _, id := range testIDs {
|
|
output, err := client.GetItem(ctx, &dynamodb.GetItemInput{
|
|
TableName: aws.String(tableName),
|
|
Key: map[string]types.AttributeValue{
|
|
"id": &types.AttributeValueMemberS{Value: id},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("GetItem failed: %v", err)
|
|
}
|
|
if len(output.Item) == 0 {
|
|
t.Fatalf("Expected item %s to exist after batch write", id)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPutAndGetItem(t *testing.T) {
|
|
client, ctx := setupTestClient(t)
|
|
|
|
// Create test table
|
|
tableName := "TestPutGet"
|
|
createTestTable(t, ctx, client, tableName)
|
|
defer deleteTestTable(t, ctx, client, tableName)
|
|
|
|
// Put item
|
|
testID := "put-get-test"
|
|
err := client.PutItem(ctx, &dynamodb.PutItemInput{
|
|
TableName: aws.String(tableName),
|
|
Item: map[string]types.AttributeValue{
|
|
"id": &types.AttributeValueMemberS{Value: testID},
|
|
"value": &types.AttributeValueMemberS{Value: "test-value"},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("PutItem failed: %v", err)
|
|
}
|
|
|
|
// Get item
|
|
output, err := client.GetItem(ctx, &dynamodb.GetItemInput{
|
|
TableName: aws.String(tableName),
|
|
Key: map[string]types.AttributeValue{
|
|
"id": &types.AttributeValueMemberS{Value: testID},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("GetItem failed: %v", err)
|
|
}
|
|
|
|
if len(output.Item) == 0 {
|
|
t.Fatal("Expected item to exist")
|
|
}
|
|
|
|
valueAttr, ok := output.Item["value"]
|
|
if !ok {
|
|
t.Fatal("Expected 'value' attribute")
|
|
}
|
|
|
|
value := valueAttr.(*types.AttributeValueMemberS).Value
|
|
if value != "test-value" {
|
|
t.Fatalf("Expected value 'test-value', got '%s'", value)
|
|
}
|
|
}
|
|
|
|
func TestUpdateItem(t *testing.T) {
|
|
client, ctx := setupTestClient(t)
|
|
|
|
// Create test table
|
|
tableName := "TestUpdate"
|
|
createTestTable(t, ctx, client, tableName)
|
|
defer deleteTestTable(t, ctx, client, tableName)
|
|
|
|
// Put initial item
|
|
testID := "update-test"
|
|
err := client.PutItem(ctx, &dynamodb.PutItemInput{
|
|
TableName: aws.String(tableName),
|
|
Item: map[string]types.AttributeValue{
|
|
"id": &types.AttributeValueMemberS{Value: testID},
|
|
"value": &types.AttributeValueMemberS{Value: "initial"},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("PutItem failed: %v", err)
|
|
}
|
|
|
|
// Update item
|
|
_, err = client.UpdateItem(ctx, &dynamodb.UpdateItemInput{
|
|
TableName: aws.String(tableName),
|
|
Key: map[string]types.AttributeValue{
|
|
"id": &types.AttributeValueMemberS{Value: testID},
|
|
},
|
|
UpdateExpression: aws.String("SET #v = :val"),
|
|
ExpressionAttributeNames: map[string]string{
|
|
"#v": "value",
|
|
},
|
|
ExpressionAttributeValues: map[string]types.AttributeValue{
|
|
":val": &types.AttributeValueMemberS{Value: "updated"},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("UpdateItem failed: %v", err)
|
|
}
|
|
|
|
// Verify update
|
|
output, err := client.GetItem(ctx, &dynamodb.GetItemInput{
|
|
TableName: aws.String(tableName),
|
|
Key: map[string]types.AttributeValue{
|
|
"id": &types.AttributeValueMemberS{Value: testID},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("GetItem failed: %v", err)
|
|
}
|
|
|
|
value := output.Item["value"].(*types.AttributeValueMemberS).Value
|
|
if value != "updated" {
|
|
t.Fatalf("Expected value 'updated', got '%s'", value)
|
|
}
|
|
}
|
|
|
|
func TestDeleteItem(t *testing.T) {
|
|
client, ctx := setupTestClient(t)
|
|
|
|
// Create test table
|
|
tableName := "TestDelete"
|
|
createTestTable(t, ctx, client, tableName)
|
|
defer deleteTestTable(t, ctx, client, tableName)
|
|
|
|
// Put item
|
|
testID := "delete-test"
|
|
err := client.PutItem(ctx, &dynamodb.PutItemInput{
|
|
TableName: aws.String(tableName),
|
|
Item: map[string]types.AttributeValue{
|
|
"id": &types.AttributeValueMemberS{Value: testID},
|
|
"value": &types.AttributeValueMemberS{Value: "test"},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("PutItem failed: %v", err)
|
|
}
|
|
|
|
// Delete item
|
|
err = client.DeleteItem(ctx, &dynamodb.DeleteItemInput{
|
|
TableName: aws.String(tableName),
|
|
Key: map[string]types.AttributeValue{
|
|
"id": &types.AttributeValueMemberS{Value: testID},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("DeleteItem failed: %v", err)
|
|
}
|
|
|
|
// Verify deletion
|
|
output, err := client.GetItem(ctx, &dynamodb.GetItemInput{
|
|
TableName: aws.String(tableName),
|
|
Key: map[string]types.AttributeValue{
|
|
"id": &types.AttributeValueMemberS{Value: testID},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("GetItem failed: %v", err)
|
|
}
|
|
|
|
if len(output.Item) != 0 {
|
|
t.Fatal("Expected item to be deleted")
|
|
}
|
|
}
|
|
|
|
func TestQuery(t *testing.T) {
|
|
client, ctx := setupTestClient(t)
|
|
|
|
// Create test table
|
|
tableName := "TestQuery"
|
|
createTestTable(t, ctx, client, tableName)
|
|
defer deleteTestTable(t, ctx, client, tableName)
|
|
|
|
// Put test items
|
|
testIDs := []string{"query-1", "query-2", "query-3"}
|
|
for _, id := range testIDs {
|
|
err := client.PutItem(ctx, &dynamodb.PutItemInput{
|
|
TableName: aws.String(tableName),
|
|
Item: map[string]types.AttributeValue{
|
|
"id": &types.AttributeValueMemberS{Value: id},
|
|
"value": &types.AttributeValueMemberS{Value: "test"},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("PutItem failed: %v", err)
|
|
}
|
|
}
|
|
|
|
// Query for specific item
|
|
output, err := client.Query(ctx, &dynamodb.QueryInput{
|
|
TableName: aws.String(tableName),
|
|
KeyConditionExpression: aws.String("id = :id"),
|
|
ExpressionAttributeValues: map[string]types.AttributeValue{
|
|
":id": &types.AttributeValueMemberS{Value: "query-1"},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Query failed: %v", err)
|
|
}
|
|
|
|
if len(output.Items) != 1 {
|
|
t.Fatalf("Expected 1 item, got %d", len(output.Items))
|
|
}
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
func createTestTable(t *testing.T, ctx context.Context, client *DynamoDBClient, tableName string) {
|
|
t.Helper()
|
|
|
|
_, err := client.client.CreateTable(ctx, &dynamodb.CreateTableInput{
|
|
TableName: aws.String(tableName),
|
|
AttributeDefinitions: []types.AttributeDefinition{
|
|
{
|
|
AttributeName: aws.String("id"),
|
|
AttributeType: types.ScalarAttributeTypeS,
|
|
},
|
|
},
|
|
KeySchema: []types.KeySchemaElement{
|
|
{
|
|
AttributeName: aws.String("id"),
|
|
KeyType: types.KeyTypeHash,
|
|
},
|
|
},
|
|
BillingMode: types.BillingModePayPerRequest,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to create test table: %v", err)
|
|
}
|
|
|
|
// Wait for table to be active
|
|
waiter := dynamodb.NewTableExistsWaiter(client.client)
|
|
err = waiter.Wait(ctx, &dynamodb.DescribeTableInput{
|
|
TableName: aws.String(tableName),
|
|
}, 30*time.Second)
|
|
if err != nil {
|
|
t.Fatalf("Failed waiting for table to be active: %v", err)
|
|
}
|
|
}
|
|
|
|
func deleteTestTable(t *testing.T, ctx context.Context, client *DynamoDBClient, tableName string) {
|
|
t.Helper()
|
|
|
|
_, err := client.client.DeleteTable(ctx, &dynamodb.DeleteTableInput{
|
|
TableName: aws.String(tableName),
|
|
})
|
|
if err != nil {
|
|
t.Logf("Warning: Failed to delete test table: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestCreateUsersTable tests the Users table creation
|
|
func TestCreateUsersTable(t *testing.T) {
|
|
client, ctx := setupTestClient(t)
|
|
|
|
// Delete table if it exists
|
|
client.client.DeleteTable(ctx, &dynamodb.DeleteTableInput{
|
|
TableName: aws.String("Users"),
|
|
})
|
|
time.Sleep(1 * time.Second)
|
|
|
|
// Create table
|
|
err := client.CreateUsersTable(ctx)
|
|
if err != nil {
|
|
t.Fatalf("CreateUsersTable failed: %v", err)
|
|
}
|
|
|
|
// Verify table exists and has correct schema
|
|
output, err := client.client.DescribeTable(ctx, &dynamodb.DescribeTableInput{
|
|
TableName: aws.String("Users"),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("DescribeTable failed: %v", err)
|
|
}
|
|
|
|
// Verify key schema
|
|
if len(output.Table.KeySchema) != 1 {
|
|
t.Fatalf("Expected 1 key, got %d", len(output.Table.KeySchema))
|
|
}
|
|
if *output.Table.KeySchema[0].AttributeName != "user_id" {
|
|
t.Errorf("Expected partition key 'user_id', got '%s'", *output.Table.KeySchema[0].AttributeName)
|
|
}
|
|
|
|
// Test idempotency - calling again should not error
|
|
err = client.CreateUsersTable(ctx)
|
|
if err != nil {
|
|
t.Fatalf("CreateUsersTable should be idempotent: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestCreatePagesTable tests the Pages table creation with composite key
|
|
func TestCreatePagesTable(t *testing.T) {
|
|
client, ctx := setupTestClient(t)
|
|
|
|
// Delete table if it exists
|
|
client.client.DeleteTable(ctx, &dynamodb.DeleteTableInput{
|
|
TableName: aws.String("Pages"),
|
|
})
|
|
time.Sleep(1 * time.Second)
|
|
|
|
// Create table
|
|
err := client.CreatePagesTable(ctx)
|
|
if err != nil {
|
|
t.Fatalf("CreatePagesTable failed: %v", err)
|
|
}
|
|
|
|
// Verify table exists and has correct schema
|
|
output, err := client.client.DescribeTable(ctx, &dynamodb.DescribeTableInput{
|
|
TableName: aws.String("Pages"),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("DescribeTable failed: %v", err)
|
|
}
|
|
|
|
// Verify composite key schema
|
|
if len(output.Table.KeySchema) != 2 {
|
|
t.Fatalf("Expected 2 keys, got %d", len(output.Table.KeySchema))
|
|
}
|
|
if *output.Table.KeySchema[0].AttributeName != "user_id" {
|
|
t.Errorf("Expected partition key 'user_id', got '%s'", *output.Table.KeySchema[0].AttributeName)
|
|
}
|
|
if *output.Table.KeySchema[1].AttributeName != "page_id" {
|
|
t.Errorf("Expected sort key 'page_id', got '%s'", *output.Table.KeySchema[1].AttributeName)
|
|
}
|
|
}
|
|
|
|
// TestCreateWidgetsTable tests the Widgets table creation
|
|
func TestCreateWidgetsTable(t *testing.T) {
|
|
client, ctx := setupTestClient(t)
|
|
|
|
// Delete table if it exists
|
|
client.client.DeleteTable(ctx, &dynamodb.DeleteTableInput{
|
|
TableName: aws.String("Widgets"),
|
|
})
|
|
time.Sleep(1 * time.Second)
|
|
|
|
// Create table
|
|
err := client.CreateWidgetsTable(ctx)
|
|
if err != nil {
|
|
t.Fatalf("CreateWidgetsTable failed: %v", err)
|
|
}
|
|
|
|
// Verify table exists and has correct schema
|
|
output, err := client.client.DescribeTable(ctx, &dynamodb.DescribeTableInput{
|
|
TableName: aws.String("Widgets"),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("DescribeTable failed: %v", err)
|
|
}
|
|
|
|
// Verify composite key schema
|
|
if len(output.Table.KeySchema) != 2 {
|
|
t.Fatalf("Expected 2 keys, got %d", len(output.Table.KeySchema))
|
|
}
|
|
if *output.Table.KeySchema[0].AttributeName != "page_id" {
|
|
t.Errorf("Expected partition key 'page_id', got '%s'", *output.Table.KeySchema[0].AttributeName)
|
|
}
|
|
if *output.Table.KeySchema[1].AttributeName != "widget_id" {
|
|
t.Errorf("Expected sort key 'widget_id', got '%s'", *output.Table.KeySchema[1].AttributeName)
|
|
}
|
|
}
|
|
|
|
// TestCreateBookmarksTable tests the Bookmarks table creation with GSI
|
|
func TestCreateBookmarksTable(t *testing.T) {
|
|
client, ctx := setupTestClient(t)
|
|
|
|
// Delete table if it exists
|
|
client.client.DeleteTable(ctx, &dynamodb.DeleteTableInput{
|
|
TableName: aws.String("Bookmarks"),
|
|
})
|
|
time.Sleep(1 * time.Second)
|
|
|
|
// Create table
|
|
err := client.CreateBookmarksTable(ctx)
|
|
if err != nil {
|
|
t.Fatalf("CreateBookmarksTable failed: %v", err)
|
|
}
|
|
|
|
// Verify table exists and has correct schema
|
|
output, err := client.client.DescribeTable(ctx, &dynamodb.DescribeTableInput{
|
|
TableName: aws.String("Bookmarks"),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("DescribeTable failed: %v", err)
|
|
}
|
|
|
|
// Verify composite key schema
|
|
if len(output.Table.KeySchema) != 2 {
|
|
t.Fatalf("Expected 2 keys, got %d", len(output.Table.KeySchema))
|
|
}
|
|
if *output.Table.KeySchema[0].AttributeName != "widget_id" {
|
|
t.Errorf("Expected partition key 'widget_id', got '%s'", *output.Table.KeySchema[0].AttributeName)
|
|
}
|
|
if *output.Table.KeySchema[1].AttributeName != "bookmark_id" {
|
|
t.Errorf("Expected sort key 'bookmark_id', got '%s'", *output.Table.KeySchema[1].AttributeName)
|
|
}
|
|
|
|
// Verify GSI exists
|
|
if len(output.Table.GlobalSecondaryIndexes) != 1 {
|
|
t.Fatalf("Expected 1 GSI, got %d", len(output.Table.GlobalSecondaryIndexes))
|
|
}
|
|
gsi := output.Table.GlobalSecondaryIndexes[0]
|
|
if *gsi.IndexName != "UserBookmarksIndex" {
|
|
t.Errorf("Expected GSI name 'UserBookmarksIndex', got '%s'", *gsi.IndexName)
|
|
}
|
|
if len(gsi.KeySchema) != 2 {
|
|
t.Fatalf("Expected 2 GSI keys, got %d", len(gsi.KeySchema))
|
|
}
|
|
if *gsi.KeySchema[0].AttributeName != "user_id" {
|
|
t.Errorf("Expected GSI partition key 'user_id', got '%s'", *gsi.KeySchema[0].AttributeName)
|
|
}
|
|
if *gsi.KeySchema[1].AttributeName != "created_at" {
|
|
t.Errorf("Expected GSI sort key 'created_at', got '%s'", *gsi.KeySchema[1].AttributeName)
|
|
}
|
|
}
|
|
|
|
// TestCreateNotesTable tests the Notes table creation with GSI
|
|
func TestCreateNotesTable(t *testing.T) {
|
|
client, ctx := setupTestClient(t)
|
|
|
|
// Delete table if it exists
|
|
client.client.DeleteTable(ctx, &dynamodb.DeleteTableInput{
|
|
TableName: aws.String("Notes"),
|
|
})
|
|
time.Sleep(1 * time.Second)
|
|
|
|
// Create table
|
|
err := client.CreateNotesTable(ctx)
|
|
if err != nil {
|
|
t.Fatalf("CreateNotesTable failed: %v", err)
|
|
}
|
|
|
|
// Verify table exists and has correct schema
|
|
output, err := client.client.DescribeTable(ctx, &dynamodb.DescribeTableInput{
|
|
TableName: aws.String("Notes"),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("DescribeTable failed: %v", err)
|
|
}
|
|
|
|
// Verify composite key schema
|
|
if len(output.Table.KeySchema) != 2 {
|
|
t.Fatalf("Expected 2 keys, got %d", len(output.Table.KeySchema))
|
|
}
|
|
if *output.Table.KeySchema[0].AttributeName != "widget_id" {
|
|
t.Errorf("Expected partition key 'widget_id', got '%s'", *output.Table.KeySchema[0].AttributeName)
|
|
}
|
|
if *output.Table.KeySchema[1].AttributeName != "note_id" {
|
|
t.Errorf("Expected sort key 'note_id', got '%s'", *output.Table.KeySchema[1].AttributeName)
|
|
}
|
|
|
|
// Verify GSI exists
|
|
if len(output.Table.GlobalSecondaryIndexes) != 1 {
|
|
t.Fatalf("Expected 1 GSI, got %d", len(output.Table.GlobalSecondaryIndexes))
|
|
}
|
|
gsi := output.Table.GlobalSecondaryIndexes[0]
|
|
if *gsi.IndexName != "UserNotesIndex" {
|
|
t.Errorf("Expected GSI name 'UserNotesIndex', got '%s'", *gsi.IndexName)
|
|
}
|
|
}
|
|
|
|
// TestCreateTagAssociationsTable tests the TagAssociations table creation with GSI
|
|
func TestCreateTagAssociationsTable(t *testing.T) {
|
|
client, ctx := setupTestClient(t)
|
|
|
|
// Delete table if it exists
|
|
client.client.DeleteTable(ctx, &dynamodb.DeleteTableInput{
|
|
TableName: aws.String("TagAssociations"),
|
|
})
|
|
time.Sleep(1 * time.Second)
|
|
|
|
// Create table
|
|
err := client.CreateTagAssociationsTable(ctx)
|
|
if err != nil {
|
|
t.Fatalf("CreateTagAssociationsTable failed: %v", err)
|
|
}
|
|
|
|
// Verify table exists and has correct schema
|
|
output, err := client.client.DescribeTable(ctx, &dynamodb.DescribeTableInput{
|
|
TableName: aws.String("TagAssociations"),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("DescribeTable failed: %v", err)
|
|
}
|
|
|
|
// Verify composite key schema
|
|
if len(output.Table.KeySchema) != 2 {
|
|
t.Fatalf("Expected 2 keys, got %d", len(output.Table.KeySchema))
|
|
}
|
|
if *output.Table.KeySchema[0].AttributeName != "item_id" {
|
|
t.Errorf("Expected partition key 'item_id', got '%s'", *output.Table.KeySchema[0].AttributeName)
|
|
}
|
|
if *output.Table.KeySchema[1].AttributeName != "tag_name" {
|
|
t.Errorf("Expected sort key 'tag_name', got '%s'", *output.Table.KeySchema[1].AttributeName)
|
|
}
|
|
|
|
// Verify GSI exists
|
|
if len(output.Table.GlobalSecondaryIndexes) != 1 {
|
|
t.Fatalf("Expected 1 GSI, got %d", len(output.Table.GlobalSecondaryIndexes))
|
|
}
|
|
gsi := output.Table.GlobalSecondaryIndexes[0]
|
|
if *gsi.IndexName != "TagItemsIndex" {
|
|
t.Errorf("Expected GSI name 'TagItemsIndex', got '%s'", *gsi.IndexName)
|
|
}
|
|
if len(gsi.KeySchema) != 2 {
|
|
t.Fatalf("Expected 2 GSI keys, got %d", len(gsi.KeySchema))
|
|
}
|
|
if *gsi.KeySchema[0].AttributeName != "tag_name" {
|
|
t.Errorf("Expected GSI partition key 'tag_name', got '%s'", *gsi.KeySchema[0].AttributeName)
|
|
}
|
|
if *gsi.KeySchema[1].AttributeName != "user_id" {
|
|
t.Errorf("Expected GSI sort key 'user_id', got '%s'", *gsi.KeySchema[1].AttributeName)
|
|
}
|
|
|
|
// Verify projection type is INCLUDE
|
|
if gsi.Projection.ProjectionType != types.ProjectionTypeInclude {
|
|
t.Errorf("Expected projection type INCLUDE, got %v", gsi.Projection.ProjectionType)
|
|
}
|
|
}
|
|
|
|
// TestCreateGroupsTable tests the Groups table creation
|
|
func TestCreateGroupsTable(t *testing.T) {
|
|
client, ctx := setupTestClient(t)
|
|
|
|
// Delete table if it exists
|
|
client.client.DeleteTable(ctx, &dynamodb.DeleteTableInput{
|
|
TableName: aws.String("Groups"),
|
|
})
|
|
time.Sleep(1 * time.Second)
|
|
|
|
// Create table
|
|
err := client.CreateGroupsTable(ctx)
|
|
if err != nil {
|
|
t.Fatalf("CreateGroupsTable failed: %v", err)
|
|
}
|
|
|
|
// Verify table exists and has correct schema
|
|
output, err := client.client.DescribeTable(ctx, &dynamodb.DescribeTableInput{
|
|
TableName: aws.String("Groups"),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("DescribeTable failed: %v", err)
|
|
}
|
|
|
|
// Verify composite key schema
|
|
if len(output.Table.KeySchema) != 2 {
|
|
t.Fatalf("Expected 2 keys, got %d", len(output.Table.KeySchema))
|
|
}
|
|
if *output.Table.KeySchema[0].AttributeName != "widget_id" {
|
|
t.Errorf("Expected partition key 'widget_id', got '%s'", *output.Table.KeySchema[0].AttributeName)
|
|
}
|
|
if *output.Table.KeySchema[1].AttributeName != "group_id" {
|
|
t.Errorf("Expected sort key 'group_id', got '%s'", *output.Table.KeySchema[1].AttributeName)
|
|
}
|
|
}
|
|
|
|
// TestCreateSharedItemsTable tests the SharedItems table creation with GSI
|
|
func TestCreateSharedItemsTable(t *testing.T) {
|
|
client, ctx := setupTestClient(t)
|
|
|
|
// Delete table if it exists
|
|
client.client.DeleteTable(ctx, &dynamodb.DeleteTableInput{
|
|
TableName: aws.String("SharedItems"),
|
|
})
|
|
time.Sleep(1 * time.Second)
|
|
|
|
// Create table
|
|
err := client.CreateSharedItemsTable(ctx)
|
|
if err != nil {
|
|
t.Fatalf("CreateSharedItemsTable failed: %v", err)
|
|
}
|
|
|
|
// Verify table exists and has correct schema
|
|
output, err := client.client.DescribeTable(ctx, &dynamodb.DescribeTableInput{
|
|
TableName: aws.String("SharedItems"),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("DescribeTable failed: %v", err)
|
|
}
|
|
|
|
// Verify key schema
|
|
if len(output.Table.KeySchema) != 1 {
|
|
t.Fatalf("Expected 1 key, got %d", len(output.Table.KeySchema))
|
|
}
|
|
if *output.Table.KeySchema[0].AttributeName != "share_id" {
|
|
t.Errorf("Expected partition key 'share_id', got '%s'", *output.Table.KeySchema[0].AttributeName)
|
|
}
|
|
|
|
// Verify GSI exists
|
|
if len(output.Table.GlobalSecondaryIndexes) != 1 {
|
|
t.Fatalf("Expected 1 GSI, got %d", len(output.Table.GlobalSecondaryIndexes))
|
|
}
|
|
gsi := output.Table.GlobalSecondaryIndexes[0]
|
|
if *gsi.IndexName != "UserSharesIndex" {
|
|
t.Errorf("Expected GSI name 'UserSharesIndex', got '%s'", *gsi.IndexName)
|
|
}
|
|
if len(gsi.KeySchema) != 2 {
|
|
t.Fatalf("Expected 2 GSI keys, got %d", len(gsi.KeySchema))
|
|
}
|
|
if *gsi.KeySchema[0].AttributeName != "user_id" {
|
|
t.Errorf("Expected GSI partition key 'user_id', got '%s'", *gsi.KeySchema[0].AttributeName)
|
|
}
|
|
if *gsi.KeySchema[1].AttributeName != "created_at" {
|
|
t.Errorf("Expected GSI sort key 'created_at', got '%s'", *gsi.KeySchema[1].AttributeName)
|
|
}
|
|
}
|
|
|
|
// TestCreatePreferencesTable tests the Preferences table creation
|
|
func TestCreatePreferencesTable(t *testing.T) {
|
|
client, ctx := setupTestClient(t)
|
|
|
|
// Delete table if it exists
|
|
client.client.DeleteTable(ctx, &dynamodb.DeleteTableInput{
|
|
TableName: aws.String("Preferences"),
|
|
})
|
|
time.Sleep(1 * time.Second)
|
|
|
|
// Create table
|
|
err := client.CreatePreferencesTable(ctx)
|
|
if err != nil {
|
|
t.Fatalf("CreatePreferencesTable failed: %v", err)
|
|
}
|
|
|
|
// Verify table exists and has correct schema
|
|
output, err := client.client.DescribeTable(ctx, &dynamodb.DescribeTableInput{
|
|
TableName: aws.String("Preferences"),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("DescribeTable failed: %v", err)
|
|
}
|
|
|
|
// Verify key schema
|
|
if len(output.Table.KeySchema) != 1 {
|
|
t.Fatalf("Expected 1 key, got %d", len(output.Table.KeySchema))
|
|
}
|
|
if *output.Table.KeySchema[0].AttributeName != "user_id" {
|
|
t.Errorf("Expected partition key 'user_id', got '%s'", *output.Table.KeySchema[0].AttributeName)
|
|
}
|
|
}
|
|
|
|
// TestCreateAllTables tests creating all tables at once
|
|
func TestCreateAllTables(t *testing.T) {
|
|
client, ctx := setupTestClient(t)
|
|
|
|
// Delete all tables if they exist
|
|
tables := []string{
|
|
"Users", "Pages", "Widgets", "Bookmarks", "Notes",
|
|
"TagAssociations", "Groups", "SharedItems", "Preferences",
|
|
}
|
|
for _, table := range tables {
|
|
client.client.DeleteTable(ctx, &dynamodb.DeleteTableInput{
|
|
TableName: aws.String(table),
|
|
})
|
|
}
|
|
time.Sleep(2 * time.Second)
|
|
|
|
// Create all tables
|
|
createFuncs := []struct {
|
|
name string
|
|
create func(context.Context) error
|
|
}{
|
|
{"Users", client.CreateUsersTable},
|
|
{"Pages", client.CreatePagesTable},
|
|
{"Widgets", client.CreateWidgetsTable},
|
|
{"Bookmarks", client.CreateBookmarksTable},
|
|
{"Notes", client.CreateNotesTable},
|
|
{"TagAssociations", client.CreateTagAssociationsTable},
|
|
{"Groups", client.CreateGroupsTable},
|
|
{"SharedItems", client.CreateSharedItemsTable},
|
|
{"Preferences", client.CreatePreferencesTable},
|
|
}
|
|
|
|
for _, fn := range createFuncs {
|
|
err := fn.create(ctx)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create %s table: %v", fn.name, err)
|
|
}
|
|
}
|
|
|
|
// Verify all tables exist
|
|
listOutput, err := client.client.ListTables(ctx, &dynamodb.ListTablesInput{})
|
|
if err != nil {
|
|
t.Fatalf("ListTables failed: %v", err)
|
|
}
|
|
|
|
tableMap := make(map[string]bool)
|
|
for _, tableName := range listOutput.TableNames {
|
|
tableMap[tableName] = true
|
|
}
|
|
|
|
for _, table := range tables {
|
|
if !tableMap[table] {
|
|
t.Errorf("Expected table %s to exist", table)
|
|
}
|
|
}
|
|
}
|