import { INestApplication } from "@nestjs/common"; import { Test } from "@nestjs/testing"; import request from "supertest"; import { AppModule } from "@/modules/app.module"; describe("Auth - Refresh Token Flow (e2e)", () => { let app: INestApplication; let accessToken: string; let refreshToken: string; let expiresAt: string; beforeAll(async () => { const moduleRef = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleRef.createNestApplication(); await app.init(); }); afterAll(async () => { await app.close(); }); describe("Registration with refresh token", () => { it("should register a new user and return access token, refresh token, and expiry", async () => { const registerMutation = ` mutation Register($name: String!, $email: String!, $password: String!) { register(name: $name, email: $email, password: $password) { access_token refresh_token expires_at user { id email name } } } `; const response = await request(app.getHttpServer()) .post("/graphql") .send({ query: registerMutation, variables: { name: "Test User", email: `test-${Date.now()}@example.com`, password: "password123", }, }) .expect(200); expect(response.body.errors).toBeUndefined(); expect(response.body.data.register).toBeDefined(); expect(response.body.data.register.access_token).toBeDefined(); expect(response.body.data.register.refresh_token).toBeDefined(); expect(response.body.data.register.expires_at).toBeDefined(); expect(response.body.data.register.user).toBeDefined(); // Store tokens for later tests accessToken = response.body.data.register.access_token; refreshToken = response.body.data.register.refresh_token; expiresAt = response.body.data.register.expires_at; // Validate expiry is in the future const expiryDate = new Date(expiresAt); expect(expiryDate.getTime()).toBeGreaterThan(Date.now()); }); }); describe("Login with refresh token", () => { const testEmail = `login-test-${Date.now()}@example.com`; const testPassword = "password123"; beforeAll(async () => { // Register a user first const registerMutation = ` mutation Register($name: String!, $email: String!, $password: String!) { register(name: $name, email: $email, password: $password) { access_token } } `; await request(app.getHttpServer()) .post("/graphql") .send({ query: registerMutation, variables: { name: "Login Test User", email: testEmail, password: testPassword, }, }); }); it("should login and return access token, refresh token, and expiry", async () => { const loginMutation = ` mutation Login($email: String!, $password: String!) { login(email: $email, password: $password) { access_token refresh_token expires_at user { id email name } } } `; const response = await request(app.getHttpServer()) .post("/graphql") .send({ query: loginMutation, variables: { email: testEmail, password: testPassword, }, }) .expect(200); expect(response.body.errors).toBeUndefined(); expect(response.body.data.login).toBeDefined(); expect(response.body.data.login.access_token).toBeDefined(); expect(response.body.data.login.refresh_token).toBeDefined(); expect(response.body.data.login.expires_at).toBeDefined(); expect(response.body.data.login.user.email).toBe(testEmail); }); }); describe("Refresh Token Mutation", () => { it("should refresh access token using refresh token", async () => { const refreshMutation = ` mutation RefreshToken($refresh_token: String!) { refreshToken(refresh_token: $refresh_token) { access_token refresh_token expires_at } } `; const response = await request(app.getHttpServer()) .post("/graphql") .send({ query: refreshMutation, variables: { refresh_token: refreshToken, }, }) .expect(200); expect(response.body.errors).toBeUndefined(); expect(response.body.data.refreshToken).toBeDefined(); expect(response.body.data.refreshToken.access_token).toBeDefined(); expect(response.body.data.refreshToken.refresh_token).toBeDefined(); expect(response.body.data.refreshToken.expires_at).toBeDefined(); // New tokens should be different from old ones (JWT includes iat timestamp) expect(response.body.data.refreshToken.access_token).toBeDefined(); expect(response.body.data.refreshToken.refresh_token).toBeDefined(); expect(typeof response.body.data.refreshToken.access_token).toBe( "string", ); expect(typeof response.body.data.refreshToken.refresh_token).toBe( "string", ); // Update tokens for next test accessToken = response.body.data.refreshToken.access_token; refreshToken = response.body.data.refreshToken.refresh_token; }); it("should fail with invalid refresh token", async () => { const refreshMutation = ` mutation RefreshToken($refresh_token: String!) { refreshToken(refresh_token: $refresh_token) { access_token refresh_token expires_at } } `; const response = await request(app.getHttpServer()) .post("/graphql") .send({ query: refreshMutation, variables: { refresh_token: "invalid-token", }, }) .expect(200); expect(response.body.errors).toBeDefined(); expect(response.body.errors[0].message).toContain( "Invalid or expired refresh token", ); }); it("should fail when using access token as refresh token", async () => { const refreshMutation = ` mutation RefreshToken($refresh_token: String!) { refreshToken(refresh_token: $refresh_token) { access_token refresh_token expires_at } } `; const response = await request(app.getHttpServer()) .post("/graphql") .send({ query: refreshMutation, variables: { refresh_token: accessToken, // Using access token instead of refresh token }, }) .expect(200); expect(response.body.errors).toBeDefined(); expect(response.body.errors[0].message).toContain( "Invalid or expired refresh token", ); }); }); describe("Protected Route with Refreshed Token", () => { it("should access protected route with new access token", async () => { const meQuery = ` query Me { me { id email name } } `; const response = await request(app.getHttpServer()) .post("/graphql") .set("Authorization", `Bearer ${accessToken}`) .send({ query: meQuery, }) .expect(200); expect(response.body.errors).toBeUndefined(); expect(response.body.data.me).toBeDefined(); expect(response.body.data.me.email).toBeDefined(); }); }); describe("Token Expiry Validation", () => { it("should return expiry time in ISO 8601 format", async () => { const registerMutation = ` mutation Register($name: String!, $email: String!, $password: String!) { register(name: $name, email: $email, password: $password) { expires_at } } `; const response = await request(app.getHttpServer()) .post("/graphql") .send({ query: registerMutation, variables: { name: "Expiry Test User", email: `expiry-test-${Date.now()}@example.com`, password: "password123", }, }) .expect(200); const expiryDate = new Date(response.body.data.register.expires_at); // Should be a valid date expect(expiryDate.toString()).not.toBe("Invalid Date"); // Should be in the future expect(expiryDate.getTime()).toBeGreaterThan(Date.now()); // Should be approximately 15 minutes in the future (default config) const fifteenMinutesFromNow = Date.now() + 15 * 60 * 1000; const timeDiff = Math.abs(expiryDate.getTime() - fifteenMinutesFromNow); // Allow 10 seconds tolerance for test execution time expect(timeDiff).toBeLessThan(10000); }); }); });