diff --git a/backend/Makefile b/backend/Makefile index 44fdb7d..a6503a6 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -12,6 +12,10 @@ ORG_PATH := ${CURDIR} ENVS = \ export TF_VAR_HOME=$(HOME); +.PHONY: update +update: ## Update go.mod + go mod tidy + .PHONY: gen gen: ## Generate GraphQL implementations printf "${GREEN} Clean up files\n\n"; \ @@ -30,7 +34,7 @@ fmt: ## Format code .PHONY: test test: ## Run tests - printf "${GREEN}Run all tests\n\n"; \ + printf "${GREEN}Run all tests\n\n${WHITE}"; \ ${ENV} go test -v ./...; printf "${GREEN}Done\n"; \ diff --git a/backend/db/migrations/20240704123000_create_users_and_todos_tables.sql b/backend/db/migrations/20240704123000_create_users_and_todos_tables.sql index 0b8f9c0..03b9966 100644 --- a/backend/db/migrations/20240704123000_create_users_and_todos_tables.sql +++ b/backend/db/migrations/20240704123000_create_users_and_todos_tables.sql @@ -7,17 +7,17 @@ CREATE TABLE users name VARCHAR(255) NOT NULL ); -CREATE TABLE todos +CREATE TABLE cards ( - id VARCHAR(255) PRIMARY KEY, - text VARCHAR(255) NOT NULL, - done BOOLEAN NOT NULL DEFAULT FALSE, - user_id VARCHAR(255) NOT NULL, - FOREIGN KEY (user_id) REFERENCES users (id) + id SERIAL PRIMARY KEY, + front TEXT NOT NULL, + back TEXT NOT NULL, + review_date TIMESTAMP NOT NULL, + interval_days INT NOT NULL DEFAULT 1 ); -- +goose Down -- SQL section 'Down' is executed when this migration is rolled back. -DROP TABLE IF EXISTS todos; +DROP TABLE IF EXISTS cards; DROP TABLE IF EXISTS users; diff --git a/backend/go.mod b/backend/go.mod index 5f3761c..4e512ef 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -5,6 +5,7 @@ go 1.22.5 require ( github.com/99designs/gqlgen v0.17.49 github.com/labstack/echo/v4 v4.12.0 + github.com/stretchr/testify v1.9.0 github.com/testcontainers/testcontainers-go v0.31.0 github.com/vektah/gqlparser/v2 v2.5.16 gorm.io/driver/postgres v1.5.9 @@ -21,6 +22,7 @@ require ( github.com/containerd/containerd v1.7.15 // indirect github.com/containerd/log v0.1.0 // indirect github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.5.0 // indirect github.com/docker/docker v25.0.5+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect @@ -42,6 +44,7 @@ require ( github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/klauspost/compress v1.16.0 // indirect + github.com/kr/text v0.2.0 // indirect github.com/labstack/gommon v0.4.2 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.7 // indirect @@ -56,6 +59,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/shirou/gopsutil/v3 v3.23.12 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect @@ -82,4 +86,5 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20230731190214-cbb8c96f2d6d // indirect google.golang.org/grpc v1.58.3 // indirect google.golang.org/protobuf v1.34.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index 3fc8e2e..ee8ff72 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -28,6 +28,7 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -86,6 +87,10 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4= github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0= github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= @@ -121,6 +126,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= @@ -237,6 +244,8 @@ google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSs google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/backend/graph/generated.go b/backend/graph/generated.go index a6830ff..7258cc1 100644 --- a/backend/graph/generated.go +++ b/backend/graph/generated.go @@ -48,17 +48,21 @@ type DirectiveRoot struct { type ComplexityRoot struct { Card struct { - Done func(childComplexity int) int - ID func(childComplexity int) int - Text func(childComplexity int) int - User func(childComplexity int) int + Back func(childComplexity int) int + Front func(childComplexity int) int + ID func(childComplexity int) int + IntervalDays func(childComplexity int) int + ReviewDate func(childComplexity int) int } Mutation struct { - CreateTodo func(childComplexity int, input model.NewCard) int + CreateCard func(childComplexity int, input model.NewCard) int + DeleteCard func(childComplexity int, id string) int + UpdateCard func(childComplexity int, id string, input model.NewCard) int } Query struct { + Card func(childComplexity int, id string) int Cards func(childComplexity int) int } @@ -69,10 +73,13 @@ type ComplexityRoot struct { } type MutationResolver interface { - CreateTodo(ctx context.Context, input model.NewCard) (*model.Card, error) + CreateCard(ctx context.Context, input model.NewCard) (*model.Card, error) + UpdateCard(ctx context.Context, id string, input model.NewCard) (*model.Card, error) + DeleteCard(ctx context.Context, id string) (bool, error) } type QueryResolver interface { Cards(ctx context.Context) ([]*model.Card, error) + Card(ctx context.Context, id string) (*model.Card, error) } type executableSchema struct { @@ -94,12 +101,19 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in _ = ec switch typeName + "." + field { - case "Card.done": - if e.complexity.Card.Done == nil { + case "Card.back": + if e.complexity.Card.Back == nil { + break + } + + return e.complexity.Card.Back(childComplexity), true + + case "Card.front": + if e.complexity.Card.Front == nil { break } - return e.complexity.Card.Done(childComplexity), true + return e.complexity.Card.Front(childComplexity), true case "Card.id": if e.complexity.Card.ID == nil { @@ -108,31 +122,67 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Card.ID(childComplexity), true - case "Card.text": - if e.complexity.Card.Text == nil { + case "Card.interval_days": + if e.complexity.Card.IntervalDays == nil { + break + } + + return e.complexity.Card.IntervalDays(childComplexity), true + + case "Card.review_date": + if e.complexity.Card.ReviewDate == nil { + break + } + + return e.complexity.Card.ReviewDate(childComplexity), true + + case "Mutation.createCard": + if e.complexity.Mutation.CreateCard == nil { break } - return e.complexity.Card.Text(childComplexity), true + args, err := ec.field_Mutation_createCard_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.CreateCard(childComplexity, args["input"].(model.NewCard)), true - case "Card.user": - if e.complexity.Card.User == nil { + case "Mutation.deleteCard": + if e.complexity.Mutation.DeleteCard == nil { break } - return e.complexity.Card.User(childComplexity), true + args, err := ec.field_Mutation_deleteCard_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } - case "Mutation.createTodo": - if e.complexity.Mutation.CreateTodo == nil { + return e.complexity.Mutation.DeleteCard(childComplexity, args["id"].(string)), true + + case "Mutation.updateCard": + if e.complexity.Mutation.UpdateCard == nil { break } - args, err := ec.field_Mutation_createTodo_args(context.TODO(), rawArgs) + args, err := ec.field_Mutation_updateCard_args(context.TODO(), rawArgs) if err != nil { return 0, false } - return e.complexity.Mutation.CreateTodo(childComplexity, args["input"].(model.NewCard)), true + return e.complexity.Mutation.UpdateCard(childComplexity, args["id"].(string), args["input"].(model.NewCard)), true + + case "Query.card": + if e.complexity.Query.Card == nil { + break + } + + args, err := ec.field_Query_card_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Query.Card(childComplexity, args["id"].(string)), true case "Query.cards": if e.complexity.Query.Cards == nil { @@ -280,7 +330,7 @@ var parsedSchema = gqlparser.MustLoadSchema(sources...) // region ***************************** args.gotpl ***************************** -func (ec *executionContext) field_Mutation_createTodo_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { +func (ec *executionContext) field_Mutation_createCard_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} var arg0 model.NewCard @@ -295,6 +345,45 @@ func (ec *executionContext) field_Mutation_createTodo_args(ctx context.Context, return args, nil } +func (ec *executionContext) field_Mutation_deleteCard_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 string + if tmp, ok := rawArgs["id"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) + arg0, err = ec.unmarshalNID2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["id"] = arg0 + return args, nil +} + +func (ec *executionContext) field_Mutation_updateCard_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 string + if tmp, ok := rawArgs["id"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) + arg0, err = ec.unmarshalNID2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["id"] = arg0 + var arg1 model.NewCard + if tmp, ok := rawArgs["input"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) + arg1, err = ec.unmarshalNNewCard2backendᚋgraphᚋmodelᚐNewCard(ctx, tmp) + if err != nil { + return nil, err + } + } + args["input"] = arg1 + return args, nil +} + func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -310,6 +399,21 @@ func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs return args, nil } +func (ec *executionContext) field_Query_card_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 string + if tmp, ok := rawArgs["id"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) + arg0, err = ec.unmarshalNID2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["id"] = arg0 + return args, nil +} + func (ec *executionContext) field___Type_enumValues_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -392,8 +496,8 @@ func (ec *executionContext) fieldContext_Card_id(_ context.Context, field graphq return fc, nil } -func (ec *executionContext) _Card_text(ctx context.Context, field graphql.CollectedField, obj *model.Card) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Card_text(ctx, field) +func (ec *executionContext) _Card_front(ctx context.Context, field graphql.CollectedField, obj *model.Card) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Card_front(ctx, field) if err != nil { return graphql.Null } @@ -406,7 +510,7 @@ func (ec *executionContext) _Card_text(ctx context.Context, field graphql.Collec }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.Text, nil + return obj.Front, nil }) if err != nil { ec.Error(ctx, err) @@ -423,7 +527,7 @@ func (ec *executionContext) _Card_text(ctx context.Context, field graphql.Collec return ec.marshalNString2string(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Card_text(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Card_front(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Card", Field: field, @@ -436,8 +540,8 @@ func (ec *executionContext) fieldContext_Card_text(_ context.Context, field grap return fc, nil } -func (ec *executionContext) _Card_done(ctx context.Context, field graphql.CollectedField, obj *model.Card) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Card_done(ctx, field) +func (ec *executionContext) _Card_back(ctx context.Context, field graphql.CollectedField, obj *model.Card) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Card_back(ctx, field) if err != nil { return graphql.Null } @@ -450,7 +554,7 @@ func (ec *executionContext) _Card_done(ctx context.Context, field graphql.Collec }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.Done, nil + return obj.Back, nil }) if err != nil { ec.Error(ctx, err) @@ -462,26 +566,26 @@ func (ec *executionContext) _Card_done(ctx context.Context, field graphql.Collec } return graphql.Null } - res := resTmp.(bool) + res := resTmp.(string) fc.Result = res - return ec.marshalNBoolean2bool(ctx, field.Selections, res) + return ec.marshalNString2string(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Card_done(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Card_back(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Card", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Boolean does not have child fields") + return nil, errors.New("field of type String does not have child fields") }, } return fc, nil } -func (ec *executionContext) _Card_user(ctx context.Context, field graphql.CollectedField, obj *model.Card) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Card_user(ctx, field) +func (ec *executionContext) _Card_review_date(ctx context.Context, field graphql.CollectedField, obj *model.Card) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Card_review_date(ctx, field) if err != nil { return graphql.Null } @@ -494,7 +598,7 @@ func (ec *executionContext) _Card_user(ctx context.Context, field graphql.Collec }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.User, nil + return obj.ReviewDate, nil }) if err != nil { ec.Error(ctx, err) @@ -506,32 +610,137 @@ func (ec *executionContext) _Card_user(ctx context.Context, field graphql.Collec } return graphql.Null } - res := resTmp.(*model.User) + res := resTmp.(string) fc.Result = res - return ec.marshalNUser2ᚖbackendᚋgraphᚋmodelᚐUser(ctx, field.Selections, res) + return ec.marshalNString2string(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Card_user(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Card_review_date(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Card", Field: field, IsMethod: false, IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Card_interval_days(ctx context.Context, field graphql.CollectedField, obj *model.Card) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Card_interval_days(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.IntervalDays, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(int) + fc.Result = res + return ec.marshalNInt2int(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Card_interval_days(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Card", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _Mutation_createCard(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_createCard(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().CreateCard(rctx, fc.Args["input"].(model.NewCard)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*model.Card) + fc.Result = res + return ec.marshalNCard2ᚖbackendᚋgraphᚋmodelᚐCard(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_createCard(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "id": - return ec.fieldContext_User_id(ctx, field) - case "name": - return ec.fieldContext_User_name(ctx, field) + return ec.fieldContext_Card_id(ctx, field) + case "front": + return ec.fieldContext_Card_front(ctx, field) + case "back": + return ec.fieldContext_Card_back(ctx, field) + case "review_date": + return ec.fieldContext_Card_review_date(ctx, field) + case "interval_days": + return ec.fieldContext_Card_interval_days(ctx, field) } - return nil, fmt.Errorf("no field named %q was found under type User", field.Name) + return nil, fmt.Errorf("no field named %q was found under type Card", field.Name) }, } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_createCard_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } return fc, nil } -func (ec *executionContext) _Mutation_createTodo(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Mutation_createTodo(ctx, field) +func (ec *executionContext) _Mutation_updateCard(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_updateCard(ctx, field) if err != nil { return graphql.Null } @@ -544,7 +753,7 @@ func (ec *executionContext) _Mutation_createTodo(ctx context.Context, field grap }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().CreateTodo(rctx, fc.Args["input"].(model.NewCard)) + return ec.resolvers.Mutation().UpdateCard(rctx, fc.Args["id"].(string), fc.Args["input"].(model.NewCard)) }) if err != nil { ec.Error(ctx, err) @@ -561,7 +770,7 @@ func (ec *executionContext) _Mutation_createTodo(ctx context.Context, field grap return ec.marshalNCard2ᚖbackendᚋgraphᚋmodelᚐCard(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Mutation_createTodo(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Mutation_updateCard(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Mutation", Field: field, @@ -571,12 +780,14 @@ func (ec *executionContext) fieldContext_Mutation_createTodo(ctx context.Context switch field.Name { case "id": return ec.fieldContext_Card_id(ctx, field) - case "text": - return ec.fieldContext_Card_text(ctx, field) - case "done": - return ec.fieldContext_Card_done(ctx, field) - case "user": - return ec.fieldContext_Card_user(ctx, field) + case "front": + return ec.fieldContext_Card_front(ctx, field) + case "back": + return ec.fieldContext_Card_back(ctx, field) + case "review_date": + return ec.fieldContext_Card_review_date(ctx, field) + case "interval_days": + return ec.fieldContext_Card_interval_days(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Card", field.Name) }, @@ -588,7 +799,62 @@ func (ec *executionContext) fieldContext_Mutation_createTodo(ctx context.Context } }() ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Mutation_createTodo_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + if fc.Args, err = ec.field_Mutation_updateCard_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + +func (ec *executionContext) _Mutation_deleteCard(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_deleteCard(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().DeleteCard(rctx, fc.Args["id"].(string)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + fc.Result = res + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_deleteCard(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Boolean does not have child fields") + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_deleteCard_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { ec.Error(ctx, err) return fc, err } @@ -636,12 +902,14 @@ func (ec *executionContext) fieldContext_Query_cards(_ context.Context, field gr switch field.Name { case "id": return ec.fieldContext_Card_id(ctx, field) - case "text": - return ec.fieldContext_Card_text(ctx, field) - case "done": - return ec.fieldContext_Card_done(ctx, field) - case "user": - return ec.fieldContext_Card_user(ctx, field) + case "front": + return ec.fieldContext_Card_front(ctx, field) + case "back": + return ec.fieldContext_Card_back(ctx, field) + case "review_date": + return ec.fieldContext_Card_review_date(ctx, field) + case "interval_days": + return ec.fieldContext_Card_interval_days(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Card", field.Name) }, @@ -649,6 +917,70 @@ func (ec *executionContext) fieldContext_Query_cards(_ context.Context, field gr return fc, nil } +func (ec *executionContext) _Query_card(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query_card(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().Card(rctx, fc.Args["id"].(string)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*model.Card) + fc.Result = res + return ec.marshalOCard2ᚖbackendᚋgraphᚋmodelᚐCard(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Query_card(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Card_id(ctx, field) + case "front": + return ec.fieldContext_Card_front(ctx, field) + case "back": + return ec.fieldContext_Card_back(ctx, field) + case "review_date": + return ec.fieldContext_Card_review_date(ctx, field) + case "interval_days": + return ec.fieldContext_Card_interval_days(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Card", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query_card_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Query___type(ctx, field) if err != nil { @@ -2646,27 +2978,45 @@ func (ec *executionContext) unmarshalInputNewCard(ctx context.Context, obj inter asMap[k] = v } - fieldsInOrder := [...]string{"text", "userId"} + if _, present := asMap["interval_days"]; !present { + asMap["interval_days"] = 1 + } + + fieldsInOrder := [...]string{"front", "back", "review_date", "interval_days"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { continue } switch k { - case "text": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("text")) + case "front": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("front")) data, err := ec.unmarshalNString2string(ctx, v) if err != nil { return it, err } - it.Text = data - case "userId": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("userId")) + it.Front = data + case "back": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("back")) data, err := ec.unmarshalNString2string(ctx, v) if err != nil { return it, err } - it.UserID = data + it.Back = data + case "review_date": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("review_date")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.ReviewDate = data + case "interval_days": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("interval_days")) + data, err := ec.unmarshalOInt2ᚖint(ctx, v) + if err != nil { + return it, err + } + it.IntervalDays = data } } @@ -2697,18 +3047,23 @@ func (ec *executionContext) _Card(ctx context.Context, sel ast.SelectionSet, obj if out.Values[i] == graphql.Null { out.Invalids++ } - case "text": - out.Values[i] = ec._Card_text(ctx, field, obj) + case "front": + out.Values[i] = ec._Card_front(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "back": + out.Values[i] = ec._Card_back(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "done": - out.Values[i] = ec._Card_done(ctx, field, obj) + case "review_date": + out.Values[i] = ec._Card_review_date(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "user": - out.Values[i] = ec._Card_user(ctx, field, obj) + case "interval_days": + out.Values[i] = ec._Card_interval_days(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } @@ -2754,9 +3109,23 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("Mutation") - case "createTodo": + case "createCard": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { - return ec._Mutation_createTodo(ctx, field) + return ec._Mutation_createCard(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "updateCard": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_updateCard(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "deleteCard": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_deleteCard(ctx, field) }) if out.Values[i] == graphql.Null { out.Invalids++ @@ -2824,6 +3193,25 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "card": + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_card(ctx, field) + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) case "__type": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { @@ -3314,6 +3702,21 @@ func (ec *executionContext) marshalNID2string(ctx context.Context, sel ast.Selec return res } +func (ec *executionContext) unmarshalNInt2int(ctx context.Context, v interface{}) (int, error) { + res, err := graphql.UnmarshalInt(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNInt2int(ctx context.Context, sel ast.SelectionSet, v int) graphql.Marshaler { + res := graphql.MarshalInt(v) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + func (ec *executionContext) unmarshalNNewCard2backendᚋgraphᚋmodelᚐNewCard(ctx context.Context, v interface{}) (model.NewCard, error) { res, err := ec.unmarshalInputNewCard(ctx, v) return res, graphql.ErrorOnPath(ctx, err) @@ -3334,16 +3737,6 @@ func (ec *executionContext) marshalNString2string(ctx context.Context, sel ast.S return res } -func (ec *executionContext) marshalNUser2ᚖbackendᚋgraphᚋmodelᚐUser(ctx context.Context, sel ast.SelectionSet, v *model.User) graphql.Marshaler { - if v == nil { - if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - ec.Errorf(ctx, "the requested element is null which the schema does not allow") - } - return graphql.Null - } - return ec._User(ctx, sel, v) -} - func (ec *executionContext) marshalN__Directive2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirective(ctx context.Context, sel ast.SelectionSet, v introspection.Directive) graphql.Marshaler { return ec.___Directive(ctx, sel, &v) } @@ -3623,6 +4016,29 @@ func (ec *executionContext) marshalOBoolean2ᚖbool(ctx context.Context, sel ast return res } +func (ec *executionContext) marshalOCard2ᚖbackendᚋgraphᚋmodelᚐCard(ctx context.Context, sel ast.SelectionSet, v *model.Card) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._Card(ctx, sel, v) +} + +func (ec *executionContext) unmarshalOInt2ᚖint(ctx context.Context, v interface{}) (*int, error) { + if v == nil { + return nil, nil + } + res, err := graphql.UnmarshalInt(v) + return &res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalOInt2ᚖint(ctx context.Context, sel ast.SelectionSet, v *int) graphql.Marshaler { + if v == nil { + return graphql.Null + } + res := graphql.MarshalInt(*v) + return res +} + func (ec *executionContext) unmarshalOString2ᚖstring(ctx context.Context, v interface{}) (*string, error) { if v == nil { return nil, nil diff --git a/backend/graph/model/models_gen.go b/backend/graph/model/models_gen.go index 0743967..51a3b29 100644 --- a/backend/graph/model/models_gen.go +++ b/backend/graph/model/models_gen.go @@ -3,18 +3,21 @@ package model type Card struct { - ID string `json:"id"` - Text string `json:"text"` - Done bool `json:"done"` - User *User `json:"user"` + ID string `json:"id"` + Front string `json:"front"` + Back string `json:"back"` + ReviewDate string `json:"review_date"` + IntervalDays int `json:"interval_days"` } type Mutation struct { } type NewCard struct { - Text string `json:"text"` - UserID string `json:"userId"` + Front string `json:"front"` + Back string `json:"back"` + ReviewDate string `json:"review_date"` + IntervalDays *int `json:"interval_days,omitempty"` } type Query struct { diff --git a/backend/graph/schema.graphqls b/backend/graph/schema.graphqls index d608a6f..229cef1 100644 --- a/backend/graph/schema.graphqls +++ b/backend/graph/schema.graphqls @@ -2,27 +2,33 @@ # # https://gqlgen.com/getting-started/ -type Card { - id: ID! - text: String! - done: Boolean! - user: User! -} - type User { id: ID! name: String! } -type Query { - cards: [Card!]! +type Card { + id: ID! + front: String! + back: String! + review_date: String! + interval_days: Int! } input NewCard { - text: String! - userId: String! + front: String! + back: String! + review_date: String! + interval_days: Int = 1 } -type Mutation { - createTodo(input: NewCard!): Card! +type Query { + cards: [Card!]! + card(id: ID!): Card } + +type Mutation { + createCard(input: NewCard!): Card! + updateCard(id: ID!, input: NewCard!): Card! + deleteCard(id: ID!): Boolean! +} \ No newline at end of file diff --git a/backend/graph/schema.resolvers.go b/backend/graph/schema.resolvers.go index 1591266..9c5653d 100644 --- a/backend/graph/schema.resolvers.go +++ b/backend/graph/schema.resolvers.go @@ -10,9 +10,19 @@ import ( "fmt" ) -// CreateTodo is the resolver for the createTodo field. -func (r *mutationResolver) CreateTodo(ctx context.Context, input model.NewCard) (*model.Card, error) { - panic(fmt.Errorf("not implemented: CreateTodo - createTodo")) +// CreateCard is the resolver for the createCard field. +func (r *mutationResolver) CreateCard(ctx context.Context, input model.NewCard) (*model.Card, error) { + panic(fmt.Errorf("not implemented: CreateCard - createCard")) +} + +// UpdateCard is the resolver for the updateCard field. +func (r *mutationResolver) UpdateCard(ctx context.Context, id string, input model.NewCard) (*model.Card, error) { + panic(fmt.Errorf("not implemented: UpdateCard - updateCard")) +} + +// DeleteCard is the resolver for the deleteCard field. +func (r *mutationResolver) DeleteCard(ctx context.Context, id string) (bool, error) { + panic(fmt.Errorf("not implemented: DeleteCard - deleteCard")) } // Cards is the resolver for the cards field. @@ -20,6 +30,11 @@ func (r *queryResolver) Cards(ctx context.Context) ([]*model.Card, error) { panic(fmt.Errorf("not implemented: Cards - cards")) } +// Card is the resolver for the card field. +func (r *queryResolver) Card(ctx context.Context, id string) (*model.Card, error) { + panic(fmt.Errorf("not implemented: Card - card")) +} + // Mutation returns MutationResolver implementation. func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} } diff --git a/backend/pkg/flashcard/flashcard_system.go b/backend/pkg/flashcard/flashcard_system.go new file mode 100644 index 0000000..c14e000 --- /dev/null +++ b/backend/pkg/flashcard/flashcard_system.go @@ -0,0 +1,35 @@ +package flashcard + +import ( + "gorm.io/gorm" + "time" +) + +// Card struct represents a flashcard with necessary fields +type Card struct { + ID uint `gorm:"primaryKey"` + Front string `gorm:"not null"` + Back string `gorm:"not null"` + ReviewDate time.Time `gorm:"not null"` + IntervalDays int `gorm:"not null;default:1"` +} + +// GetDueCards retrieves flashcards that are due for review +func GetDueCards(db *gorm.DB) ([]Card, error) { + var cards []Card + now := time.Now() + result := db.Where("review_date <= ?", now).Find(&cards) + return cards, result.Error +} + +// UpdateCardReview updates the review date and interval of a flashcard +func UpdateCardReview(card *Card, db *gorm.DB) error { + card.IntervalDays *= 2 + card.ReviewDate = time.Now().AddDate(0, 0, card.IntervalDays) + return db.Save(card).Error +} + +// MigrateDB performs the database migration for the Card struct +func MigrateDB(db *gorm.DB) error { + return db.AutoMigrate(&Card{}) +} diff --git a/backend/pkg/flashcard/flashcard_system_test.go b/backend/pkg/flashcard/flashcard_system_test.go new file mode 100644 index 0000000..c7d896d --- /dev/null +++ b/backend/pkg/flashcard/flashcard_system_test.go @@ -0,0 +1,117 @@ +package flashcard + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" + _ "gorm.io/driver/postgres" + "gorm.io/gorm" + + "backend/pkg/repository" +) + +func setupTestDB(t *testing.T) (*gorm.DB, func()) { + t.Helper() + + ctx := context.Background() + req := testcontainers.ContainerRequest{ + Image: "postgres:latest", + ExposedPorts: []string{"5432/tcp"}, + Env: map[string]string{ + "POSTGRES_USER": "test", + "POSTGRES_PASSWORD": "test", + "POSTGRES_DB": "test", + }, + WaitingFor: wait.ForListeningPort("5432/tcp"), + } + postgresContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + }) + if err != nil { + t.Fatalf("could not start container: %v", err) + } + + host, err := postgresContainer.Host(ctx) + if err != nil { + t.Fatalf("could not get container host: %v", err) + } + + port, err := postgresContainer.MappedPort(ctx, "5432/tcp") + if err != nil { + t.Fatalf("could not get container port: %v", err) + } + + dbConfig := repository.DBConfig{ + Host: host, + Port: port.Port(), + User: "test", + Password: "test", + DBName: "test", + SSLMode: "disable", + } + + pg := repository.NewPostgres(dbConfig) + if err := pg.Open(); err != nil { + t.Fatalf("could not connect to database: %v", err) + } + + if err := MigrateDB(pg.DB); err != nil { + t.Fatalf("could not migrate database: %v", err) + } + + return pg.DB, func() { + postgresContainer.Terminate(ctx) + } +} + +func TestGetDueCards(t *testing.T) { + db, teardown := setupTestDB(t) + defer teardown() + + // Create a card that is due for review + dueCard := Card{ + Front: "Test Front", + Back: "Test Back", + ReviewDate: time.Now().AddDate(0, 0, -1), // yesterday + IntervalDays: 1, + } + db.Create(&dueCard) + + // Test GetDueCards + cards, err := GetDueCards(db) + assert.NoError(t, err) + assert.Len(t, cards, 1) + assert.Equal(t, dueCard.Front, cards[0].Front) + assert.Equal(t, dueCard.Back, cards[0].Back) +} + +func TestUpdateCardReview(t *testing.T) { + db, teardown := setupTestDB(t) + defer teardown() + + // Create a card that is due for review + card := Card{ + Front: "Test Front", + Back: "Test Back", + ReviewDate: time.Now().AddDate(0, 0, -1), // yesterday + IntervalDays: 1, + } + db.Create(&card) + + // Test UpdateCardReview + err := UpdateCardReview(&card, db) + assert.NoError(t, err) + + // Retrieve updated card + var updatedCard Card + db.First(&updatedCard, card.ID) + + assert.Equal(t, card.ID, updatedCard.ID) + assert.Equal(t, 2, updatedCard.IntervalDays) + assert.True(t, updatedCard.ReviewDate.After(time.Now())) +}